Add Lua 5.4 scripting integration for dynamic game loading
- Integrated Lua 5.4 engine (32-bit mode for embedded ARM) - Created LuaGame wrapper class implementing Game interface - Added C++ bindings exposing renderer, game state, and input to Lua - Implemented SD card loader for automatic .lua game discovery - Updated GameLauncher to support std::function for lambda captures - Made Game class members public for Lua bindings access - Added example Lua games: counter, snake, bouncing ball - Included comprehensive API documentation Games can now be written as .lua text files on SD card and loaded without recompilation. Build size: 747KB UF2, Lua VM uses ~50-80KB RAM.
This commit is contained in:
233
games/lua_examples/API.md
Normal file
233
games/lua_examples/API.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Lua Game API Documentation
|
||||
|
||||
This document describes the API available to Lua game scripts on the Pico game console.
|
||||
|
||||
## Script Structure
|
||||
|
||||
Every Lua game must define three functions:
|
||||
|
||||
```lua
|
||||
function init()
|
||||
-- Initialize game state
|
||||
end
|
||||
|
||||
function update(event)
|
||||
-- Process input and update game logic
|
||||
-- Return true if redraw needed, false otherwise
|
||||
return needs_redraw
|
||||
end
|
||||
|
||||
function draw()
|
||||
-- Render the game to screen
|
||||
end
|
||||
```
|
||||
|
||||
## Metadata Comments
|
||||
|
||||
Add metadata at the top of your script:
|
||||
|
||||
```lua
|
||||
-- NAME: My Awesome Game
|
||||
-- DESC: A fun game about squares
|
||||
```
|
||||
|
||||
## Global Tables
|
||||
|
||||
### `game` - Game State and Info
|
||||
|
||||
- `game.width()` - Get display width in pixels
|
||||
- `game.height()` - Get display height in pixels
|
||||
- `game.exit()` - Request exit to game launcher
|
||||
- `game.vars` - Table for persistent state variables
|
||||
|
||||
Example:
|
||||
```lua
|
||||
function init()
|
||||
game.vars.score = 0
|
||||
game.vars.player_x = 100
|
||||
game.vars.state = 0
|
||||
end
|
||||
```
|
||||
|
||||
### `renderer` - Drawing Functions
|
||||
|
||||
All coordinates are in pixels. Color is boolean: `true` = black/on, `false` = white/off.
|
||||
|
||||
#### Basic Shapes
|
||||
|
||||
- `renderer.clear(white)` - Clear screen to white (true) or black (false)
|
||||
- `renderer.pixel(x, y, on)` - Set single pixel
|
||||
- `renderer.line(x0, y0, x1, y1, on, width)` - Draw line (width optional, default 1)
|
||||
- `renderer.rect(x, y, w, h, on, filled)` - Draw rectangle (filled optional, default false)
|
||||
- `renderer.circle(x, y, radius, on, filled)` - Draw circle (filled optional, default false)
|
||||
- `renderer.triangle(x0, y0, x1, y1, x2, y2, on, filled)` - Draw triangle
|
||||
|
||||
#### Text
|
||||
|
||||
- `renderer.text(x, y, text, on)` - Draw text string
|
||||
|
||||
Example:
|
||||
```lua
|
||||
function draw()
|
||||
renderer.clear(false) -- Clear to black
|
||||
renderer.rect(50, 50, 100, 80, true, true) -- Filled white rectangle
|
||||
renderer.circle(100, 100, 30, true, false) -- White circle outline
|
||||
renderer.text(10, 10, "Score: " .. tostring(game.vars.score), true)
|
||||
end
|
||||
```
|
||||
|
||||
### `INPUT` - Input Event Types
|
||||
|
||||
Constants for checking `event.type`:
|
||||
|
||||
- `INPUT.NONE` - No input
|
||||
- `INPUT.TOUCH_DOWN` - Touch/click started
|
||||
- `INPUT.TOUCH_MOVE` - Touch/drag in progress
|
||||
- `INPUT.TOUCH_UP` - Touch/click released
|
||||
- `INPUT.BUTTON_0` - Physical button 0 pressed
|
||||
- `INPUT.BUTTON_1` - Physical button 1 pressed
|
||||
- `INPUT.GESTURE` - Gesture detected
|
||||
|
||||
## Input Event Structure
|
||||
|
||||
The `event` parameter passed to `update()` has these fields:
|
||||
|
||||
- `event.type` - Event type (see INPUT constants)
|
||||
- `event.x` - X coordinate (for touch events)
|
||||
- `event.y` - Y coordinate (for touch events)
|
||||
- `event.button_id` - Button identifier (for button events)
|
||||
- `event.valid` - Boolean, true if event is valid
|
||||
|
||||
Example:
|
||||
```lua
|
||||
function update(event)
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
print("Touch at: " .. event.x .. ", " .. event.y)
|
||||
game.vars.last_touch_x = event.x
|
||||
game.vars.last_touch_y = event.y
|
||||
return true -- Request redraw
|
||||
end
|
||||
return false
|
||||
end
|
||||
```
|
||||
|
||||
## State Machine Pattern
|
||||
|
||||
Use `game.vars` to implement state machines:
|
||||
|
||||
```lua
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
end
|
||||
|
||||
function update(event)
|
||||
if game.vars.state == STATE_MENU then
|
||||
-- Handle menu input
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
elseif game.vars.state == STATE_PLAYING then
|
||||
-- Handle gameplay input
|
||||
elseif game.vars.state == STATE_GAME_OVER then
|
||||
-- Handle game over screen
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
if game.vars.state == STATE_MENU then
|
||||
-- Draw menu
|
||||
elseif game.vars.state == STATE_PLAYING then
|
||||
-- Draw game
|
||||
elseif game.vars.state == STATE_GAME_OVER then
|
||||
-- Draw game over screen
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Standard Lua Libraries
|
||||
|
||||
Available libraries:
|
||||
- `math` - Math functions (`math.sin`, `math.random`, etc.)
|
||||
- `string` - String manipulation
|
||||
- `table` - Table operations (`table.insert`, `table.remove`, etc.)
|
||||
- `coroutine` - Coroutines for advanced flow control
|
||||
|
||||
**Not available** (embedded environment):
|
||||
- `io` - File I/O (use SD card APIs in C++ if needed)
|
||||
- `os` - Operating system functions
|
||||
- `debug` - Debugging functions
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Keep it Simple** - Lua is slower than C++. Keep game logic simple for smooth performance.
|
||||
|
||||
2. **Use Local Variables** - Local variables are faster than globals:
|
||||
```lua
|
||||
local function move_player() -- Local function
|
||||
local speed = 5 -- Local variable
|
||||
game.vars.player_x = game.vars.player_x + speed
|
||||
end
|
||||
```
|
||||
|
||||
3. **Minimize Allocations** - Reuse tables instead of creating new ones:
|
||||
```lua
|
||||
-- Good: Reuse existing table
|
||||
game.vars.snake[1].x = new_x
|
||||
|
||||
-- Bad: Creates garbage
|
||||
game.vars.snake[1] = {x = new_x, y = new_y}
|
||||
```
|
||||
|
||||
4. **Efficient Drawing** - Only redraw when needed. Return `false` from `update()` if nothing changed.
|
||||
|
||||
5. **Frame Limiting** - For animation, use frame counters:
|
||||
```lua
|
||||
function update(event)
|
||||
game.vars.frame = (game.vars.frame or 0) + 1
|
||||
if game.vars.frame % 5 == 0 then -- Every 5 frames
|
||||
-- Update animation
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
```
|
||||
|
||||
## Example: Complete Minimal Game
|
||||
|
||||
```lua
|
||||
-- NAME: Click Counter
|
||||
-- DESC: Count your clicks
|
||||
|
||||
function init()
|
||||
game.vars.count = 0
|
||||
end
|
||||
|
||||
function update(event)
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
game.vars.count = game.vars.count + 1
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false)
|
||||
local text = "Clicks: " .. tostring(game.vars.count)
|
||||
renderer.text(20, 20, text, true)
|
||||
end
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
1. Save your `.lua` file to SD card in `/games/` directory
|
||||
2. Eject SD card and insert into Pico console
|
||||
3. Power on - your game will appear in the launcher menu
|
||||
4. Select and play!
|
||||
|
||||
No recompilation needed - edit scripts directly on SD card for rapid iteration.
|
||||
81
games/lua_examples/ball.lua
Normal file
81
games/lua_examples/ball.lua
Normal file
@@ -0,0 +1,81 @@
|
||||
-- NAME: Bouncing Ball
|
||||
-- DESC: Physics demo with state management
|
||||
|
||||
-- States
|
||||
local STATE_PAUSED = 0
|
||||
local STATE_RUNNING = 1
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_PAUSED
|
||||
game.vars.ball_x = game.width() / 2
|
||||
game.vars.ball_y = game.height() / 2
|
||||
game.vars.vel_x = 3
|
||||
game.vars.vel_y = 2
|
||||
game.vars.radius = 10
|
||||
game.vars.frame_count = 0
|
||||
|
||||
print("Bouncing Ball initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
-- Toggle pause on tap
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
if game.vars.state == STATE_PAUSED then
|
||||
game.vars.state = STATE_RUNNING
|
||||
else
|
||||
game.vars.state = STATE_PAUSED
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Update physics if running
|
||||
if game.vars.state == STATE_RUNNING then
|
||||
-- Move ball
|
||||
game.vars.ball_x = game.vars.ball_x + game.vars.vel_x
|
||||
game.vars.ball_y = game.vars.ball_y + game.vars.vel_y
|
||||
|
||||
-- Bounce off walls
|
||||
if game.vars.ball_x - game.vars.radius < 0 or game.vars.ball_x + game.vars.radius > game.width() then
|
||||
game.vars.vel_x = -game.vars.vel_x
|
||||
game.vars.ball_x = math.max(game.vars.radius, math.min(game.width() - game.vars.radius, game.vars.ball_x))
|
||||
end
|
||||
|
||||
if game.vars.ball_y - game.vars.radius < 0 or game.vars.ball_y + game.vars.radius > game.height() then
|
||||
game.vars.vel_y = -game.vars.vel_y
|
||||
game.vars.ball_y = math.max(game.vars.radius, math.min(game.height() - game.vars.radius, game.vars.ball_y))
|
||||
end
|
||||
|
||||
game.vars.frame_count = game.vars.frame_count + 1
|
||||
return true -- Always redraw when running
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false)
|
||||
|
||||
-- Draw ball
|
||||
renderer.circle(game.vars.ball_x, game.vars.ball_y, game.vars.radius, true, true)
|
||||
|
||||
-- Draw trail (previous positions)
|
||||
local trail_radius = game.vars.radius - 2
|
||||
if trail_radius > 2 then
|
||||
renderer.circle(game.vars.ball_x - game.vars.vel_x,
|
||||
game.vars.ball_y - game.vars.vel_y,
|
||||
trail_radius, true, false)
|
||||
end
|
||||
|
||||
-- Draw status
|
||||
if game.vars.state == STATE_PAUSED then
|
||||
renderer.text(10, 10, "PAUSED - Tap to start", true)
|
||||
else
|
||||
renderer.text(10, 10, "Frames: " .. tostring(game.vars.frame_count), true)
|
||||
renderer.text(10, 25, "Tap to pause", true)
|
||||
end
|
||||
|
||||
-- Draw velocity vector
|
||||
local arrow_x = game.vars.ball_x + game.vars.vel_x * 3
|
||||
local arrow_y = game.vars.ball_y + game.vars.vel_y * 3
|
||||
renderer.line(game.vars.ball_x, game.vars.ball_y, arrow_x, arrow_y, true, 2)
|
||||
end
|
||||
49
games/lua_examples/counter.lua
Normal file
49
games/lua_examples/counter.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
-- NAME: Touch Counter
|
||||
-- DESC: Simple tap counter demo
|
||||
|
||||
-- Initialize game state
|
||||
function init()
|
||||
game.vars.count = 0
|
||||
game.vars.last_x = 0
|
||||
game.vars.last_y = 0
|
||||
print("Counter initialized")
|
||||
end
|
||||
|
||||
-- Update game logic based on input
|
||||
function update(event)
|
||||
-- Check if touch/button pressed
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.count = game.vars.count + 1
|
||||
game.vars.last_x = event.x
|
||||
game.vars.last_y = event.y
|
||||
print("Count: " .. game.vars.count)
|
||||
return true -- Request redraw
|
||||
end
|
||||
|
||||
return false -- No redraw needed
|
||||
end
|
||||
|
||||
-- Draw the game
|
||||
function draw()
|
||||
-- Clear screen
|
||||
renderer.clear(true)
|
||||
|
||||
-- Draw title
|
||||
renderer.text(20, 20, "Touch Counter", true)
|
||||
|
||||
-- Draw count (centered)
|
||||
local count_text = "Count: " .. tostring(game.vars.count)
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 10, count_text, true)
|
||||
|
||||
-- Draw last touch position
|
||||
if game.vars.count > 0 then
|
||||
local pos_text = "Last: (" .. tostring(game.vars.last_x) .. ", " .. tostring(game.vars.last_y) .. ")"
|
||||
renderer.text(20, game.height() - 30, pos_text, true)
|
||||
|
||||
-- Draw marker at last touch
|
||||
renderer.circle(game.vars.last_x, game.vars.last_y, 5, true, false)
|
||||
end
|
||||
|
||||
-- Draw instructions
|
||||
renderer.text(20, 50, "Tap screen to increment", true)
|
||||
end
|
||||
216
games/lua_examples/snake.lua
Normal file
216
games/lua_examples/snake.lua
Normal file
@@ -0,0 +1,216 @@
|
||||
-- NAME: Snake Game
|
||||
-- DESC: Classic snake with state machine
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
-- Game constants
|
||||
local CELL_SIZE = 10
|
||||
local GRID_W = 40
|
||||
local GRID_H = 28
|
||||
|
||||
-- Initialize game
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
game.vars.high_score = 0
|
||||
|
||||
-- Snake as array of {x, y} positions
|
||||
game.vars.snake = {}
|
||||
game.vars.snake[1] = {x = 20, y = 14}
|
||||
game.vars.snake[2] = {x = 19, y = 14}
|
||||
game.vars.snake[3] = {x = 18, y = 14}
|
||||
|
||||
game.vars.dir_x = 1
|
||||
game.vars.dir_y = 0
|
||||
|
||||
game.vars.food_x = 30
|
||||
game.vars.food_y = 14
|
||||
|
||||
game.vars.frame_count = 0
|
||||
game.vars.move_speed = 10 -- Frames between moves
|
||||
|
||||
print("Snake Game initialized")
|
||||
end
|
||||
|
||||
-- Update game logic
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
-- State: MENU
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_PLAYING
|
||||
game.vars.score = 0
|
||||
init_snake()
|
||||
spawn_food()
|
||||
return true
|
||||
end
|
||||
|
||||
-- State: PLAYING
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle input for direction change
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
local head = game.vars.snake[1]
|
||||
local head_screen_x = head.x * CELL_SIZE
|
||||
local head_screen_y = head.y * CELL_SIZE
|
||||
|
||||
local dx = event.x - head_screen_x
|
||||
local dy = event.y - head_screen_y
|
||||
|
||||
-- Change direction based on touch relative to head
|
||||
if math.abs(dx) > math.abs(dy) then
|
||||
if dx > 0 and game.vars.dir_x ~= -1 then
|
||||
game.vars.dir_x = 1
|
||||
game.vars.dir_y = 0
|
||||
elseif dx < 0 and game.vars.dir_x ~= 1 then
|
||||
game.vars.dir_x = -1
|
||||
game.vars.dir_y = 0
|
||||
end
|
||||
else
|
||||
if dy > 0 and game.vars.dir_y ~= -1 then
|
||||
game.vars.dir_x = 0
|
||||
game.vars.dir_y = 1
|
||||
elseif dy < 0 and game.vars.dir_y ~= 1 then
|
||||
game.vars.dir_x = 0
|
||||
game.vars.dir_y = -1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Move snake every N frames
|
||||
game.vars.frame_count = game.vars.frame_count + 1
|
||||
if game.vars.frame_count >= game.vars.move_speed then
|
||||
game.vars.frame_count = 0
|
||||
move_snake()
|
||||
return true
|
||||
end
|
||||
|
||||
-- State: GAME_OVER
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Draw game
|
||||
function draw()
|
||||
renderer.clear(false)
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
-- Draw: MENU
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "SNAKE", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
|
||||
|
||||
if game.vars.high_score > 0 then
|
||||
local hs_text = "High: " .. tostring(game.vars.high_score)
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2 + 20, hs_text, true)
|
||||
end
|
||||
|
||||
-- Draw: PLAYING
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Draw snake
|
||||
for i = 1, #game.vars.snake do
|
||||
local seg = game.vars.snake[i]
|
||||
local filled = (i == 1) -- Head filled, body outline
|
||||
renderer.rect(seg.x * CELL_SIZE, seg.y * CELL_SIZE, CELL_SIZE, CELL_SIZE, true, filled)
|
||||
end
|
||||
|
||||
-- Draw food
|
||||
renderer.circle(game.vars.food_x * CELL_SIZE + CELL_SIZE / 2,
|
||||
game.vars.food_y * CELL_SIZE + CELL_SIZE / 2,
|
||||
CELL_SIZE / 2, true, true)
|
||||
|
||||
-- Draw score
|
||||
local score_text = "Score: " .. tostring(game.vars.score)
|
||||
renderer.text(5, 5, score_text, true)
|
||||
|
||||
-- Draw: GAME_OVER
|
||||
elseif state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
|
||||
|
||||
local score_text = "Score: " .. tostring(game.vars.score)
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2, score_text, true)
|
||||
|
||||
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true)
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: Initialize snake
|
||||
function init_snake()
|
||||
game.vars.snake = {}
|
||||
game.vars.snake[1] = {x = 20, y = 14}
|
||||
game.vars.snake[2] = {x = 19, y = 14}
|
||||
game.vars.snake[3] = {x = 18, y = 14}
|
||||
game.vars.dir_x = 1
|
||||
game.vars.dir_y = 0
|
||||
end
|
||||
|
||||
-- Helper: Spawn food at random position
|
||||
function spawn_food()
|
||||
game.vars.food_x = math.random(0, GRID_W - 1)
|
||||
game.vars.food_y = math.random(0, GRID_H - 1)
|
||||
end
|
||||
|
||||
-- Helper: Move snake
|
||||
function move_snake()
|
||||
local head = game.vars.snake[1]
|
||||
local new_head = {
|
||||
x = head.x + game.vars.dir_x,
|
||||
y = head.y + game.vars.dir_y
|
||||
}
|
||||
|
||||
-- Check wall collision
|
||||
if new_head.x < 0 or new_head.x >= GRID_W or new_head.y < 0 or new_head.y >= GRID_H then
|
||||
game_over()
|
||||
return
|
||||
end
|
||||
|
||||
-- Check self collision
|
||||
for i = 1, #game.vars.snake do
|
||||
local seg = game.vars.snake[i]
|
||||
if new_head.x == seg.x and new_head.y == seg.y then
|
||||
game_over()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Check food collision
|
||||
local ate_food = false
|
||||
if new_head.x == game.vars.food_x and new_head.y == game.vars.food_y then
|
||||
ate_food = true
|
||||
game.vars.score = game.vars.score + 10
|
||||
spawn_food()
|
||||
|
||||
-- Increase speed slightly
|
||||
if game.vars.move_speed > 3 then
|
||||
game.vars.move_speed = game.vars.move_speed - 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Move snake
|
||||
table.insert(game.vars.snake, 1, new_head)
|
||||
|
||||
if not ate_food then
|
||||
table.remove(game.vars.snake) -- Remove tail
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: Game over
|
||||
function game_over()
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
|
||||
if game.vars.score > game.vars.high_score then
|
||||
game.vars.high_score = game.vars.score
|
||||
end
|
||||
|
||||
print("Game Over! Score: " .. game.vars.score)
|
||||
end
|
||||
Reference in New Issue
Block a user