Scrinks!
Scrinks?
Scrinks is a (toy, very wip) game engine written in C++ and Lua. It’s not meant to be anything more than a testbed for some of my crazier ideas, as well as a project to help myself learn more lower-level OpenGL, and maybe Vulkan in the future.
I’m not very good at coming up with names, sorry! :)
A quick tour of a new and basic project
Note that all of this is subject to change, so think of this as a snapshot of how to setup a project at the time of writing.
Project files
First you’ll want to setup your project.lua
:
return {
name = "Space Demo",
entry = "scripts/game.lua",
render = "2d",
classes = [
"scripts/bullet.lua",
"scripts/player.lua",
]
}
Well, there’s a few things here to point out:
- This config is entirely lua, so it can execute code!
- We define an entrypoint lua script, which requires an
init
function defined which is called once the setup is complete and the game loop is ready. - The list of classes is hardcoded (but as this is just lua we could find files on disk)
- The engine currently has two pipelines: a WIP 3D deferred renderer
3d
, and a very basic 2D sprite renderer2d
which we’re using here.
Initialisation
From there we need to instantiate some data once our game loop is about to run, from the scripts/game.lua
mentioned above:
function init()
layer = sprite_layer.new()
:sheet("textures/space.png")
:tiles_per_row(16)
:tile_size(vec2.new(64,64))
:instance_count(100000)
:init()
local ship = player.new()
:position(100, 100)
:layer(layer)
end
Here we see the init
function which sets up some basic nodes for our game. Note that while the ship
variable is a local in lua, it’s memory is managed by the node system, as we don’t specify a parent in new
, it defaults to being owned by the root node (which is the node that owns this entry lua script).
A few notable things here:
- This is not very lua-ish, the syntax will likely be updated to pass in a table of parameters instead of the current method chaining approach, but this requires a bit of thinking to get right when including properties, which will be discussed below.
sprite_layer
is a built-in node that provides efficient instanced-based sprite rendering, although currently the only per-instance data is a position and tile index. This will eventually be updated to support scaling, rotation, flipping, & tinting as requested (so that you don’t need to have rotation if nothing is rotated such as in a tile-map).player
is a lua-defined class (fromscripts/player.lua
in the classes array we defined earlier). We can see below how it’s setup to have a base class of theSprite
node.
Defining a Lua class
player
class: scripts/player.lua
:
require "lib.key_map"
require_asset "space.timer"
__base_node = sprite
__props = {
move_speed = 7
}
-- move stuff
local move_target_x = 0
local y_height = 100
-- etc...
This script will be loaded before initialisation, as it was defined in the classes
array in the project. During this pre-processing the file will be interpreted and then certain values will be picked out to help create the player
class. The __base_node
object is a type of built in node that serves as the base-node that is created whenever a player
object is new’d, this is somewhat similar to Godot where scripts can extend built-in classes.
We can also see the __props
table, this will setup property data that is stored on the node (outside of lua). This is important: Any lua variables declared in the script will persist in the lua state for that node, but can’t be accessed outside of that lua state. While it would be possible to access this data from other objects, it creates issues with threading (as lua states are inherently thread unsafe). Property data is therefore stored in the Node and specifically defined here, although you can add properties dynamically if required, this syntax just sets properties with whatever default values you want. So for example move_target_x
won’t be accessible from any other node or script, but move_speed
is accessible both in the script, from self:move_speed
as well as settable/gettable directly from other scripts, for example:
local bullet = bullet.new()
:tile_index(17)
:position(x + 16 + vec.x * 20, y + vec.y * 20)
:layer(self:layer())
bullet.velocity = vec
bullet.hit_player = false
bullet.hit_enemy = true
Here we are setting the properties of velocity
, hit_player
and hit_enemy
, then later these are accessed by the bullet.lua:
__base_node = sprite
__props = {
velocity = vec2.new(0,-1),
move_speed = 30,
hit_player = false,
hit_enemy = false,
}
function fixed_update()
self:translate(self.velocity * self.move_speed)
-- etc...
end
Here we can see the properies being accessed inside fixed_update
using the self
directive. In the previous code block, the move_speed
property is never set which means it will be it’s default value of 30
.
You may have noticed self
throughout these code examples, I’m not sure if it’s the best to overload this value as it has specific meaning in lua (e.g. I could use this
or something else) but it creates a nice consistent syntax.
Require
One thing to touch on briefly, going back to the first class example, is require
and require_asset
:
require "lib.key_map"
require_asset "timer"
Here we see two require methods: One imports Scrinks’ built in key_map
library, the other imports a custom user-defined scripts/timer.lua
file, which is a more typical lua class that exists outside of the Node ecosystem and will be memory-managed by lua (although the lua state’s lifetime is managed by the node that owns the script).
require 'lib.class'
timer = {}
local timer_mt = class(timer)
function timer:new(cooldown)
return setmetatable({ cooldown = cooldown, current = 0, tick_rate = 1.0 }, timer_mt)
end
function timer:tick()
-- note: fixed_delta is a builtin that the engine defines.
self.current = self.current - (fixed_delta * self.tick_rate)
if self.current <= 0 then
self.current = self.cooldown
return true
end
return false
end