원클릭으로
bpcore-engine
// Lua game framework for Gameboy Advance with sprites, tilemaps, entities, collision, audio, multiplayer
// Lua game framework for Gameboy Advance with sprites, tilemaps, entities, collision, audio, multiplayer
Build KDE Plasma 6 widgets with Python backend and QML UI, including metadata, deployment, and KDE Store distribution
Develop plugins, tools, and extensions for OpenCode AI coding agent with MCP, LSP integration, custom tools, and SDK usage
Async HTTP server and client for Python with WebSocket support, middleware, streaming, and server-sent events
Data validation using Python type hints with Pydantic models, settings, serialization, and performance optimization
PyQt/PySide multimedia - audio playback, video playback, camera, audio recording, media player
PyQt/PySide6 overview hub - installation, comparison, project structure. See sub-skills for detailed topics.
| name | bpcore-engine |
| description | 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"]} |
Comprehensive guide for building Gameboy Advance games using the BPCore Engine Lua framework.
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.
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.
| 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 |
| 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 |
| 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 |
| 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) |
| Function | Signature | Description |
|---|---|---|
btn(num) | btn(num) | Button held? (bool) |
btnp(num) | btnp(num) | Button pressed? (bool) |
btnnp(num) | btnnp(num) | Button released? (bool) |
| Function | Signature | Description |
|---|---|---|
print(text, x, y, fg?, bg?) | print(text, x, y, [fg], [bg]) | Print to overlay |
| Function | Signature | Description |
|---|---|---|
music(f, offset?) | music(f, [offset]) | Play background music |
sound(f, priority) | sound(f, priority) | Play sound effect |
| 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 |
| 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 |
| 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 |
| 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 |
Important: BPCore Engine requires BPCoreEngine.gba base ROM with compiled C++ code and Lua interpreter. This is not a custom ROM you create.
Official Repository: Clone from GitHub
git clone https://github.com/evanbowman/BPCore-Engine.git
cd BPCore-Engine
Required Files: Repository contains:
BPCoreEngine.gba - Engine base ROM (~3.5 MB)build.lua - Build script (Lua 5.3)test/ - Example projectsAlternatives:
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
./
├── 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
Step 1: Create manifest.lua
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" }
}
return app
Step 2: Run build script
lua build.lua manifest.lua BPCoreEngine.gba output.gba
Step 3: Verify
Build checks manifest files exist, formats are valid, scripts readable, output ROM created (~3.5-4 MB).
Step 4: Test with mGBA
mGBA output.gba --log --memview
--log shows engine log() output, --memview opens memory viewer for debugging.
BMP Tilesets: Indexed color (256 colors), tiles are 8x8 pixels. Overlay: 16x16 tiles, Tile_0/1: 32x32 tiles, Sprites: 8x8 sprites.
Audio: Mono 16kHz signed 8-bit PCM. Convert with FFmpeg:
ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s8 output.raw
Tilemap CSV: Comma-separated tile indices, each row is one tile row. Tile indices start at 1 (0 = empty).
GBA provides 32KB SRAM that persists across restarts, unlike volatile IRAM (8KB).
SRAM: 32KB
├── Offset 0x00: Save data
├── Offset 0xFF: Save marker
└── Offset 0x00-0xFF: Checksum
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
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
SaveGame = {}
SaveGame.__index = SaveGame
function SaveGame:new()
local self = setmetatable({}, SaveGame)
self.created = false
return self
end
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
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
function SaveGame:load()
if not self.created then return false 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)
return true
end
Two GBA systems connect via link cable on right side of cartridge.
Device 1 Device 2
┌─────────────┐ ┌─────────────┐
│ SEND QUEUE │ ←──→ │ SEND QUEUE │
│ (32pk) │ ←──→ │ (32pk) │
└─────────────┘ └─────────────┘
│ REC QUEUE │ ←──→ │ REC QUEUE │
│ (64pk) │ │ (64pk) │
└─────────────┘ └─────────────┘
Limits: 64 receive packets, 32 send packets per device. Overflow causes loss.
[Sender ID (1 byte)][Data (up to 10 bytes)]
[Total: 11 bytes max]
Examples:
"1HELLO" = Player 1 sent
"2WORLD" = Player 2 sent
local connected = connect(10) -- 10 second timeout
if connected then
print("Connected!", 10, 10)
else
print("Failed", 10, 10)
end
disconnect() -- Clean up when done
connect() is blocking. Calling it while connected auto-disconnects first.
-- Send (max 11 bytes including sender ID)
send("hello")
send("P1:MOVE:10,20")
send(string.char(1, 2, 3, 4))
-- Receive
local packet = recv()
while packet do
local sender = string.sub(packet, 1, 1)
local data = string.sub(packet, 2)
if sender == "1" then
-- From player 1
elseif sender == "2" then
-- From player 2
end
packet = recv()
end
local my_x, my_y = 120, 80
local opp_x, opp_y = 0, 0
function update_network()
send(string.char(my_x) .. string.char(my_y))
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()
spr(1, opp_x, opp_y)
spr(0, my_x, my_y)
end
function sync_position_fast()
poke(_IRAM, my_x)
poke(_IRAM + 1, my_y)
poke(_IRAM + 2, my_score)
send_iram(_IRAM)
if recv_iram(_IRAM) then
opp_x = peek(_IRAM)
opp_y = peek(_IRAM + 1)
opp_score = peek(_IRAM + 2)
end
end
local last_sent_x, last_sent_y = 0, 0
function update_gameplay()
if btn(4) then my_x = my_x - speed end
if btn(5) then my_x = my_x + speed 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
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
Problem: Sprites redraw every frame - excessive calls waste CPU.
Solution: Batch stationary sprites.
local static_sprites = {}
local dynamic_sprites = {}
function init_static_sprites()
static_sprites[1] = {1, 100, 80, false, false}
static_sprites[2] = {2, 50, 80, false, false}
end
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
Problem: Engine supports 128 entities - creating new ones is wasteful.
Solution: Pre-allocate and reuse.
local entities = {}
local next_entity = 0
local active_entities = 0
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
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
Constraint: 3 sound channels + 1 music channel.
Strategy: Prioritize important sounds.
local AUDIO_PRIORITIES = {
player_jump = 100,
player_attack = 90,
player_hurt = 95,
collect_item = 50,
background = 10
}
function play_sound(filename, importance)
local priority = importance or AUDIO_PRIORITIES[filename] or 50
sound(filename, priority)
end
local GameState = {
main_menu = 0,
playing = 1,
paused = 2,
game_over = 3
}
local current_state = GameState.main_menu
function state_transition(new_state)
print("State: " .. new_state, 1, 1, 0xFFFF)
current_state = new_state
end
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
local last_tile_updates = {}
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
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
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
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
-- Camera center (screen: 240x160)
camera(120, 80) -- Middle of screen
-- 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
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
local world_width = 512 -- 32 tiles × 16 pixels
local world_height = 256 -- 32 tiles × 8 pixels
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
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
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
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
function safe_create_entity(props)
if next_entity > #entities then
print("Pool exhausted", 1, 1, 0xFFFF)
return nil
end
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 safe_receive_packet()
local pkt = recv()
if not pkt then
return nil, "No packet"
end
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
local sender = string.byte(pkt, 1)
if sender ~= 1 and sender ~= 2 then
return nil, "Unknown sender"
end
return pkt, "OK"
end
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
function safe_peek(addr)
if addr < 0 or addr > 31999 then
return 0
end
return peek(addr)
end
This skill covers BPCore Engine development for GBA:
Refer to official repository for updates.
#BQ|---