582 added, 907 removed. Audit A to C.
- ---
- name: bpcore-engine
- description: "BPCore Engine - Lua game framework for Gameboy Advance with sprites, tilemaps, entities, collision, audio, multiplayer"
- metadata:
- author: mte90
- version: "1.0.0"
- tags:
- - lua
- - gba
- - game-engine
- - gameboy-advance
- - retro-gaming
- ---
-
# BPCore Engine Skill
Comprehensive guide for building Gameboy Advance games using the BPCore Engine Lua framework.
## Overview
- BPCore Engine (Blind jumP Core Engine) is a Lua game framework specifically designed for the Gameboy Advance (GBA) handheld console. The framework combines a C++ engine with an embedded Lua interpreter, allowing developers to create GBA games using Lua without needing to write C++ code or use a compiler. The API design takes inspiration from fantasy consoles like Pico-8 and Tic80, making it familiar to developers who have experience with those platforms.
-
- The Gameboy Advance presents unique challenges for game development due to its limited hardware resources. The GBA features a 240x160 pixel screen, a 16.78 MHz ARM7TDMI processor, and only 256KB of RAM available for Lua code and data. Because Lua is resource-intensive, the engine is best suited for creating relatively small minigames rather than complex, resource-intensive applications. If you need to build more ambitious projects, consider learning C++ and using the Butano engine instead.
-
- The engine provides a comprehensive API that covers sprite rendering, tile-based graphics, entity management with collision detection, button input handling, text rendering with UTF-8 support, audio playback, save/load functionality via SRAM, and multiplayer networking through the GBA's link cable. The build system uses a simple Lua script to package resources into a ROM file, making the development workflow accessible and straightforward.
-
- ## Setup and Installation
-
- ### Prerequisites
-
- To start developing with BPCore Engine, you need three essential components. First, you need a copy of the `build.lua` script from the BPCore Engine repository, which handles the ROM building process. Second, you need the `BPCoreEngine.gba` base ROM file that contains the engine's compiled code. Third, you need Lua 5.3 installed on your development machine, as the build script is written in Lua.
-
- The build process works by taking the base ROM file and appending a resource bundle containing all your game assets and scripts. When the GBA boots, the engine locates this resource bundle, loads your main.lua script, and begins executing your game code. This approach allows you to create complete games using only Lua scripts and resource files, without needing to recompile the C++ engine.
-
- ### Creating a Manifest
-
- The manifest.lua file tells the build system which resources to include in your ROM. This file defines your game's metadata, specifies asset files, and identifies the main script entry point. Here is a comprehensive example demonstrating all available manifest options:
+ BPCore Engine (Blind jumP Core Engine) is a Lua game framework for GBA that combines C++ with embedded Lua, letting developers create games without C++ or compilers. Inspired by Pico-8 and Tic80, it's suited for small minigames given the GBA's limited resources: 240x160 screen, 16.78 MHz ARM7TDMI, and 256KB RAM for Lua/data.
- ```lua
- local app = {
- -- Game identification in the ROM header
- name = "MyGame",
- gamecode = "ABCD", -- Four-character game code (optional)
- makercode = "BC", -- Two-character maker code (optional)
-
- -- Graphics resources
- tilesets = {
- "overlay.bmp", -- Text and UI tiles
- "tile0.bmp", -- Main background tiles
- "tile1.bmp", -- Additional tile set
- },
-
- spritesheets = {
- "spritesheet.bmp", -- Sprite graphics
- },
-
- -- Audio resources
- audio = {
- "music.raw", -- Background music
- "jump.wav", -- Sound effect
- "coin.wav", -- Sound effect
- },
-
- -- Lua scripts (main.lua is the entry point)
- scripts = {
- "main.lua",
- "menu.lua",
- "game.lua",
- "utils.lua",
- },
-
- -- Miscellaneous files
- misc = {
- "level1.csv", -- Tilemap data
- "spritedata.txt", -- Configuration
- }
- }
+ The engine provides sprite rendering, tile-based graphics, entity management with collision, button input, UTF-8 text, audio, save/load via SRAM, and multiplayer through the link cable. The build system uses Lua to package resources into ROM.
- return app
- ```
+ ## API Reference
- The manifest structure separates different resource types into their respective categories. Tilesets are 8x8 tile graphics used for background layers, while spritesheets contain 16x16 sprite graphics. Audio files must be in a specific format: mono 16kHz signed 8-bit PCM. Scripts are your Lua game code, and miscellaneous files can include any additional data your game needs.
+ ### Entity Functions
- ### Building Your ROM
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `ent()` | `ent()` | Create entity, returns handle |
+ | `entpos(e, x, y)` | `entpos(e, x, y)` | Set position, returns self |
+ | `entz(e, z)` | `entz(e, z)` | Set Z-order (0-255), returns self |
+ | `entspr(e, sprite, xflip, yflip)` | `entspr(e, sprite, xflip, yflip)` | Set sprite and flips, returns self |
+ | `entspd(e, x, y)` | `entspd(e, x, y)` | Set movement speed, returns self |
+ | `entslot(e, slot, value)` | `entslot(e, slot, value)` | Store in slot, returns self |
+ | `entslots(e, count)` | `entslots(e, count)` | Allocate slots, returns self |
+ | `entanim(e, start, len, rate)` | `entanim(e, start, len, rate)` | Set animation |
+ | `entag(e, tag)` | `entag(e, tag)` | Set collision tag, returns self |
+ | `enthb(e, ox, oy, w, h)` | `enthb(e, ox, oy, w, h)` | Set hitbox, returns self |
+ | `del(e, auto?)` | `del(e, [auto])` | Delete entity |
+ | `ents()` | `ents()` | Get all entities table |
- Once you have created your manifest.lua and placed all your resource files in the appropriate directory, building the ROM is straightforward. Run the build.lua script with Lua 5.3, specifying your manifest file and the base ROM:
+ ### Sprite & Tile Functions
- ```bash
- lua53 build.lua manifest.lua BPCoreEngine.gba output.gba
- ```
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `txtr(layer_id, filename)` | `txtr(layer_id, filename)` | Load texture from file |
+ | `txtr(layer_id, ptr, len)` | `txtr(layer_id, ptr, len)` | Load from pointer/length |
+ | `file(filename)` | `file(filename)` | Get file pointer/length |
+ | `spr(sprite_id, x, y)` | `spr(sprite_id, x, y)` | Draw sprite |
+ | `spr(sprite_id, x, y, xflip, yflip)` | `spr(sprite_id, x, y, xflip, yflip)` | Draw with flips |
+ | `tile(layer_id, x, y, num?)` | `tile(layer_id, x, y, [num])` | Draw/read tile |
+ | `tilemap(f, layer, w, h, dx, dy, sx, sy)` | `tilemap(f, layer, w, h, dx, dy, sx, sy)` | Load CSV tilemap |
+ | `clear()` | `clear()` | Clear sprites, VSync |
+ | `display()` | `display()` | Send sprites to display |
+ | `fade(amount, color?, tl?, bg?)` | `fade(amount, [color], [tl], [bg])` | Fade layers |
- The build script will parse your manifest, verify that all referenced files exist, and create a new ROM file with your game bundled inside. If there are any errors in your manifest or missing files, the build will fail with informative error messages that help you identify and fix the issue.
+ ### Camera & Scroll
- After building, you can test your ROM using an emulator such as mGBA, which provides excellent debugging tools including a logging window for the engine's log() function, memory viewers for inspecting IRAM and SRAM, and a disassembler for troubleshooting.
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `camera(x, y)` | `camera(x, y)` | Set camera center |
+ | `scroll(layer, x, y)` | `scroll(layer, x, y)` | Set layer scroll |
+ | `priority(s, bg, t0, t1)` | `priority(s, bg, t0, t1)` | Set render priority |
- ## Graphics System
+ ### Collision
- ### Understanding Layers and Memory
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `ecolle(e1, e2)` | `ecolle(e1, e2)` | Check collision (bool) |
+ | `ecoll(e, tag)` | `ecoll(e, tag)` | Find tag matches (array) |
+ | `enthb(e)` | `enthb(e)` | Get hitbox (getter) |
- The GBA uses a tile-based display system where all graphics are composed of small tiles. Sprites are always 16x16 pixels, while background tiles are 8x8 pixels. The engine provides four tile-based layers plus a sprite layer, each with different characteristics and use cases.
+ ### Input
- The overlay layer (layer ID 0) is a 32x32 tile layer that displays in front of all other content. This layer is primarily used for text rendering via the print() function, but you can also draw tiles directly on it. The overlay is persistent like all tile layers, meaning tiles remain on screen until you explicitly change them.
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `btn(num)` | `btn(num)` | Button held? (bool) |
+ | `btnp(num)` | `btnp(num)` | Button pressed? (bool) |
+ | `btnnp(num)` | `btnnp(num)` | Button released? (bool) |
- Tile layer 1, also known as tile_1, is a larger 64x64 tile layer that displays behind sprites but in front of tile_0 and the background. This makes it suitable for foreground parallax elements or game elements that should appear behind sprites but above the main background.
+ ### Graphics
- Tile layer 0 (layer ID 2, also called tile_0) is another 64x64 tile layer that displays behind sprites, tile_1, and the overlay, but in front of the background layer. Use this for your main game background where you want layered depth effects.
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `print(text, x, y, fg?, bg?)` | `print(text, x, y, [fg], [bg])` | Print to overlay |
- The background layer (layer ID 3) is a 32x32 tile layer that displays behind everything else. It shares texture memory with tile_0, so you need to balance your graphical resources between these two layers carefully.
+ ### Audio
- Sprites (loaded via layer ID 4) are dynamic objects that the engine renders on top of tile layers. Unlike tiles, sprites must be redrawn every frame using the clear() and display() cycle.
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `music(f, offset?)` | `music(f, [offset])` | Play background music |
+ | `sound(f, priority)` | `sound(f, priority)` | Play sound effect |
- ### Loading Textures
+ ### Memory
- The txtr() function loads image data from resource bundle files into VRAM for use by specific layers. You can load by filename for initial loading, or preload with the file() function for faster texture swapping during gameplay:
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `poke(addr, value)` | `poke(addr, value)` | Write byte |
+ | `poke4(addr, value)` | `poke4(addr, value)` | Write 32-bit word |
+ | `peek(addr)` | `peek(addr)` | Read byte |
+ | `peek4(addr)` | `peek4(addr)` | Read 32-bit word |
+ | `memput(addr, data)` | `memput(addr, data)` | Write to memory |
+ | `memget(addr, len?)` | `memget(addr, [len])` | Read from memory |
- ```lua
- -- Load a tileset into tile layer 1
- txtr(1, "forest_tiles.bmp")
+ ### System
- -- Load the spritesheet
- txtr(4, "player_sprites.bmp")
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `delta()` | `delta()` | Microseconds since last call |
+ | `sleep(frames)` | `sleep(frames)` | Sleep N frames |
+ | `startup_time()` | `startup_time()` | Get boot time |
+ | `fdog()` | `fdog()` | Feed watchdog timer |
+ | `log(msg)` | `log(msg)` | Debug log |
+ | `rline()` | `rline()` | Get raster line |
+ | `flimit(fps)` | `flimit(fps)` | Set frame rate limit |
+ | `next_script(filename)` | `next_script(filename)` | Switch Lua script |
- -- Preload multiple textures for fast swapping
- local forest_ptr, forest_len = file("forest_tiles.bmp")
- local cave_ptr, cave_len = file("cave_tiles.bmp")
+ ### Multiplayer
- -- Later in your game, switch textures instantly
- txtr(1, forest_ptr, forest_len) -- Switch to forest
- txtr(1, cave_ptr, cave_len) -- Switch to cave
- ```
+ | Function | Signature | Description |
+ |----------|-----------|-------------|
+ | `connect(timeout)` | `connect(timeout)` | Connect (blocking) |
+ | `disconnect()` | `disconnect()` | Disconnect |
+ | `send(data)` | `send(data)` | Send message (max 11 bytes) |
+ | `recv()` | `recv()` | Receive message |
+ | `send_iram(ptr)` | `send_iram(ptr)` | Send from IRAM |
+ | `recv_iram(ptr)` | `recv_iram(ptr)` | Receive to IRAM |
- The texture loading system supports BMP files with specific constraints. The engine expects indexed color images where the palette defines available colors. For tilesets, the image dimensions should match the layer size (32x32 or 64x64 tiles, each tile being 8x8 pixels). Spritesheets can contain multiple 16x16 sprite frames arranged in a grid.
+ ### Layer IDs
- ### Drawing Sprites
+ | ID | Layer | Size | Description |
+ |----|-------|------|-------------|
+ | 0 | Overlay | 32x32 tiles | Front, persistent |
+ | 1 | Tile Layer 1 | 64x64 tiles | Behind sprites, in front of tile_0 |
+ | 2 | Tile Layer 0 | 64x64 tiles | Main background |
+ | 3 | Background | 32x32 tiles | Back layer |
+ | 4 | Sprites | Dynamic | On-top sprites |
- The spr() function draws sprites from the loaded spritesheet at specified screen coordinates. Sprites are not persistent—you must redraw them every frame within your game loop:
+ ## Installation & Project Structure
- ```lua
- function draw()
- -- Draw sprite index 0 at position (100, 80)
- spr(0, 100, 80)
-
- -- Draw sprite with horizontal flip
- spr(5, 50, 60, true, false)
-
- -- Draw sprite with vertical flip
- spr(5, 150, 60, false, true)
-
- -- Draw sprite with both flips (rotated 180 degrees)
- spr(5, 200, 80, true, true)
- end
- ```
+ ### Getting BPCoreEngine.gba
- Sprite indices correspond to positions in your spritesheet. If your spritesheet is organized as a grid, calculate indices by counting from left to right, top to bottom. The GBA hardware supports a maximum of 128 sprites on screen simultaneously, so be mindful of this limit in busy scenes.
+ **Important**: BPCore Engine requires `BPCoreEngine.gba` base ROM with compiled C++ code and Lua interpreter. This is **not** a custom ROM you create.
- Important: The engine draws sprites with increasing depth, meaning later spr() calls appear behind earlier ones. This may seem counterintuitive, but it ensures that when you exceed the 128 sprite limit, background sprites are hidden rather than foreground sprites.
+ #### Where to Obtain
- ### Drawing Tiles
+ 1. **Official Repository**: Clone from [GitHub](https://github.com/evanbowman/BPCore-Engine)
+ ```bash
+ git clone https://github.com/evanbowman/BPCore-Engine.git
+ cd BPCore-Engine
+ ```
- The tile() function draws individual tiles on tile layers. Unlike sprites, tiles are persistent—they remain on screen without needing to be redrawn. This makes tiles efficient for backgrounds but requires careful management when you want to change them:
+ 2. **Required Files**: Repository contains:
+ - `BPCoreEngine.gba` - Engine base ROM (~3.5 MB)
+ - `build.lua` - Build script (Lua 5.3)
+ - `test/` - Example projects
- ```lua
- -- Draw tile index 1 at position (10, 10) on layer 1
- tile(1, 10, 10, 1)
+ 3. **Alternatives**:
+ - GitHub Releases may have pre-built GBA files
+ - mGBA emulator includes BPCore version in some distributions
- -- Draw the same tile across an area
- for x = 0, 63 do
- for y = 0, 63 do
- tile(2, x, y, 1) -- Fill layer 0 with tile 1
- end
- end
+ #### File Placement
- -- Read the current tile at a position (getter mode)
- local current = tile(2, 5, 5) -- Returns tile index at that position
```
-
- The tile() function operates differently depending on whether you provide a tile_num argument. With a tile number, it sets the tile at that position. Without it, the function returns the current tile index at the specified position, useful for collision detection or terrain checking.
-
- ### Loading Tilemaps
-
- For large background areas, loading tiles one by one is impractical. The tilemap() function allows you to load pre-made tilemaps from CSV files, which you can export from map editors like Tiled:
-
- ```lua
- -- Load a full tilemap from a CSV file
- -- tilemap(filename, layer, width, height, dest_x, dest_y, src_x, src_y)
- tilemap("level1.csv", 2, 64, 64, 0, 0, 0, 0)
-
- -- Load only a portion of a larger tilemap
- -- Load a 20x15 section starting from column 10, row 5
- tilemap("world.csv", 1, 20, 15, 0, 0, 10, 5)
+ your-project/
+ ├── BPCoreEngine.gba # Base ROM (required)
+ ├── build.lua # Build script
+ ├── manifest.lua # Your manifest
+ ├── overlay.bmp # Text/UI tiles
+ ├── tile0.bmp # Main background (64x64 tiles)
+ ├── tile1.bmp # Foreground (64x64 tiles)
+ ├── spritesheet.bmp # Sprites (16x16)
+ ├── music.raw # Music (mono 16kHz PCM)
+ ├── sfx.wav # Sound effects
+ ├── main.lua # Entry point
+ └── data.txt # Resources
```
- The CSV format uses comma-separated integer values representing tile indices. Each row in the CSV corresponds to a row of tiles in the layer. The function will fail if the file doesn't exist or if your parameters would cause an out-of-bounds access.
-
- ### Camera and Scrolling
-
- The camera() function sets the center point of the view, affecting all subsequent sprite rendering. The scroll() function provides additional control for individual layers:
-
- ```lua
- -- Set camera center to (x, y)
- camera(120, 80) -- Center on middle of screen
-
- -- Additional scrolling for tile layers (relative to camera)
- scroll(2, 0, 0) -- No extra scroll on tile_0
- scroll(1, 10, 5) -- Offset tile_1 by 10 pixels horizontal, 5 vertical
+ ### Build Layout
- -- Overlay scroll is absolute (not affected by camera)
- scroll(0, 16, 0) -- Scroll overlay 16 pixels right
```
-
- Camera scrolling automatically affects sprites and the two map layers (tile_0 and tile_1). The overlay and background layers do not scroll with the camera, giving you flexibility in creating layered parallax effects. The scroll amounts for tile layers are relative to the camera position, while the overlay scroll is absolute.
-
- ### Layer Priority
-
- The priority() function lets you reorder the rendering layers to control which elements appear in front of others:
-
- ```lua
- -- priority(sprite, background, tile_0, tile_1)
- -- Values 0-3: 0 = nearest to screen, 3 = furthest
-
- -- Default priority
- priority(1, 3, 3, 2)
-
- -- Put background behind everything
- priority(1, 3, 2, 1)
-
- -- Bring sprites to front, tile_1 to back
- priority(0, 3, 2, 3)
+ ./
+ ├── src/
+ │ ├── main.lua # Main loop
+ │ ├── game.lua # Game logic
+ │ ├── menu.lua # Menu
+ │ └── level1.csv # Tilemap
+ ├── assets/
+ │ ├── graphics/
+ │ │ ├── spritesheet.bmp
+ │ │ ├── tiles0.bmp
+ │ │ └── tiles1.bmp
+ │ ├── audio/
+ │ │ ├── music.raw
+ │ │ ├── jump.wav
+ │ │ └── coin.wav
+ │ └── tilemaps/
+ │ └── world.csv
+ ├── build.lua # Build script
+ ├── BPCoreEngine.gba # Engine ROM
+ └── dist/
+ └── yourgame.gba # Output
```
- The overlay always has priority 0 and cannot be changed. When layers share the same priority value, the engine uses a fixed precedence order: sprites > tile_0 > background > overlay > tile_1. Understanding this order helps you achieve the desired visual layering.
-
- ### Screen Effects
+ ### Building Your ROM
- The fade() function creates fade effects for transitions between scenes or states:
+ **Step 1**: Create `manifest.lua`
```lua
- -- Simple fade to black
- fade(1.0) -- Fully faded (black)
- fade(0.0) -- No fade (fully visible)
- fade(0.5) -- Half faded
-
- -- Fade to custom color (hex colors)
- fade(0.8, 0xFFFF) -- Fade to white
+ local app = {
+ name = "My Adventure",
+ gamecode = "ABCD",
+ makercode = "BC",
+ tilesets = { "overlay.bmp", "tile0.bmp", "tile1.bmp" },
+ spritesheets = { "spritesheet.bmp" },
+ audio = { "music.raw", "jump.wav", "coin.wav" },
+ scripts = { "main.lua", "game.lua", "menu.lua" },
+ misc = { "level1.csv" }
+ }
- -- Control which layers are affected
- fade(0.5, nil, false, true) -- Only fade the overlay
+ return app
```
- The fade amount ranges from 0.0 (fully visible) to 1.0 (fully faded). By default, fade affects all layers except text with custom colors. The custom color option allows creating transitions to specific colors rather than black.
-
- ### Display Cycle
-
- Every frame follows a specific sequence: update game logic, clear sprites, draw new sprites, then display. The clear() function clears all sprites and performs a VSync to synchronize with the display refresh:
+ **Step 2**: Run build script
- ```lua
- function main_loop()
- while true do
- -- Update game state (button input, physics, etc.)
- update(delta())
-
- -- Clear previous frame's sprites
- clear()
-
- -- Draw all sprites for this frame
- draw()
-
- -- Send sprites to display
- display()
- end
- end
+ ```bash
+ lua build.lua manifest.lua BPCoreEngine.gba output.gba
```
- The clear() function erases all sprites from the screen but does not affect tiles. Tiles persist across frames, so you only need to set up your background tiles once during loading. The display() function sends all pending sprite draw calls to the GBA hardware, making them visible.
-
- ### Frame Rate Control
+ **Step 3**: Verify
- The flimit() function restricts the frame rate to help manage CPU usage:
+ Build checks manifest files exist, formats are valid, scripts readable, output ROM created (~3.5-4 MB).
- ```lua
- -- Limit to 30 FPS for slower games
- flimit(30)
+ **Step 4**: Test with mGBA
- -- Full 60 FPS (default)
- flimit(60)
+ ```bash
+ mGBA output.gba --log --memview
```
- Lower frame rates can help if your game logic is complex and needs more time per frame. The GBA has limited CPU resources, so finding the right balance between frame rate and game complexity is important.
+ `--log` shows engine log() output, `--memview` opens memory viewer for debugging.
- ### Monitoring Performance
+ ### File Formats
- The rline() function returns the current raster line number, which helps diagnose performance issues:
+ **BMP Tilesets**: Indexed color (256 colors), tiles are 8x8 pixels. Overlay: 16x16 tiles, Tile_0/1: 32x32 tiles, Sprites: 8x8 sprites.
- ```lua
- -- Check if you're taking too long per frame
- local line = rline()
- if line > 160 then
- -- Game is lagging: taking too long to update
- print("LAG!", 1, 1)
- end
+ **Audio**: Mono 16kHz signed 8-bit PCM. Convert with FFmpeg:
+ ```bash
+ ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s8 output.raw
```
- The GBA screen has 160 visible raster lines. If your game logic causes the raster to advance past line 160, you have exceeded your frame budget and the game will exhibit visual lag or tearing.
-
- ## Entity System
-
- ### Entity Overview
-
- Entities are enhanced sprite objects with automatic rendering, hitbox support, and collision detection. Unlike regular sprites that you must redraw every frame, entities are automatically managed by the engine—they are redrawn each frame without explicit calls, and the engine handles sorting them by Z-order for proper depth rendering.
-
- The entity system supports a maximum of 128 entities simultaneously. All entity functions that modify properties return the entity as a result, enabling method chaining. When called without modification arguments, these functions act as getters, returning the current property values.
-
- ### Creating and Managing Entities
-
- ```lua
- -- Create a new entity
- local player = ent()
+ **Tilemap CSV**: Comma-separated tile indices, each row is one tile row. Tile indices start at 1 (0 = empty).
- -- Set entity properties with chaining
- entspr(entpos(entz(player, 10), 100, 80), 0)
- entspd(player, 0, 0) -- No automatic movement
+ ## Save/Load to SRAM
- -- Or use the chained form directly
- local enemy = entpos(entspr(ent(), 5), 120, 100)
+ ### SRAM Overview
- -- Delete an entity when done
- del(player)
- del(enemy)
+ GBA provides **32KB SRAM** that persists across restarts, unlike volatile IRAM (8KB).
- -- Delete entity when animation finishes
- del(some_effect_entity, 1) -- Parameter 1 means delete on animation end
```
-
- The del() function is essential for resource management. The Lua garbage collector does not automatically clean up entities—the engine owns them. Always delete entities when they are no longer needed to free their slot for new entities.
-
- ### Entity Properties
-
- Each entity has several properties that control its appearance and behavior:
-
- ```lua
- local e = ent()
-
- -- Set sprite (sprite_id, xflip, yflip)
- entspr(e, 10, false, false)
-
- -- Get sprite info
- local sprite_id, xflip, yflip = entspr(e)
-
- -- Set position
- entpos(e, 50, 60)
-
- -- Get position
- local x, y = entpos(e)
-
- -- Set Z-order (0-255, higher renders in front)
- entz(e, 100)
-
- -- Get Z-order
- local z = entz(e)
-
- -- Tag the entity for collision filtering
- entag(e, 100) -- Tag as enemy type
-
- -- Get tag
- local tag = entag(e)
+ SRAM: 32KB
+ ├── Offset 0x00: Save data
+ ├── Offset 0xFF: Save marker
+ └── Offset 0x00-0xFF: Checksum
```
- Entity properties persist across frames without needing to be set again. The engine automatically handles repositioning and rendering based on these stored values.
-
- ### Hitboxes and Collision
-
- Entities include configurable hitboxes for collision detection. By default, entities have a 16x16 hitbox centered on their position, matching the sprite size:
+ ### Basic Save/Load
```lua
- -- Set custom hitbox
- -- enthb(entity, x_origin, y_origin, width, height)
- enthb(e, 4, 4, 8, 8) -- Smaller hitbox centered in sprite
-
- -- Check collision between two entities
- if ecole(entity1, entity2) then
- -- Collision detected!
+ function save_game()
+ local offset = 0
+
+ poke4(_SRAM + offset, player_x); offset = offset + 4
+ poke4(_SRAM + offset, player_y); offset = offset + 4
+ poke4(_SRAM + offset, player_health); offset = offset + 4
+ poke4(_SRAM + offset, player_score); offset = offset + 4
+ poke(_SRAM + offset, current_level); offset = offset + 1
+ poke(_SRAM + 31999, 0x42) -- Save marker
+
+ print("Saved!", 1, 1, 0xFFFF)
end
- -- Check collision with entities of a specific tag
- -- Returns array of up to 16 colliding entities
- local collisions = ecoll(entity1, 100) -- Find all entities tagged 100
- for i = 1, #collisions do
- local other = collisions[i]
- -- Handle collision with each entity
+ function load_game()
+ if peek(_SRAM + 31999) ~= 0x42 then
+ return false
+ end
+
+ player_x = peek4(_SRAM); offset = offset + 4
+ player_y = peek4(_SRAM); offset = offset + 4
+ player_health = peek4(_SRAM); offset = offset + 4
+ player_score = peek4(_SRAM); offset = offset + 4
+ current_level = peek(_SRAM)
+
+ print("Loaded!", 1, 1, 0xFFFF)
+ return true
end
```
- Hitboxes are anchored relative to the entity center. The x_origin and y_origin values are subtracted from the entity's center to position the hitbox. This allows creating hitboxes that don't exactly match the visual sprite—for example, making a character with a sword have a larger attack hitbox than their body hitbox.
-
- ### Entity Movement and Slots
-
- The entspd() function sets automatic movement that the engine applies each frame:
-
- ```lua
- -- Set entity speed (pixels per frame)
- entspd(e, 2, 1) -- Move 2 pixels right, 1 pixel down per frame
-
- -- Entity slots store arbitrary data
- entslots(e, 5) -- Allocate 5 slots for this entity
-
- -- Store and retrieve values
- entslot(e, 1, 100) -- Store 100 in slot 1
- entslot(e, 2, "data") -- Store string in slot 2
-
- local value1 = entslot(e, 1) -- Retrieve slot 1 value
- local value2 = entslot(e, 2) -- Retrieve slot 2 value
- ```
-
- Entity slots use 1-based indexing, matching Lua table conventions. The slot system provides a way to store game-specific data associated with each entity, such as health points, animation frames, or custom flags. Accessing invalid slots (0 or beyond allocated count) raises a fatal error.
-
- ### Entity Animation
-
- The entanim() function creates sprite animations that cycle through keyframes:
-
- ```lua
- -- Animate entity
- -- entanim(entity, start_keyframe, length, rate)
- entanim(e, 0, 4, 2) -- Animate frames 0-3, advancing every 2 display() calls
-
- -- Create and automatically delete a one-shot effect
- local effect = entpos(entspr(ent(), 20), 100, 100)
- entanim(effect, 0, 6, 1) -- Animate frames 0-5
- del(effect, 1) -- Delete when animation completes
- ```
-
- The rate parameter controls animation speed—higher values mean slower animation. Each display() call advances the animation counter, so the effective animation speed depends on your frame rate.
-
- ### Getting All Entities
-
- For advanced use cases, you can retrieve a table of all active entities:
+ ### Structured Save Class
```lua
- -- Get table of all entities
- local all_entities = ents()
-
- -- Iterate through entities
- for i = 1, #all_entities do
- local e = all_entities[i]
- -- Process each entity
- end
- ```
-
- Use this function sparingly—it allocates a new table each call, which impacts performance. Only call it when necessary, such as when switching scripts and needing to clean up or preserve entity state.
-
- ## Input System
-
- ### Button State Functions
-
- The input system provides three functions for different button states:
+ SaveGame = {}
+ SaveGame.__index = SaveGame
- ```lua
- -- btn(num) - Returns true if button is currently held down
- if btn(0) then -- A button
- -- Player is holding A
+ function SaveGame:new()
+ local self = setmetatable({}, SaveGame)
+ self.created = false
+ return self
end
- -- btnp(num) - Returns true on frame button was pressed (just pressed)
- if btnp(6) then -- Up button
- -- Player just pressed Up (transition from unpressed to pressed)
+ function SaveGame:create()
+ if peek(_SRAM + 31999) ~= 0x42 then
+ poke4(_SRAM, 1) -- version
+ poke4(_SRAM + 4, 1) -- created
+ poke4(_SRAM + 8, 0) -- packed
+
+ self.player = {x = 120, y = 80, health = 100, score = 0, lives = 3}
+ poke4(_SRAM + 12, self.player.x)
+ poke4(_SRAM + 16, self.player.y)
+ poke4(_SRAM + 20, self.player.health)
+ poke4(_SRAM + 24, self.player.score)
+ poke(_SRAM + 28, self.player.lives)
+
+ poke(_SRAM + 31999, 0x42)
+ self.created = true
+ end
end
- -- btnnp(num) - Returns true on frame button was released (just not pressed)
- if btnnp(1) then -- B button
- -- Player just released B
+ function SaveGame:save()
+ poke(_SRAM, self.player.x)
+ poke(_SRAM + 4, self.player.y)
+ poke(_SRAM + 8, self.player.health)
+ poke(_SRAM + 12, self.player.score)
+ poke(_SRAM + 16, self.player.lives)
end
- ```
- Button IDs: 0=A, 1=B, 2=Start, 3=Select, 4=Left, 5=Right, 6=Up, 7=Down, 8=L bumper, 9=R bumper.
-
- Use btn() for continuous actions like movement, btnp() for single-trigger actions like jumping or menu selection, and btnnp() for detecting button releases.
-
- ### Input Example
-
- ```lua
- function update_movement(dt)
- local dx = 0
- local dy = 0
-
- -- Continuous movement with directional buttons
- if btn(4) then dx = dx - 1 end -- Left
- if btn(5) then dx = dx + 1 end -- Right
- if btn(6) then dy = dy - 1 end -- Up
- if btn(7) then dy = dy + 1 end -- Down
-
- -- Normalize diagonal movement
- if dx ~= 0 and dy ~= 0 then
- dx = dx * 0.707
- dy = dy * 0.707
- end
-
- -- Apply movement
- x = x + dx * speed * dt
- y = y + dy * speed * dt
+ function SaveGame:load()
+ if not self.created then return false end
- -- Single actions
- if btnp(0) then
- -- Jump on A press
- velocity_y = -10
- end
+ self.player.x = peek(_SRAM)
+ self.player.y = peek(_SRAM + 4)
+ self.player.health = peek(_SRAM + 8)
+ self.player.score = peek(_SRAM + 12)
+ self.player.lives = peek(_SRAM + 16)
- if btnp(2) then
- -- Pause menu on Start
- game_state = "paused"
- end
+ return true
end
```
- ## Text Rendering
-
- ### Basic Printing
-
- The print() function renders text to the overlay layer using the built-in system font:
-
- ```lua
- -- Basic usage
- print("Hello, World!", 1, 1)
-
- -- Custom colors (foreground, background)
- print("Colored text", 5, 5, 0xFFFF, 0x0000)
-
- -- Using custom color IDs (palette indices)
- print("Custom palette", 1, 10, 4, 5)
- ```
-
- The x and y coordinates are in tile units (not pixels), representing positions in the 32x32 overlay grid. Each tile is 8x8 pixels, so coordinate (30, 19) places text at the bottom-right corner of the screen.
-
- ### Character Limitations
-
- Text rendering requires copying glyphs into VRAM. The engine uses the first 80 tile slots in the overlay layer for glyph mapping, which means you cannot display more than 80 unique text characters onscreen simultaneously. This limitation matters most in games with many different characters visible at once, such as RPGs with dialogue boxes.
-
- ```lua
- -- This works fine (fewer than 80 unique characters)
- print("Score: 100", 1, 1)
- print("Lives: 3", 1, 3)
-
- -- This may cause issues with many unique characters
- print("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 1, 1)
- -- Many repeated characters are fine, but each unique character uses a slot
- ```
-
- If you exceed the limit, some characters may not render correctly. Strategies to work around this include limiting visible text, using fewer unique characters, or breaking long text across multiple frames.
-
- ### UTF-8 Support
-
- BPCore supports UTF-8 encoded text, including characters beyond basic ASCII:
-
- ```lua
- -- English text
- print("Score: " .. score, 1, 1)
-
- -- Accented characters (Spanish, French)
- print("¡Hola! ¿Cómo estás?", 1, 5)
-
- -- Japanese Katakana
- print("ゲーム", 1, 10)
-
- -- Russian alphabet
- print("Привет", 1, 12)
-
- -- Chinese characters (2500 most common included)
- print("你好世界", 1, 15)
- ```
-
- The engine includes English alphanumeric characters, accented characters for Spanish and French, a selection of Japanese Katakana, the Russian alphabet, some Scandinavian glyphs, and approximately 2500 of the most common Chinese characters. Not all Unicode characters are supported—only those included in the built-in font.
-
- ### System Font Colors
-
- The overlay layer shares graphics memory with the system font. Text uses color indices 2 and 3 from the overlay layer's palette by default for foreground and background. To customize these colors, include an 8x8 pixel calibration tile at index 0 of your overlay tileset: the top band sets the text background, the middle band sets the foreground, and the bottom band should be black.
-
- ```lua
- -- Text with default colors uses palette indices 2 (fg) and 3 (bg)
- print("Default colors", 1, 1)
-
- -- Custom color IDs reference palette entries directly
- print("Custom", 1, 5, 4, 6) -- Use palette index 4 for foreground, 6 for background
- ```
-
- ## Audio System
-
- ### Music Playback
-
- The music() function plays background music from audio files:
-
- ```lua
- -- Start playing music from the beginning
- music("music.raw", 0)
-
- -- Start playing from a specific offset (in microseconds)
- music("battle_music.raw", 0)
- music("victory_music.raw", 0) -- Jump to victory theme
-
- -- Music loops automatically
- ```
-
- Audio files must be mono 16kHz signed 8-bit PCM format. The .raw extension is conventional—your audio files can have any extension as long as the format is correct. Many audio editors or FFmpeg can convert audio to this format:
-
- ```bash
- # Convert MP3 to GBA-compatible format using FFmpeg
- ffmpeg -i input.mp3 -ar 16000 -ac 1 -f sv8 input.raw
- ```
-
- The offset parameter lets you start playback from a specific position in the file, useful for looping music at different points or creating seamless transitions.
-
- ### Sound Effects
-
- The sound() function plays one-shot sound effects:
-
- ```lua
- -- Play sound effect (file, priority)
- sound("jump.wav", 5)
- sound("coin.wav", 10)
- sound("explosion.wav", 8)
-
- -- Higher priority sounds can evict lower priority ones
- -- The engine has 3 sound channels plus 1 music channel
- ```
-
- The engine supports four audio channels total: three for sound effects and one for music. When you play a sound and all three effect channels are busy, the engine evicts the sound with the lowest priority to play your new sound. Set higher priority values for more important sounds like player jumping or taking damage, and lower priorities for ambient or frequently playing sounds.
-
- ```lua
- -- Example: Prioritize player sounds over environmental sounds
- sound("player_jump.wav", 100) -- Very important
- sound("player_land.wav", 90) -- Important
- sound("footstep.wav", 10) -- Less important
- sound("wind.wav", 5) -- Can be interrupted
- ```
-
- ## Memory System
-
- ### Memory Regions
+ ## Multiplayer Link Cable Protocol
- The GBA provides several memory regions accessible through the engine's peek/poke functions. Two regions are writable: _IRAM (Internal RAM, 8000 bytes of fast on-chip memory) and _SRAM (Save RAM, 32KB of persistent storage). Other memory regions can be read but not written directly.
+ ### Hardware
- ```lua
- -- Access IRAM (fast, volatile)
- poke(_IRAM, 65) -- Write byte 65 to IRAM
- poke4(_IRAM + 4, 12345) -- Write 32-bit word
+ Two GBA systems connect via link cable on right side of cartridge.
- local val = peek(_IRAM) -- Read byte
- local val4 = peek4(_IRAM + 4) -- Read 32-bit word
+ ### Communication
- -- Access SRAM (slower, persistent across restarts)
- poke(_SRAM, 100)
- poke4(_SRAM + 100, 999)
```
-
- _IRAM is fast but volatile—data is lost when the game restarts. Use it for temporary data that doesn't need to persist between sessions, such as caching or inter-script communication. _SRAM is slower but persistent—data survives game restarts and can be used for save games.
-
- ### String Operations
-
- For larger data transfers, use memput and memget:
-
- ```lua
- -- Write string to memory
- memput(_IRAM, "Hello")
-
- -- Read string from memory
- local data = memget(_IRAM, 5) -- Read 5 bytes
- print(data, 1, 1) -- Prints "Hello"
-
- -- Write multiple values
- poke4(_IRAM, 1000) -- Player score
- poke4(_IRAM + 4, 50) -- Player health
- poke4(_IRAM + 8, 3) -- Player lives
- poke(_IRAM + 12, 1) -- Current level
-
- -- Read saved game data
- local score = peek4(_SRAM)
- local health = peek4(_SRAM + 4)
- local level = peek(_SRAM + 8)
+ Device 1 Device 2
+ ┌─────────────┐ ┌─────────────┐
+ │ SEND QUEUE │ ←──→ │ SEND QUEUE │
+ │ (32pk) │ ←──→ │ (32pk) │
+ └─────────────┘ └─────────────┘
+ │ REC QUEUE │ ←──→ │ REC QUEUE │
+ │ (64pk) │ │ (64pk) │
+ └─────────────┘ └─────────────┘
```
- ### Reading Resource Files
-
- The file() function provides access to bundled resource files:
-
- ```lua
- -- Get pointer and length to a file
- local ptr, len = file("level1.csv")
-
- -- Read first 10 bytes of the file
- local data = memget(ptr, 10)
+ **Limits**: 64 receive packets, 32 send packets per device. Overflow causes loss.
- -- Read individual bytes
- local byte1 = peek(ptr)
- local byte2 = peek(ptr + 1)
+ ### Packet Format
- -- Read file into a string
- for i = 1, len do
- local byte = peek(ptr + i - 1)
- -- Process byte
- end
```
-
- Files are read-only (they reside in ROM), so you cannot write to them directly. Use file() to read game data, level definitions, or any other static data bundled with your game.
-
- ### Save/Load Implementation Example
-
- ```lua
- -- Save game state to SRAM
- function save_game()
- local offset = 0
-
- -- Write player position
- poke4(_SRAM + offset, player_x); offset = offset + 4
- poke4(_SRAM + offset, player_y); offset = offset + 4
-
- -- Write player stats
- poke4(_SRAM + offset, player_health); offset = offset + 4
- poke4(_SRAM + offset, player_score); offset = offset + 4
-
- -- Write level info
- poke(_SRAM + offset, current_level); offset = offset + 1
-
- -- Write flag to indicate save exists
- poke(_SRAM + 255, 0x42) -- Save marker
-
- print("Game Saved!", 10, 10, 0xFFFF)
- end
+ [Sender ID (1 byte)][Data (up to 10 bytes)]
+ [Total: 11 bytes max]
- -- Load game state from SRAM
- function load_game()
- -- Check if save exists
- if peek(_SRAM + 255) ~= 0x42 then
- return false -- No save found
- end
-
- local offset = 0
-
- -- Read player position
- player_x = peek4(_SRAM + offset); offset = offset + 4
- player_y = peek4(_SRAM + offset); offset = offset + 4
-
- -- Read player stats
- player_health = peek4(_SRAM + offset); offset = offset + 4
- player_score = peek4(_SRAM + offset); offset = offset + 4
-
- -- Read level info
- current_level = peek(_SRAM + offset)
-
- return true
- end
+ Examples:
+ "1HELLO" = Player 1 sent
+ "2WORLD" = Player 2 sent
```
- ## Multiplayer System
-
- ### Link Cable Overview
-
- BPCore supports multiplayer gaming through the GBA's link cable, allowing two (and potentially more in future versions) GBA devices to communicate. The implementation uses asynchronous I/O with queues for sending and receiving packets.
-
- Important characteristics: messages are not guaranteed to arrive in order or at all. Each device maintains a 64-packet receive queue and a 32-packet send queue. Overflowing either queue causes packet loss. In practice, limiting send() calls to a few packets per frame prevents any noticeable packet loss.
-
### Connection Management
```lua
- -- Attempt to connect to another GBA
- -- Returns true on success, false on timeout
local connected = connect(10) -- 10 second timeout
if connected then
print("Connected!", 10, 10)
else
- print("Connection failed", 10, 10)
+ print("Failed", 10, 10)
end
- -- During connect(), all other game logic blocks
- -- Only connect() is a blocking call
-
- -- When done playing, disconnect
- disconnect()
+ disconnect() -- Clean up when done
```
- The connect() function is the only blocking call in the multiplayer API. It will wait up to the specified timeout for a connection to be established. If you call connect() while already connected, it automatically calls disconnect() first.
+ `connect()` is blocking. Calling it while connected auto-disconnects first.
- ### Sending and Receiving Messages
+ ### Sending & Receiving
```lua
- -- Send a message (max 11 bytes)
- send("hello") -- Send string
- send("P1:MOVE:10,20") -- Send structured data
- send(string.char(1, 2, 3, 4)) -- Send binary data
+ -- Send (max 11 bytes including sender ID)
+ send("hello")
+ send("P1:MOVE:10,20")
+ send(string.char(1, 2, 3, 4))
- -- Receive messages
+ -- Receive
local packet = recv()
while packet do
- -- packet is prefixed with sender ID: "1hello" or "2hello"
local sender = string.sub(packet, 1, 1)
- local message = string.sub(packet, 2)
+ local data = string.sub(packet, 2)
if sender == "1" then
- -- Message from player 1
+ -- From player 1
elseif sender == "2" then
- -- Message from player 2
+ -- From player 2
end
- packet = recv() -- Get next message
+ packet = recv()
end
```
- Messages have a maximum size of 11 bytes of data, plus 1 byte for the sender ID prefix. The first character of received messages indicates which device sent it: "1" for the first connected device, "2" for the second. Even with only two players, messages include this prefix for future compatibility with four-player support.
-
- ### Basic Multiplayer Example
+ ### Basic Example
```lua
- -- Simple two-player position sync
-
- -- Player state
- local my_x = 120
- local my_y = 80
+ local my_x, my_y = 120, 80
+ local opp_x, opp_y = 0, 0
function update_network()
- -- Send my position
- local msg = string.char(my_x) .. string.char(my_y)
- send(msg)
+ send(string.char(my_x) .. string.char(my_y))
- -- Receive opponent position
local pkt = recv()
while pkt do
opp_x = string.byte(pkt, 2)
opp_y = string.byte(pkt, 3)
pkt = recv()
end
end
function draw_network()
- -- Draw opponent
spr(1, opp_x, opp_y)
-
- -- Draw myself
spr(0, my_x, my_y)
end
```
- ### Advanced Binary I/O
-
- For performance-critical code, use send_iram() and recv_iram() to avoid string operations:
+ ### Binary I/O (Fast)
```lua
- -- Using IRAM for faster packet handling
- function sync_position()
- -- Pack position into IRAM
+ function sync_position_fast()
poke(_IRAM, my_x)
poke(_IRAM + 1, my_y)
-
- -- Send raw packet
+ poke(_IRAM + 2, my_score)
send_iram(_IRAM)
- -- Receive into IRAM
if recv_iram(_IRAM) then
- local sender = peek(_IRAM)
- local their_x = peek(_IRAM + 1)
- local their_y = peek(_IRAM + 2)
- -- Process position
+ opp_x = peek(_IRAM)
+ opp_y = peek(_IRAM + 1)
+ opp_score = peek(_IRAM + 2)
end
end
```
- This approach avoids string allocation in tight loops, which matters when sending frequent updates. The first byte in received IRAM packets is the sender ID, followed by up to 11 bytes of data.
-
- ## Program Structure
-
- ### Main Loop Pattern
+ ### Synchronization
- The typical BPCore game follows an update-draw cycle:
+ #### Client-Server Model
```lua
- -- Game variables
- local x = 100
- local y = 80
- local speed = 2
+ local last_sent_x, last_sent_y = 0, 0
- function update(dt)
- -- Handle input
- if btn(4) then x = x - speed end -- Left
- if btn(5) then x = x + speed end -- Right
- if btn(6) then y = y - speed end -- Up
- if btn(7) then y = y + speed end -- Down
+ function update_gameplay()
+ if btn(4) then my_x = my_x - speed end
+ if btn(5) then my_x = my_x + speed end
- -- Clamp to screen bounds
- if x < 0 then x = 0 end
- if x > 224 then x = 224 end
- if y < 0 then y = 0 end
- if y > 144 then y = 144 end
+ if last_sent_x ~= my_x or last_sent_y ~= my_y then
+ send(string.char(my_x) .. string.char(my_y))
+ last_sent_x, last_sent_y = my_x, my_y
+ end
end
+ ```
- function draw()
- clear()
- spr(0, x, y)
- display()
- end
+ #### Lerp/Interpolation
- -- Main loop
- function main_loop()
- while true do
- update(delta())
- clear()
- draw()
+ ```lua
+ local target_x, target_y = 0, 0
+ local lerp_factor = 0.1
+
+ function update_gameplay()
+ if current_time - last_network_update > 100 then
+ local pkt = recv()
+ if pkt then
+ target_x = string.byte(pkt, 2)
+ target_y = string.byte(pkt, 3)
+ last_network_update = current_time
+ end
end
+
+ my_x = my_x + (target_x - my_x) * lerp_factor
+ my_y = my_y + (target_y - my_y) * lerp_factor
end
-
- main_loop()
```
- The game loop continuously calls update() with the time delta, clears the screen, draws all visible sprites, and displays them. Place all game logic in update() and all drawing in draw() for clean organization.
+ ## Optimization Patterns
- ### Script Management
+ ### Sprite Batching
- For games larger than available RAM, split your code into multiple scripts:
+ **Problem**: Sprites redraw every frame - excessive calls waste CPU.
- ```lua
- -- main.lua
- local player_x = 100
- local player_y = 100
- local score = 0
+ **Solution**: Batch stationary sprites.
- -- Save state to IRAM for next script
- poke4(_IRAM, player_x)
- poke4(_IRAM + 4, player_y)
- poke4(_IRAM + 8, score)
+ ```lua
+ local static_sprites = {}
+ local dynamic_sprites = {}
- -- Store entities that need cleanup
- local all_ents = ents()
- poke4(_IRAM + 12, #all_ents)
+ function init_static_sprites()
+ static_sprites[1] = {1, 100, 80, false, false}
+ static_sprites[2] = {2, 50, 80, false, false}
+ end
- -- Load next script
- next_script("game.lua")
+ function draw()
+ clear()
+
+ for i = 1, #static_sprites do
+ local s = static_sprites[i]
+ spr(s[1], s[2], s[3], s[4], s[5])
+ end
+
+ for i = 1, #dynamic_sprites do
+ local s = dynamic_sprites[i]
+ spr(s[1], s[2], s[3], s[4], s[5])
+ end
+
+ display()
+ end
```
- When calling next_script(), the current script finishes execution and the engine loads and runs the specified script. Scripts start with a clean slate—they have no access to the previous script's Lua variables. Use IRAM to pass data between scripts.
+ ### Entity Pool Management
+ **Problem**: Engine supports 128 entities - creating new ones is wasteful.
+
+ **Solution**: Pre-allocate and reuse.
+
```lua
- -- game.lua (continuing from main.lua)
+ local entities = {}
+ local next_entity = 0
+ local active_entities = 0
- -- Restore state from IRAM
- local player_x = peek4(_IRAM)
- local player_y = peek4(_IRAM + 4)
- local score = peek4(_IRAM + 8)
+ function pool_init(count)
+ for i = 1, count do
+ local e = ent()
+ entities[i] = e
+ entspd(e, 0, 0)
+ end
+ next_entity = 1
+ active_entities = count
+ end
- -- Continue game...
+ function pool_create(props)
+ if next_entity > #entities then
+ error("Pool exhausted!")
+ end
+
+ local e = entities[next_entity]
+ next_entity = next_entity + 1
+
+ entpos(e, props.x, props.y)
+ entspr(e, props.sprite, props.xflip, props.yflip)
+ entag(e, props.tag)
+ active_entities = active_entities + 1
+
+ return e
+ end
+
+ function pool_recycle(entity)
+ for i = 1, #entities do
+ if entities[i] == entity then
+ table.remove(entities, i)
+ break
+ end
+ end
+ del(entity)
+ active_entities = active_entities - 1
+ end
```
- Any entities created in the previous script persist in the engine, but your Lua variables do not. Use ents() to get the list of existing entities if you need to manage them across scripts.
+ ### Audio Mixing Limits
- ### Including Shared Code
+ **Constraint**: 3 sound channels + 1 music channel.
- The dofile() function loads additional Lua files:
+ **Strategy**: Prioritize important sounds.
```lua
- -- utils.lua
- function clamp(val, min, max)
- if val < min then return min end
- if val > max then return max end
- return val
- end
+ local AUDIO_PRIORITIES = {
+ player_jump = 100,
+ player_attack = 90,
+ player_hurt = 95,
+ collect_item = 50,
+ background = 10
+ }
- function distance(x1, y1, x2, y2)
- local dx = x2 - x1
- local dy = y2 - y1
- return math.sqrt(dx*dx + dy*dy)
+ function play_sound(filename, importance)
+ local priority = importance or AUDIO_PRIORITIES[filename] or 50
+ sound(filename, priority)
end
```
+ ### State Machine for Game Flow
+
```lua
- -- main.lua
- dofile("utils.lua")
+ local GameState = {
+ main_menu = 0,
+ playing = 1,
+ paused = 2,
+ game_over = 3
+ }
- -- Now use functions from utils.lua
- local x = clamp(player_x, 0, 224)
- local dist = distance(player_x, player_y, enemy_x, enemy_y)
- ```
+ local current_state = GameState.main_menu
- Unlike standard Lua dofile, the BPCore version does not return values. Place all your code in the loaded file at the top level (not inside functions) for it to execute. The dofile approach can reduce memory usage compared to having duplicate code in multiple scripts.
+ function state_transition(new_state)
+ print("State: " .. new_state, 1, 1, 0xFFFF)
+ current_state = new_state
+ end
- ## System Functions
+ function update_states()
+ if current_state == GameState.main_menu then
+ if btnp(0) then state_transition(GameState.playing) end
+ elseif current_state == GameState.playing then
+ update_gameplay()
+ if btnp(2) then state_transition(GameState.paused) end
+ if player_health <= 0 then state_transition(GameState.game_over) end
+ elseif current_state == GameState.paused then
+ if btnp(0) then state_transition(GameState.playing) end
+ elseif current_state == GameState.game_over then
+ if btnp(0) then
+ player_health = 100
+ player_x = 120
+ player_y = 80
+ state_transition(GameState.main_menu)
+ end
+ end
+ end
+ ```
- ### Time and Timing
+ ### Rendering Optimization
```lua
- -- Get time since last call (in microseconds)
- local dt = delta()
+ local last_tile_updates = {}
- -- Sleep for N frames
- sleep(60) -- Sleep for 60 frames (about 1 second at 60fps)
+ function update_tiles(x, y, tile_id)
+ if last_tile_updates[x] ~= y then
+ tile(2, x, y, tile_id)
+ last_tile_updates[x] = y
+ end
+ end
- -- Get startup time (if RTC hardware present)
- local boot_time = startup_time()
- -- Returns table: {year, month, day, hour, minute, second}
- print("Booted: " .. boot_time.hour .. ":" .. boot_time.minute, 1, 1)
+ function draw_visible_entities()
+ for i = 1, #entities do
+ local e = entities[i]
+ local ex, ey = entpos(e)
+
+ if ex < 0 or ex > 224 or ey < 0 or ey > 144 then
+ continue
+ end
+ spr(e)
+ end
+ end
```
- Use delta() to implement frame-rate independent movement. Multiply speeds by dt to ensure consistent movement regardless of actual frame rate. Note that delta() returns microseconds (1/1,000,000 second), so divide by a scaling factor:
+ ### Physics Optimizations
```lua
- local dt = delta() / 1000 -- Convert to milliseconds
- local dt_seconds = delta() / 1000000 -- Convert to seconds
+ function check_collision(x1, y1, w1, h1, x2, y2, w2, h2)
+ return x1 < x2 + w2 and x1 + w1 > x2 and y1 < y2 + h2 and y1 + h1 > y2
+ end
- -- Frame-rate independent movement
- x = x + velocity_x * (delta() / 10000)
+ function check_distance(x1, y1, x2, y2)
+ local dx = x2 - x1
+ local dy = y2 - y1
+ return dx * dx + dy * dy < 2500 -- 50px radius squared
+ end
```
- ### Watchdog
+ ## Camera & Scrolling System
+ ### Fundamentals
+
```lua
- -- Feed the watchdog timer
- fdog()
+ -- Camera center (screen: 240x160)
+ camera(120, 80) -- Middle of screen
- -- Not needed if calling clear() every frame
- -- Manually feed when doing long operations without display
- while loading do
- -- Long loading process
- fdog() -- Prevent watchdog timeout
- end
+ -- Scroll layers independently
+ scroll(2, 100, 50) -- Tile_0 scroll
+ scroll(1, 0, 0) -- Tile_1 no scroll
+ scroll(0, 16, 0) -- Overlay absolute scroll
```
- The watchdog timer reloads the ROM if the engine doesn't receive clear() and display() calls for more than 10 seconds. Most games don't need to manually call fdog() since they call clear() every frame anyway. However, during level loading or other operations that don't update the display, call fdog() periodically to prevent automatic reload.
+ ### Parallax Scrolling
- ### Debug Logging
+ ```lua
+ function update_camera()
+ local x, y = player_x - 120, player_y - 80
+ camera(x, y)
+
+ -- Background: 0.5x speed
+ scroll(3, (x - camera_x) * 0.5, (y - camera_y) * 0.5)
+
+ -- Tile layer 0: 0.8x speed
+ scroll(2, (x - camera_x) * 0.8, (y - camera_y) * 0.8)
+
+ -- Tile layer 1: no scroll (foreground)
+ scroll(1, 0, 0)
+ end
+ ```
+ ### World Bounds
+
```lua
- -- Write to mGBA emulator's debug log
- log("Player position: " .. x .. ", " .. y)
- log("Score: " .. score)
+ local world_width = 512 -- 32 tiles × 16 pixels
+ local world_height = 256 -- 32 tiles × 8 pixels
- -- Check engine version
- print("BPCore version: " .. _BP_VERSION, 1, 1)
+ function update_camera_bounds()
+ local x, y = player_x - 120, player_y - 80
+
+ if x < 0 then x = 0 end
+ if x > world_width - 240 then x = world_width - 240 end
+ if y < 0 then y = 0 end
+ if y > world_height - 160 then y = world_height - 160 end
+
+ camera(x, y)
+ end
```
- The log() function outputs to the mGBA emulator's logging window at debug severity. This is invaluable for debugging, especially for issues that are difficult to reproduce with visual debugging alone.
-
- ## Complete Example
+ ## Error Handling
- Here is a complete, runnable game demonstrating many BPCore features:
+ ### Out-of-Bounds Detection
```lua
- -- Complete Example Game
- -- A simple sprite that moves and collects items
-
- -- Load graphics
- txtr(4, "sprites.bmp")
- txtr(2, "tiles.bmp")
+ function safe_tile(layer, x, y, tile_num)
+ if x < 0 or x > 127 or y < 0 or y > 127 then
+ print("Tile out of bounds", 1, 1, 0xFFFF)
+ return false
+ end
+ tile(layer, x, y, tile_num)
+ return true
+ end
- -- Fill background with tile 1
- for x = 0, 63 do
- for y = 0, 63 do
- tile(2, x, y, 1)
+ function safe_sprite(sprite, x, y, xflip, yflip)
+ if x < 0 or x > 224 or y < 0 or y > 144 then
+ print("Sprite out of bounds", 1, 1, 0xFFFF)
+ return false
end
+ spr(sprite, x, y, xflip, yflip)
+ return true
end
+ ```
- -- Game state
- local player = entpos(entspr(ent(), 0), 112, 72)
- local score = 0
- local items = {}
+ ### Collision Safety
- -- Create some collectible items
- for i = 1, 5 do
- local item = entpos(entspr(ent(), 1),
- math.random(20, 220),
- math.random(20, 140))
- entag(item, 100) -- Tag as collectible
- items[i] = item
+ ```lua
+ function safe_collision_check(e1, e2)
+ if not e1 or not e2 then
+ print("Invalid entity", 1, 1, 0xFFFF)
+ return false
+ end
+ return eccole(e1, e2)
end
+ ```
- -- Player speed
- local speed = 2
+ ### Entity Pool Safety
- function update(dt)
- -- Get current position
- local x, y = entpos(player)
-
- -- Movement
- if btn(4) then x = x - speed end
- if btn(5) then x = x + speed end
- if btn(6) then y = y - speed end
- if btn(7) then y = y + speed end
-
- -- Clamp to screen
- x = math.max(0, math.min(224, x))
- y = math.max(0, math.min(144, y))
-
- -- Update position
- entpos(player, x, y)
-
- -- Check collisions with items
- local collisions = ecoll(player, 100)
- for i = 1, #collisions do
- local item = collisions[i]
-
- -- Remove collected item
- for j = 1, #items do
- if items[j] == item then
- table.remove(items, j)
- break
- end
- end
- del(item)
-
- -- Increase score and play sound
- score = score + 10
- sound("coin.wav", 5)
+ ```lua
+ function safe_create_entity(props)
+ if next_entity > #entities then
+ print("Pool exhausted", 1, 1, 0xFFFF)
+ return nil
end
- -- Exit on Start
- if btnp(2) then
- -- Could call next_script here
+ local e = ent()
+ if not e then
+ print("Failed to create entity", 1, 1, 0xFFFF)
+ return nil
end
+
+ entpos(e, props.x, props.y)
+ entspr(e, props.sprite, props.xflip, props.yflip)
+ return e
end
+ ```
- function draw()
- clear()
+ ### Multiplayer Safety
+
+ ```lua
+ function safe_receive_packet()
+ local pkt = recv()
+ if not pkt then
+ return nil, "No packet"
+ end
- -- Draw UI
- print("Score: " .. score, 1, 1, 0xFFFF, 0x0000)
+ local len = string.len(pkt)
+ if len > 11 then
+ return nil, "Packet too long"
+ end
+ if len < 2 then
+ return nil, "Packet too short"
+ end
- -- Items are automatically drawn (entities)
- -- Player is automatically drawn
+ local sender = string.byte(pkt, 1)
+ if sender ~= 1 and sender ~= 2 then
+ return nil, "Unknown sender"
+ end
- display()
+ return pkt, "OK"
end
+ ```
- -- Main loop
- function main_loop()
- while true do
- update(delta())
- clear()
- draw()
+ ### Memory Bounds
+
+ ```lua
+ function safe_poke(addr, value)
+ if addr < 0 or addr > 31999 then
+ print("Invalid address", 1, 1, 0xFFFF)
+ return false
end
+ poke(addr, value)
+ return true
end
- main_loop()
+ function safe_peek(addr)
+ if addr < 0 or addr > 31999 then
+ return 0
+ end
+ return peek(addr)
+ end
```
- This example demonstrates entity creation, sprite assignment, movement, collision detection, scoring, and the game loop structure. Study it as a template for your own games.
-
- ## Best Practices
-
- When developing with BPCore Engine, keep these guidelines in mind:
-
- Resource Management is critical on the GBA. Always delete entities when no longer needed with del(). Avoid creating objects every frame—pool reusable entities instead. Monitor your memory usage with collectgarbage("count") and watch for leaks.
-
- Performance Optimization matters significantly. The GBA has limited CPU power, so minimize calculations per frame. Use entities for persistent objects, sprites for transient effects. Call clear() and display() each frame but set up tiles only once during loading. Consider reducing frame rate with flimit(30) if needed.
-
- Layer Management requires planning. Use the right layer IDs: 0=overlay, 1=tile_1, 2=tile_0, 3=background, 4=sprites. Remember tile layers are persistent while sprites must be redrawn each frame. Plan your graphics memory usage carefully.
-
- Input Handling should use the right function for each purpose. Use btn() for continuous actions like movement, btnp() for triggered actions like jumping, and btnnp() for detecting releases.
-
- Save Data should use appropriate memory. Use _IRAM for temporary data between scripts. Use _SRAM for persistent save games. Always check for valid save data before loading.
+ ---
- Multiplayer requires handling asynchronous communication. Don't assume messages arrive. Limit send() calls to avoid queue overflow. Implement reconciliation for important state.
+ ## Summary
- The GBA is a constrained platform, and BPCore makes tradeoffs to enable Lua development. Keep your games focused and small. Complex games may exceed the platform's capabilities even with optimized Lua code.
+ This skill covers BPCore Engine development for GBA:
- ## Additional Resources
+ - **API Reference**: 60+ functions with signatures
+ - **Installation & Project Structure**: BPCoreEngine.gba sources, build process
+ - **Save/Load to SRAM**: Persistent storage patterns
+ - **Multiplayer Link Cable Protocol**: Packet formats, binary I/O
+ - **Optimization Patterns**: Sprite batching, entity pools, audio mixing, state machines
+ - **Camera & Scrolling**: Parallax, bounds
+ - **Error Handling**: Validation, safe operations
- For more information and example projects, see the official BPCore Engine repository at https://github.com/evanbowman/BPCore-Engine. The repository includes additional demo projects such as Meteorain (a puzzle game), HyperWing (a shoot-em-up boss rush), and various demonstration programs showing specific engine features.
+ Refer to [official repository](https://github.com/evanbowman/BPCore-Engine) for updates.
- When creating assets for your games, remember these technical requirements. Sprites must be 16x16 pixels, tiles must be 8x8 pixels. Audio must be mono 16kHz signed 8-bit PCM. Images should use indexed color with appropriate palettes. Tilemaps should be CSV format with comma delimiters.
+ #BQ|---
- This skill covers the complete BPCore Engine API. Refer to it when building your GBA games, and consult the official documentation for any recently added features or updates to existing functions.