Files
reilua-enhanced/template/DIALOG_STATE_PATTERN.md
Indrajith K L 7f014770c0 Add dialog state pattern documentation for game templates
Added comprehensive documentation for implementing Zelda-style
dialog systems using the GameState push/pop stack pattern.

Includes:
- Complete working dialog state example with text animation
- Integration guide for game states
- Character portraits and name box support
- Post-dialog callback handling via resume()
- Visual diagrams of state stack behavior
- Best practices for when to use push/pop vs flags

This pattern allows developers to create professional dialog systems
with proper input control and clean state management.
2025-11-05 02:22:38 +05:30

317 lines
8.4 KiB
Markdown

# 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