This is a hobby cross-platform 2D game engine developed around Lua as both a scripting language and as a data description language.
I don't plan on continuing development of the engine in its current state as I feel that the scripting system ended up too tightly integrated into the other systems. I was still able to prototype a variety of minigames with it and get a feel for what features of the engine that I'd like to recycle on the next engine.
Some parts may be interesting to look at independently from the engine, like the Serializer which dumps the entire game state, including functions, closures, and metatables, to plain-text Lua script.
- Sprite and tilemap graphics
- Tile-based pathfinding and shadow casting
- Collision detection and limited physics (continuous, linear only)
- Serialize entire game state to Lua files
- Gamepad support through SDL and GLFW
- Limited audio support through SDL
CMake 3+ is used for generating the build system. Unix Makefiles and Visual Studio generators have been tested on macOS and Windows respectively.
The engine can be built with SDL and GLFW as platforms, by setting the USE_SDL and USE_GLFW CMake options. SDL is recommended and is enabled by default. If built with both, the platform may be selected at runtime with command-line flags -sdl and -glfw.
On macOS, from the root folder:
mkdir build
cd build
cmake ..
cmake --build .
On Windows, use CMake GUI to generate a Visual Studio solution.
Run the engine with the path to a Lua script as a command-line argument. For example:
./engine asteroid.lua
If a file is not found at the specified path, the directories scripts/ and ../scripts/ will be searched as well.
This example script creates a 3x3 unit Canvas with a single sprite (on an Actor object) drawn in the center.
local game = Canvas {
camera = Camera2D {
size = { 3, 3 },
fixed = true
}
}
addCanvas(game)
local hero = Actor {
graphics = SpriteGraphics { sprite = "hero.tga" },
transform = { position = { 1, 1 } }
}
game:addActor(hero)
More complex example scripts are included in the scripts/ folder:
- asteroid.lua - space shooter game
- bomb.lua - Bomberman clone
- breakout.lua - Breakout clone
- dungeon.lua - timemap beautifier
- painter.lua - tilemap painter
- pftest.lua - pathfinding test
- physics.lua - physics test benches
- platform.lua - platformer test
- rouge.lua - Roguelike test
- runner.lua - endless running test
- snake.lua - multi-player Snake clone
- test.lua - self-reproducing serialization test
- tetromino.lua - a Tetris clone
This section documents the features of the scripting system, including the following data types:
- Canvas
- Camera2D
- Actor
- SpriteGraphics
- AabbCollider
- TileMap
- TileSet
- TileMask
- TiledGraphics
- TiledCollider
- TiledPathing
The scene encompases the global table, list of Canvas objects, and other bits of game state. The scene object is not directly exposed to scripting, but a number of global functions are defined to interact with it.
The following libraries are available: base, table, io, os, string, math, utf8. Added global math constants inf and nan. The following global functions are defined:
addCanvas(canvas)- addcanvasto the scene (stacked on top of the previous)loadClosure(data)- load the Lua function encoded in stringdatasaveState()- save the current scene as Lua script, currently written tostdoutfor developmentplaySample(filename)- play the audio clip located at stringfilenameregisterControl(control, function)- registerfunctionto control named by stringcontrolsetPortraitHint(boolean)- hint to the platform to use a portrait (true) or landscape (false) modequit()- exit the application
Classes are instantiated by their ClassName(table) method. The shorthand ClassName{key=val} can be used as well. Keys in table map to settings on the created object. New instances may also be created by cloning existing objects, by calling ClassName(instance) where instance is an instance of that class.
All instances have individual properties that can be set with the . and [] operators. For example: object.foo = 'bar' and print(object[3]). Additionally, methods set on the object can be called with the : operator, like object:baz(). Built-in properties like Canvas.addActor are read-only, but unused properties can be freely set by the user.
All class constructors take the key members, which is a table containing properties to be set on the object.
Canvas is a game object that acts like a layer in a scene.
A Canvas is created by the Canvas(table) method. The following keys may be set in table:
camera- a Camera to affect the layout of the Canvaspaused- a boolean indicating if the Canvas is pausedvisible- a boolean indicating if the children of the Canvas will be rendered
The following methods are defined on Canvas:
addActor(actor)- addactorto the CanvasremoveActor(actor)- removeactorfrom the Canvasclear()- remove all Actor children from the CanvassetCenter(actor / x, y)- centers the camera on eitheractoror coordinatesx,ysetOrigin(x, y)- places the upper left of the camera at coordinatesx,ygetCollision(x, y)- return the first Actor to collide with coordinatesx,ysetPaused(boolean)- same as thepausedproperty abovesetVisible(boolean)- same as thevisibleproperty above
The following methods may be overloaded on an instance of Canvas:
onUpdatePre(delta)- called before updating Actor children, withdeltaseconds elapsed since last frameonUpdatePost(delta)- called after updating Actor children, withdeltaseconds elapsed since last frameonClickPre(down)- called before propagating mouse clicks, with booleandownindicating press/releaseonClickPost(down)- called after propagating mouse clicks, with booleandownindicating press/release
Camera2D is an implementation of Camera that can be set on Canvas.
A Camera2D is created by the Camera2D(table) method. The following keys may be set in table:
size- a table {w, h} to set the number of game units along each axiscenter- a table {x, y} to set the center coordinate of the windowfixed- a booleantrueif both axes are locked,falseif vertical axis follows aspect ratio
There are no methods defined on Camera2D, but Canvas has methods to adjust the camera position.
Actor is a game object that represents an entity shown on a Canvas. The Actor can be decorated with components that affect the appearance and behavior of the object.
An Actor is created by the Actor(table) method. The following keys may be set in table:
graphics- a Graphics component to affect appearancecollider- a Collider component to affect collision detectionpathing- a Pathing component to affect path findingphysics- a table of physics propertiesmass- a number with the mass of the objectcor- a number with the coefficient of restitution (1 for perfectly elastic)velocity- a table {x, y} with the initial linear velocityacceleration- a table {x, y} with the initial linear acceleration
transform- a table of transform propertiesposition- a table {x, y} with the initial positionscale- a table {x, y} with the initial scale
layer- a number such that a higher value draws this on top of objects with smaller values
The following methods are defined on an instance of Actor:
getCanvas()- gets the Canvas to which this belongs, ornilgetGraphics()- gets the Graphics component, or nilsetGraphics(graphics)- sets the Graphics component tographicsgetCollider()- gets the Collider component, or nilsetCollider(collider)- sets the Collider component tocollidergetPathing()- gets the Pathing component, or nilsetPathing(pathing)- sets the Pathing component topathinggetPosition()- returns position asx,ysetPosition(x, y)- sets the position tox,ysetScale(x, y)- sets the scale tox,ytestCollision(dx, dy)- returns an Actor, or nil, which would collide if this is moved bydx,dysetVelocity(x, y)- sets the velocity tox,ygetVelocity()- returns velocity asx,yaddAcceleration(x, y)- addsx,yto the acceleration
The following methods may be overloaded on an instance of Actor:
onUpdate(delta)- called when Actor updates, withdeltaseconds elapsed since last frameonClick(down, x, y)- called when receiving mouse clicks, with booleandownand positionx,yonCollide(actor)- called on collision with Actoractor
SpriteGraphics is a type of Graphics component for Actor. This specialization draws a 2D sprite on the Canvas.
A SpriteGraphics is created by the SpriteGraphics(table) method. The following keys may be set in table:
visible- a booleantrueif the sprite will be drawncolor- a table {r, g, b} color with which to modulate the spritesprite- a string filename from which to load the sprite
The following methods are defined on an instance of SpriteGraphics:
isVisible()- returns boolean visible, same as property abovesetVisible(visible)-visibleis same as the property of the same name abovesetColor(color)-coloris same as the property of the same name above
AabbCollider is a type of Collider component for Actor. This specialization adds a 2D bounding box collider to the physics system.
An AabbCollider is created by the AabbCollider(table) method. The following keys may be set in table:
group- an integer index of the collision group to which this belongsmask- an integer bitfield mask of collision groups with which this can collidecollidable- a boolean flag to enable collisions for this Actor
The following methods are defined on an instance of AabbCollider:
setCollidable(collidable)-collidableis same as the property of the same name above
TileMap is an interface for building tiled maps from a tile atlas. This object is used by the Tiled- components for Actor.
A TileMap is created by the TileMap(table) method. The following keys may be set in table:
tileset- a TileSet containing the tile atlasmask- a TileMask containing a greyscale overlay for the tilessize- a table {w, h} for the size in tiles of the mapdata- a table of integer atlas indices in row-major order
The following methods are defined on an instance of TileMap:
setTileSet(tileset)- TileSettilesetsame as the property abovesetTileMask(mask)- TileMaskmasksame as the property abovegetSize()- returns sizew,hof the mapsetSize(w, h)- resizes the map towxhtilessetTiles(x, y, w, h, val)- fills rectx, y, w, hwith integer indexvalgetTile(x, y)- returns the atlas index of the tile at offsetx,ymoveTiles(x, y, w, h, dx, dy)- shifts tiles from rectx,y,w,hby deltadx,dycastShadows(mask, x, y, r, mode)- into TileMaskmask, cast shadows from offsetx,ywith radiusr
TileSet is an interface for a tile atlas, used by the TileMap class.
A TileSet is created by the TileSet(table) method. The following keys may be set in table:
filename- a string filename from which to load the spritesize- a table {w,h} with the size of the tile atlas in tilesdata- a table of integer bitfield flags per tile in row-major order1- tile blocks movement2- tile blocks vision
The following methods are defined on an instance of TileSet:
getSize()- returns sizew,hof the atlas in tiles
TileMask is an interface for a tile brightness mask, used with the TileMap class.
A TileMask is created by the TileMask(table) method. The following keys may be set in table:
size- a table {w,h} with the size of the tile mask in tilesdata- a table of integer tile masks from0(black) to255(white)
The following methods are defined on an instance of TileMask:
getMask(x, y)- returns tile mask at offsetx,yfillMask(value)- fills the mask with integervaluefillCircle(x, y, r, in, out)- fills with integerininside circlex,y,r, else withoutclampMask(low, high)- clamps the mask values betweenlowandhighblendMax(mask)- set each tile to the maximum between this andmaskblendMin(mask)- set each tile to the minimum between this andmask
TiledGraphics is a type of Graphics component for Actor. This specialization draws a tiled map on the Canvas.
A TiledGraphics is created by the TiledGraphics(table) method. The following keys may be set in table:
visible- a booleantrueif the map will be drawncolor- a table {r, g, b} color with which to modulate the maptilemap- a TileMap with which to draw the map
The following methods are defined on an instance of TiledGraphics:
isVisible()- returns boolean visible, same as property abovesetVisible(visible)-visibleis same as the property of the same name abovesetColor(color)-coloris same as the property of the same name abovegetTileMap()- returns the current TileMap, same as the property abovesetTileMap(tilemap)-tilemapis same as the property of the same name above
TiledCollider is a type of Collider component for Actor. This specialization adds a grid-based collider to the physics system.
A TiledCollider is created by the TiledCollider(table) method. The following keys may be set in table:
group- an integer index of the collision group to which this belongsmask- an integer bitfield mask of collision groups with which this can collidecollidable- a boolean flag to enable collisions for this Actortilemap- a TileMap with which to determine per-tile collisions
The following methods are defined on an instance of TiledCollider:
setCollidable(collidable)-collidableis same as the property of the same name abovegetTileMap()- returns the current TileMap, same as the property abovesetTileMap(tilemap)-tilemapis same as the property of the same name above
TiledPathing is a type of Pathing component for Actor. This component adds an interface for navigating a path within a TileMap.
A TiledPathing is created by the TiledPathing(table) method. The following keys may be set in table:
tilemap- a TileMap with which to determine per-tile occupancy
The following methods are defined on an instance of TiledPathing:
findPath(x1, y1, x2, y2)- finds path fromx1,y1tox2,y2and returns next stepx,yclearPath()- used for debugging (may be compiled with an overlay that draws the last path)getTileMap()- returns the current TileMap, same as the property abovesetTileMap(tilemap)-tilemapis same as the property of the same name above
Keyboard controls can be mapped to functions by calling registerControl(). The currently defined control names are 'up', 'left', 'down', 'right', 'w', 'a', 's', 'd', 'action', and 'quit'. The cardinal directions and 'action' (A button) can be triggered by gamepads as well.
Mouse clicks can be received on an Actor by overloading the onClick() method. Canvas objects can also receive mouse clicks by overriding onClickPre() and onClickPost() (which fire before and after mouse clicks are handled by children). If any of these callbacks return true, the event will be captured and not propagated further.
The global function saveState() serializes the entire game state to a new Lua script, including active Canvas and Actor objects and their children, as well as all Lua variables referenced globally or by game objects. Function variables will be saved as compiled bytecode. The output is currently written to stdout but can be redirected to file on the command line.
The sample script test.lua is a self-reproducing script that demonstrates how different kinds of objects are serialized.
New object types can be added to the scripting system by inheriting from the TUserdata<> template. This includes interface types, like IGraphics, from which graphics components like SpriteGraphics are extended; see IGraphics.hpp and SpriteGraphics.hpp for how to use the template. Concrete types also need to be registered with the scene by calling the static method initMetatable(); see Scene::load() in Scene.cpp for examples.
Types extending TUserdata<> can override the following methods:
construct(lua_State*)- handle object constructor parametersclone(lua_State*, T*)- copy state into a cloned objectdestroy(lua_State*)- release resources that must be accessed through Lua stateserialize(lua_State*, Serializer*, ObjectRef*)- store state in the serializer
New Actor components can be created by extending IGraphics, ICollider, or IPathing. New component types can be created as well by handling more interface types in Actor. The header files for all of the types mentioned in the scripting section should have plenty examples to work from.
Trevor Smith - LinkedIn





