Merge pull request #8 from cooljith91112/feature/font-rendering
Fix font antialiasing and improve UX
This commit is contained in:
@@ -142,7 +142,7 @@ int main( int argn, const char** argc ) {
|
|||||||
splashInit();
|
splashInit();
|
||||||
bool splashDone = false;
|
bool splashDone = false;
|
||||||
|
|
||||||
while ( !splashDone && !WindowShouldClose() ) {
|
while ( !splashDone ) {
|
||||||
float delta = GetFrameTime();
|
float delta = GetFrameTime();
|
||||||
splashDone = splashUpdate( delta );
|
splashDone = splashUpdate( delta );
|
||||||
splashDraw();
|
splashDraw();
|
||||||
|
|||||||
32
src/text.c
32
src/text.c
@@ -185,7 +185,9 @@ Load font from file into GPU memory (VRAM)
|
|||||||
*/
|
*/
|
||||||
int ltextLoadFont( lua_State* L ) {
|
int ltextLoadFont( lua_State* L ) {
|
||||||
if ( FileExists( luaL_checkstring( L, 1 ) ) ) {
|
if ( FileExists( luaL_checkstring( L, 1 ) ) ) {
|
||||||
uluaPushFont( L, LoadFont( lua_tostring( L, 1 ) ) );
|
Font font = LoadFont( lua_tostring( L, 1 ) );
|
||||||
|
SetTextureFilter( font.texture, TEXTURE_FILTER_POINT );
|
||||||
|
uluaPushFont( L, font );
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -207,16 +209,19 @@ int ltextLoadFontEx( lua_State* L ) {
|
|||||||
int fontSize = luaL_checkinteger( L, 2 );
|
int fontSize = luaL_checkinteger( L, 2 );
|
||||||
|
|
||||||
if ( FileExists( luaL_checkstring( L, 1 ) ) ) {
|
if ( FileExists( luaL_checkstring( L, 1 ) ) ) {
|
||||||
|
Font font;
|
||||||
if ( lua_istable( L, 3 ) ) {
|
if ( lua_istable( L, 3 ) ) {
|
||||||
int codepointCount = uluaGetTableLen( L, 3 );
|
int codepointCount = uluaGetTableLen( L, 3 );
|
||||||
int codepoints[ codepointCount ];
|
int codepoints[ codepointCount ];
|
||||||
|
|
||||||
getCodepoints( L, codepoints, 3 );
|
getCodepoints( L, codepoints, 3 );
|
||||||
uluaPushFont( L, LoadFontEx( lua_tostring( L, 1 ), fontSize, codepoints, codepointCount ) );
|
font = LoadFontEx( lua_tostring( L, 1 ), fontSize, codepoints, codepointCount );
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
uluaPushFont( L, LoadFontEx( lua_tostring( L, 1 ), fontSize, NULL, 0 ) );
|
else {
|
||||||
|
font = LoadFontEx( lua_tostring( L, 1 ), fontSize, NULL, 0 );
|
||||||
|
}
|
||||||
|
SetTextureFilter( font.texture, TEXTURE_FILTER_POINT );
|
||||||
|
uluaPushFont( L, font );
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -238,7 +243,9 @@ int ltextLoadFontFromImage( lua_State* L ) {
|
|||||||
Color key = uluaGetColor( L, 2 );
|
Color key = uluaGetColor( L, 2 );
|
||||||
int firstChar = luaL_checkinteger( L, 3 );
|
int firstChar = luaL_checkinteger( L, 3 );
|
||||||
|
|
||||||
uluaPushFont( L, LoadFontFromImage( *image, key, firstChar ) );
|
Font font = LoadFontFromImage( *image, key, firstChar );
|
||||||
|
SetTextureFilter( font.texture, TEXTURE_FILTER_POINT );
|
||||||
|
uluaPushFont( L, font );
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -255,17 +262,20 @@ int ltextLoadFontFromMemory( lua_State* L ) {
|
|||||||
Buffer* fileData = uluaGetBuffer( L, 2 );
|
Buffer* fileData = uluaGetBuffer( L, 2 );
|
||||||
int fontSize = luaL_checkinteger( L, 3 );
|
int fontSize = luaL_checkinteger( L, 3 );
|
||||||
|
|
||||||
|
Font font;
|
||||||
if ( lua_istable( L, 4 ) ) {
|
if ( lua_istable( L, 4 ) ) {
|
||||||
int codepointCount = uluaGetTableLen( L, 4 );
|
int codepointCount = uluaGetTableLen( L, 4 );
|
||||||
int codepoints[ codepointCount ];
|
int codepoints[ codepointCount ];
|
||||||
|
|
||||||
getCodepoints( L, codepoints, 4 );
|
getCodepoints( L, codepoints, 4 );
|
||||||
uluaPushFont( L, LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, codepoints, codepointCount ) );
|
font = LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, codepoints, codepointCount );
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
/* If no codepoints provided. */
|
else {
|
||||||
uluaPushFont( L, LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, NULL, 0 ) );
|
/* If no codepoints provided. */
|
||||||
|
font = LoadFontFromMemory( fileType, fileData->data, fileData->size, fontSize, NULL, 0 );
|
||||||
|
}
|
||||||
|
SetTextureFilter( font.texture, TEXTURE_FILTER_POINT );
|
||||||
|
uluaPushFont( L, font );
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
316
template/DIALOG_STATE_PATTERN.md
Normal file
316
template/DIALOG_STATE_PATTERN.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
# Dialog State Pattern
|
||||||
|
|
||||||
|
This document explains how to implement Zelda-style dialog systems using the GameState push/pop stack pattern.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The GameState system supports a **state stack** that allows you to temporarily push a new state (like a dialog) on top of the current game state, then pop back when done. This is perfect for:
|
||||||
|
|
||||||
|
- Dialog systems (Zelda-style text boxes)
|
||||||
|
- Pause menus
|
||||||
|
- Inventory screens
|
||||||
|
- Any UI that needs exclusive input control
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```lua
|
||||||
|
GameState.push(newState) -- Pushes current state to stack, switches to new state
|
||||||
|
GameState.pop() -- Returns to previous state from stack
|
||||||
|
```
|
||||||
|
|
||||||
|
When you `push()` a state:
|
||||||
|
- Current state's `leave()` is called
|
||||||
|
- New state's `enter()` is called
|
||||||
|
- Current state remains in memory on the stack
|
||||||
|
|
||||||
|
When you `pop()`:
|
||||||
|
- Current state's `leave()` is called
|
||||||
|
- Previous state's `resume()` is called (not `enter()`)
|
||||||
|
- Previous state gets control back
|
||||||
|
|
||||||
|
## Example: Dialog System
|
||||||
|
|
||||||
|
### Step 1: Create Dialog State
|
||||||
|
|
||||||
|
Create `states/dialog.lua`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local Object = require("lib.classic")
|
||||||
|
local GameState = require("lib.gamestate")
|
||||||
|
local DialogState = Object:extend()
|
||||||
|
|
||||||
|
function DialogState:new(dialogData)
|
||||||
|
self.texts = dialogData.texts or {"Default dialog text"}
|
||||||
|
self.currentIndex = 1
|
||||||
|
self.characterName = dialogData.name or ""
|
||||||
|
self.portrait = dialogData.portrait or nil
|
||||||
|
self.textSpeed = dialogData.textSpeed or 50 -- chars per second
|
||||||
|
self.currentCharIndex = 0
|
||||||
|
self.displayedText = ""
|
||||||
|
self.isComplete = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function DialogState:enter(previous)
|
||||||
|
print("Dialog opened")
|
||||||
|
self.currentCharIndex = 0
|
||||||
|
self.displayedText = ""
|
||||||
|
self.isComplete = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function DialogState:update(dt)
|
||||||
|
local currentText = self.texts[self.currentIndex]
|
||||||
|
|
||||||
|
-- Animate text reveal
|
||||||
|
if not self.isComplete then
|
||||||
|
self.currentCharIndex = self.currentCharIndex + self.textSpeed * dt
|
||||||
|
if self.currentCharIndex >= #currentText then
|
||||||
|
self.currentCharIndex = #currentText
|
||||||
|
self.isComplete = true
|
||||||
|
end
|
||||||
|
self.displayedText = currentText:sub(1, math.floor(self.currentCharIndex))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle input
|
||||||
|
if RL.IsKeyPressed(RL.KEY_ENTER) or RL.IsKeyPressed(RL.KEY_SPACE) then
|
||||||
|
if not self.isComplete then
|
||||||
|
-- Skip to end of current text
|
||||||
|
self.currentCharIndex = #currentText
|
||||||
|
self.displayedText = currentText
|
||||||
|
self.isComplete = true
|
||||||
|
else
|
||||||
|
-- Move to next text or close dialog
|
||||||
|
if self.currentIndex < #self.texts then
|
||||||
|
self.currentIndex = self.currentIndex + 1
|
||||||
|
self.currentCharIndex = 0
|
||||||
|
self.displayedText = ""
|
||||||
|
self.isComplete = false
|
||||||
|
else
|
||||||
|
-- Dialog finished, return to game
|
||||||
|
GameState.pop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DialogState:draw()
|
||||||
|
local screenSize = RL.GetScreenSize()
|
||||||
|
local boxHeight = 200
|
||||||
|
local boxY = screenSize[2] - boxHeight - 20
|
||||||
|
local padding = 20
|
||||||
|
|
||||||
|
-- Draw dialog box
|
||||||
|
RL.DrawRectangle({0, boxY, screenSize[1], boxHeight}, {20, 20, 30, 240})
|
||||||
|
RL.DrawRectangleLines({0, boxY, screenSize[1], boxHeight}, 3, RL.WHITE)
|
||||||
|
|
||||||
|
-- Draw character name if present
|
||||||
|
if self.characterName ~= "" then
|
||||||
|
local nameBoxWidth = 200
|
||||||
|
local nameBoxHeight = 40
|
||||||
|
RL.DrawRectangle({padding, boxY - nameBoxHeight + 5, nameBoxWidth, nameBoxHeight}, {40, 40, 50, 255})
|
||||||
|
RL.DrawRectangleLines({padding, boxY - nameBoxHeight + 5, nameBoxWidth, nameBoxHeight}, 2, RL.WHITE)
|
||||||
|
RL.DrawText(self.characterName, {padding + 15, boxY - nameBoxHeight + 15}, 20, RL.WHITE)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw portrait if present
|
||||||
|
local textX = padding + 10
|
||||||
|
if self.portrait then
|
||||||
|
RL.DrawTexture(self.portrait, {padding + 10, boxY + padding}, RL.WHITE)
|
||||||
|
textX = textX + self.portrait.width + 20
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw text
|
||||||
|
RL.DrawText(self.displayedText, {textX, boxY + padding}, 20, RL.WHITE)
|
||||||
|
|
||||||
|
-- Draw continue indicator
|
||||||
|
if self.isComplete then
|
||||||
|
local indicator = "▼"
|
||||||
|
if self.currentIndex < #self.texts then
|
||||||
|
indicator = "Press ENTER to continue"
|
||||||
|
else
|
||||||
|
indicator = "Press ENTER to close"
|
||||||
|
end
|
||||||
|
local indicatorSize = 16
|
||||||
|
local indicatorWidth = RL.MeasureText(indicator, indicatorSize)
|
||||||
|
RL.DrawText(indicator, {screenSize[1] - indicatorWidth - padding, boxY + boxHeight - padding - 20}, indicatorSize, RL.LIGHTGRAY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DialogState:leave()
|
||||||
|
print("Dialog closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
return DialogState
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Use Dialog in Game State
|
||||||
|
|
||||||
|
Modify `states/game.lua`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local Object = require("lib.classic")
|
||||||
|
local GameState = require("lib.gamestate")
|
||||||
|
local Animation = require("lib.animation")
|
||||||
|
local DialogState = require("states.dialog")
|
||||||
|
local GameState_Class = Object:extend()
|
||||||
|
|
||||||
|
-- ... (Player class code) ...
|
||||||
|
|
||||||
|
function GameState_Class:new()
|
||||||
|
self.player = nil
|
||||||
|
self.paused = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function GameState_Class:enter(previous)
|
||||||
|
print("Entered game state")
|
||||||
|
|
||||||
|
local screenSize = RL.GetScreenSize()
|
||||||
|
self.player = Player(screenSize[1] / 2 - 16, screenSize[2] / 2 - 16)
|
||||||
|
|
||||||
|
-- Example: Show dialog when entering game (for testing)
|
||||||
|
-- Remove this after testing
|
||||||
|
local welcomeDialog = DialogState({
|
||||||
|
name = "System",
|
||||||
|
texts = {
|
||||||
|
"Welcome to the game!",
|
||||||
|
"Use WASD or Arrow keys to move around.",
|
||||||
|
"Press ENTER to continue through dialogs."
|
||||||
|
},
|
||||||
|
textSpeed = 30
|
||||||
|
})
|
||||||
|
GameState.push(welcomeDialog)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GameState_Class:update(dt)
|
||||||
|
-- ESC pauses game
|
||||||
|
if RL.IsKeyPressed(RL.KEY_ESCAPE) then
|
||||||
|
self.paused = not self.paused
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.paused then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Example: Press T to trigger dialog (for testing)
|
||||||
|
if RL.IsKeyPressed(RL.KEY_T) then
|
||||||
|
local testDialog = DialogState({
|
||||||
|
name = "NPC",
|
||||||
|
texts = {
|
||||||
|
"Hello traveler!",
|
||||||
|
"This is an example dialog system.",
|
||||||
|
"You can have multiple text boxes.",
|
||||||
|
"Press ENTER to continue!"
|
||||||
|
},
|
||||||
|
textSpeed = 40
|
||||||
|
})
|
||||||
|
GameState.push(testDialog)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update game objects (only when not in dialog)
|
||||||
|
if self.player then
|
||||||
|
self.player:update(dt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function GameState_Class:resume(previous)
|
||||||
|
-- Called when returning from dialog
|
||||||
|
print("Resumed game state from: " .. tostring(previous))
|
||||||
|
-- You can handle post-dialog logic here
|
||||||
|
-- Example: Give item, update quest state, etc.
|
||||||
|
end
|
||||||
|
|
||||||
|
function GameState_Class:draw()
|
||||||
|
RL.ClearBackground({50, 50, 50, 255})
|
||||||
|
|
||||||
|
-- Draw game objects
|
||||||
|
if self.player then
|
||||||
|
self.player:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw pause overlay
|
||||||
|
if self.paused then
|
||||||
|
local screenSize = RL.GetScreenSize()
|
||||||
|
local centerX = screenSize[1] / 2
|
||||||
|
local centerY = screenSize[2] / 2
|
||||||
|
|
||||||
|
RL.DrawRectangle({0, 0, screenSize[1], screenSize[2]}, {0, 0, 0, 128})
|
||||||
|
|
||||||
|
local text = "PAUSED"
|
||||||
|
local size = 40
|
||||||
|
local width = RL.MeasureText(text, size)
|
||||||
|
RL.DrawText(text, {centerX - width / 2, centerY - 20}, size, RL.WHITE)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw controls hint
|
||||||
|
local hint = "WASD/ARROWS: Move | T: Test Dialog | ESC: Pause"
|
||||||
|
local hintSize = 16
|
||||||
|
local screenSize = RL.GetScreenSize()
|
||||||
|
RL.DrawText(hint, {10, screenSize[2] - 30}, hintSize, RL.LIGHTGRAY)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GameState_Class:leave()
|
||||||
|
print("Left game state")
|
||||||
|
end
|
||||||
|
|
||||||
|
return GameState_Class
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Benefits
|
||||||
|
|
||||||
|
✅ **Clean Separation**: Dialog has its own file and logic
|
||||||
|
✅ **Input Control**: Dialog gets exclusive control when active
|
||||||
|
✅ **No Coupling**: Game doesn't need to know about dialog internals
|
||||||
|
✅ **Automatic Pause**: Game automatically stops updating when dialog is pushed
|
||||||
|
✅ **Easy Extension**: Add more dialog types (shops, menus) using same pattern
|
||||||
|
✅ **Post-Dialog Logic**: Use `resume()` callback to handle what happens after dialog closes
|
||||||
|
|
||||||
|
## Advanced: Passing Data Back to Game
|
||||||
|
|
||||||
|
You can pass data when popping:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- In dialog state
|
||||||
|
function DialogState:update(dt)
|
||||||
|
if playerMadeChoice then
|
||||||
|
GameState.pop(choiceData) -- Pass choice back to game
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- In game state
|
||||||
|
function GameState_Class:resume(previous, choiceData)
|
||||||
|
if choiceData then
|
||||||
|
print("Player chose: " .. choiceData.choice)
|
||||||
|
-- Handle the choice
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Stack Visual
|
||||||
|
|
||||||
|
```
|
||||||
|
Initial: [Game State]
|
||||||
|
|
||||||
|
After push: [Game State] -> [Dialog State] (Dialog has control)
|
||||||
|
|
||||||
|
After pop: [Game State] (Game has control back)
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use Push/Pop vs Flags
|
||||||
|
|
||||||
|
**Use Push/Pop for:**
|
||||||
|
- Dialog systems
|
||||||
|
- Pause menus
|
||||||
|
- Shop interfaces
|
||||||
|
- Inventory screens
|
||||||
|
- Any state that needs exclusive control
|
||||||
|
|
||||||
|
**Use Flags (self.paused, etc.) for:**
|
||||||
|
- Simple on/off toggles
|
||||||
|
- Quick state checks
|
||||||
|
- Non-blocking overlays
|
||||||
|
- Debug info displays
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- `lib/gamestate.lua` - Full GameState implementation
|
||||||
|
- `states/game.lua` - Example game state
|
||||||
|
- `states/menu.lua` - Example menu state
|
||||||
Reference in New Issue
Block a user