evanbowman / BPCore-Engine

Lua game framework for Gameboy Advance
The Unlicense
161 stars 10 forks source link
game-engine gameboy-advance gba lua

BPCore-Engine

(Blind jumP Core Engine)

This repository includes parts of the BlindJump C++ engine, hacked together with a Lua interpreter, with the intention of allowing people to make gameboy games without needing to write C++ code or to use a compiler. The lua API for BPCore uses the simple APIs of fantasy consoles, like Pico8 or Tic80, as a model. In fact, many of the commands, like spr() and btn(), are almost the same.

Disclaimer: My goal with this project was to enable people unfamiliar with systems programming languages to make GBA games. But because Lua is resource-intensive for the GBA, this library is only suitable for making relatively small minigames (see Examples). If you want to make something complex and resource-intensive, you will need to learn a lower level language. If you are already experienced with C++, check out https://github.com/GValiente/butano!

Contents

Architecture

For ease of use, the build.lua script allows you to create Gameboy Advance ROMs entirely with Lua: you only need a copy of the build.lua script, a copy of the BPCoreEngine.gba rom, and an installation of Lua 5.3!

But how does this actually work?

The build.lua script parses a user-defined manifest.lua file, which tells the build system which resources to include in the ROM. A manifest will look something like this:


local app = {
   name = "TestApplication",
   gamecode = "ABAB", -- Optional: set the game code in the rom header (four chars)
   makercode = "BC",  -- Optional: set the maker code in the rom header (two chars)

   tilesets = {
      "overlay.bmp",
      "tile0.bmp",
   },

   spritesheets = {
      "spritesheet.bmp",
   },

   audio = {
      "my_music.raw",
   },

   scripts = {
      "main.lua",
   },

   misc = {
      "some_data.txt",
   }
}

return app

build.lua then creates a ROM file, by copying the compiled code in the BPCoreEngine.gba ROM, and appending a new section to the ROM, containing all of the resource files. The engine, upon startup, loads the address of the end of the ROM (provided by the linker), and finds the resource bundle. BPCore then loads the main.lua script from the application bundle, and turns over control to Lua (more or less, the engine does still process interrupts).

API

Sprites and Tiles

The BPCore engine uses the Gameboy Advance's tile-based display mode. All sprites are 16x16 pixels in size, and all tiles are 8x8 pixels wide. The engine provides access to four tiles layers:

To load data from the a bundled file into VRAM, use the txtr() function, with one of the layer ids above. To load a spritesheet, you may also use the txtr() function, with layer id 4.

Function Reference

Button Presses

Graphics

p1, len1 = file("tiles.bmp") p2, len2 = file("other.bmp")

txtr(1, p1, len1) -- swap texture from cached file location, slightly faster. txtr(1, p2, len2)

txtr(1, file("tiles.bmp")) -- you can do this too, although not much reason to.


* `spr(index, x, y, [xflip], [yflip])`
Draw `index` from the spritesheet at screen pixel offset (`x`,`y`). Includes optional flipping flags.

* `tile(layer, x, y, [tile_num])`
Draw tile indicated by `tile_num` in tile layer `layer`, with coordinates `x` and `y`. Unlike `spr()`, tiles are persistent, and do not need to be redrawn for each frame. If called without `tile_num`, will instead return the current tile value at `x`,`y` in `layer`.

* `tilemap(filename, layer, width, height, [dest_x], [dest_y], [src_x], [src_y])`
Deserialize and load a tilemap from a file in the resource bundle. Currently, the file must be a CSV (with comma delimiters!) containing integer tile indices. `dest_x` and `dest_y` represent the top left coordinate in the tile `layer` into which to start loading the tile data. `src_x` and `src_y` represent the top left coordinates in the tilemap file to begin loading the data from. `width` and `height` represent the dimensions of the block of data that you want to load. The first four arguments must be specified, the latter arguments will be assumed to be zero if not supplied. This function will fail if filename does not exist, or if any of the width, height, src, or dest parameters would result in an out of bounds access. You could manually load tiles with the `tile()` function, `tilemap()` mainly exists to allow people to export levels from a map editor, and to speed up map loading. Added in version 2021.9.12.3.

* `fade(amount, [custom_color_hex], [include_sprites], [include_overlay])`
Fade the screen. Amount should be in the range `0.0` to `1.0`.

* `camera(x, y)`
Set the center of the view.

* `scroll(layer, x_amount, y_amount)`
In addition to re-anchoring the camera, you may also manually set the scrolling for any of the tile layers. The scroll amounts for tile_0, tile_1, and the background are all relative, so they will be applied in addition to the camera scrolling. The camera does not scroll the overlay, so the scroll amounts for this layer are absolute.

* `priority(sprite_pr, background_pr, tile0_pr, tile1_pr)`
Reorder the engine's rendering layers, by assigning new priorities to the layers. You should use values 0-3 for priorities, 0 being the nearest layer to the screen, and 3 being the furthest layer. The overlay priority defaults to 0, and may not be changed. Default values upon startup: background=3, tile_0=3, tile_1 = 2, sprite=1, overlay=0. Certain layers will display behind other layers when assigned the same priority. Order of precedence, if all layers were to be assigned the same priority value: sprite > tile_0 > background > overlay > tile_1.

* `clear()`
Clear all sprites from the screen. Should be called once per frame. `clear()` also performs a VSync, so all game updates should be performed before the clear call, and all draw calls should be placed after the clear call. For performance reasons, `clear()` does not erase tiles from the screen. `tile()` calls, and by extension, `print()` calls, are persistent.

* `display()`
Show any recent `spr()` and `tile()` calls.

* `flimit(fps)`
Set a framerate limit. Values of 30 or 60 supported.

* `rline()`
Returns the current raster line number. The GBA screen has 160 lines. If the raster line advances past 160, you've spent too long updating data during the current frame and the game will lag.

### Entities

Entities share a lot in common with sprites, but with a few exceptions:
1) Entities have hitboxes and support collision checking.
2) The engine will automatically redraw entities for you (sprites will display _in front_ of entities).

All entity setters generally return the input entity as a result, so you can write `entpos(entspr(entity, 5), 1, 1)`, by chaining calls together. When called without additional arguments, the entity api functions act as getters (we want to provide getters, without using a bunch of gba memory by registering a duplicate set of functions).

* `ent()`
Create an entity. Max 128 allowed at a time.

* `del(entity, [parameter])`
Destroy an entity. The engine owns and manages all entities, the Lua garbage collector will not collect them. Call `del()` when you're done with an entity. If you pass an extra parameter: the following options are supported: parameter==0: no effect, the entity is not deleted, parameter==1: delete the entity when it finishes its animation.

* `entspr(entity, [sprite_id], [xflip], [yflip])`
Set an entity's sprite, with optional flipping flags. Similar to `spr()`, but for entities. Returns the input entity. When called without any of the last three arguments, returns an entity's sprite info:
```lua
entspr(entity, 5)                    -- set sprite id 5
entspr(entity, 10, true, false)      -- set sprite id with x-flip
local sprid, xflip, yflip = entspr(entity) -- retrieve sprite info

Collisions

The engine offers a few different collision functions for entities:

RAM Read/Write

NOTE: Only _SRAM and _IRAM regions are writable (see Memory below).

ptr, len = file("main.lua")
print(memget(ptr, 10), 1, 1) -- print the first 10 chars of this very script.
print(string.char(peek(ptr + 3)), 1, 3) -- print the fourth byte of this file

Math Utilities

Sound

Program Structure

-- main.lua
local a = 5000
local b = 2000

-- lots of code...

poke4(_IRAM, a)
poke4(_IRAM + 4, b)

next_script("other_file.lua")

Serial I/O

You can use the engine's asynchronous I/O library to send data to another GBA device, using the GBA's multiplayer link mode. Currently, the engine only supports two connected devices, with plans to support four devices in the future.

BPCore's implementation of network I/O does not guarantee that messages will be received in-order, or even received at all. Each device maintains a 64-packet receive queue, as well as a 32-packet send queue. Overflowing either the send queue in the sender, or the receive queue in the receiver, will result in packet loss. That said, I've used this network implementation in several GBA games; Blind Jump, Skyland, etc., and I've never had any problems with packet loss. If you limit your send() calls to a few packets per frame, you will never see any message loss.

Advanced Serial I/O

Admittedly, packing/unpacking binary data from Lua strings can have a performance impact in tight loops. As of version 21.9.13.1, the engine includes two extra send/recv functions, send_iram() and recv_iram(), allowing you to read plain bytes out of the packets with peek() and poke():

Example: advanced serial I/O usage, sends coordinates back and forth between devices:

while not btnp(0) do
   clear()
   display()
   -- wait on a button press, then connect
end
connect(10)

local x = 0
local y = 0
local ox = 0
local oy = 0

while true do

   poke4(_IRAM, x)
   poke4(_IRAM + 4, y)
   send_iram(_IRAM)

   local got_msg = recv_iram(_IRAM)
   while got_msg do
      sender = peek(_IRAM) -- message originator
      ox = peek4(_IRAM + 1) -- x, y from other device
      oy = peek4(_IRAM + 5)
      got_msg = recv_iram(_IRAM)
   end

   clear()
   display()
end

System

Result table format (string keys, integer values)

{
   year = 21,
   month = 8,
   day = 12,
   hour = 11,
   minute = 6,
   second = 3
}

(Added in version 2021.9.12.1)

Reserved words

The function syscall, as well as the variable util, should be considered reserved for future use. Do not use these variable names, if you want to seamlessly migrate to new versions of the engine.

Memory

Memory Constraints

The Gameboy Advance has two memory sections: a small and fast internal work ram (IWRAM), and a much larger block of slightly slower external work ram (EWRAM). Most of the 32kB IWRAM is currently reserved for the engine, leaving 256kB for Lua code and data.

Memory Regions

In addition to the memory used for Lua code and data, the engine provides access to a few other memory regions within the gba hardware, accessible via peek(), peek4(), poke(), and poke4().

Examples

Example Projects

For a project template, see here

A tiny demo


-- play some music from a 16 kHz signed 8-bit pcm wave file
music("my_music.raw", 0)

-- Fade the screen while we load a texture and fill the tile layer.
fade(1)

-- Load tileset from tile0.bmp into VRAM for the tile0 layer (layer 2).
txtr(2, "tile0.bmp")

-- Fill the tile0 map with some tiles.
for i = 0, 63 do
   for j = 0, 63 do
      tile(2, i, j, 1)
   end
end

fade(0)

-- Load texture for sprites into VRAM.
txtr(4, "spritesheet")

function main_loop(update, draw)
   while true do
      update(delta())
      clear()
      draw()
      display()
   end
end

local x = 0
local dir = 0

function update(dt)
   -- fade the screen based on button presses
   if btnp(6) then
      fade(0.5)
   elseif btnnp(6) then
      fade(0)
   end

   -- move the character back and forth
   if dir == 0 then
      if x < 240 then
         x = x + 1
      else
         dir = 1
      end
   else
      if x > 0 then
         x = x - 1
      else
         dir = 0
      end
   end
end

function draw()
   -- draw a sprite for our character
   spr(15, x, 60)
end

-- Let's show how much ram we're using
print(tostring(collectgarbage("count") * 1024), 3, 5)

-- enter main loop
main_loop(update, draw)

Quirks

Draw order

Calls to spr() will draw sprites with increasing depth (z-distance from the screen). Therefore, successive calls to spr() will place sprites behind previously drawn sprites. This may seem backwards at first, but we have a good reason for doing this. The Gameboy Advance only supports 128 sprites onscreen at a time. If you failed to properly keep track of your sprite count, and exceeded the limit, wouldn't you want the sprites further in the background to be hidden, rather than the nearer sprites?

System font default colors

The overlay tile layer shares graphics memory with the system font. If you load an overlay, and find that the colors of your text now display unpredictably, this is becuause the overlay text will always, by default, use the second and third colors to appear in an overlay tilesheet as the foreground and background color. To calibrate the color of the system text, place an 8x8 pixel tile (like the one pictured below) in index zero of any overlay texture. You may set the top gray band to any arbitrary color in your tileset. Change the middle white band to the color that you want to use for the foreground color of the system text. Set the bottom black band to the background color for the system text.

fade() and custom font colors

fade() does not apply to colored text, i.e. if you passed custom color hex values to the print() function. Supporting this would be practically unrealistic given the cpu frequency on a gameboy advance (we cannot realistically linearly interpolate between 256 arbitrary colors within a reasonable amount of time). Regular text, using the default overlay palette, can be faded.

Future Work

The BlindJump source code has tons of other features that I'd like to eventually add to the Lua API. In the future, I plan to add: