SokoMaker Core Concepts
I wanted to take some time to explain some basic concepts about how SokoMaker works, and my design philosophy around it.
This is out of date! SokoMaker’s APIs change frequently (especially at time of writing, pre-early access). Take away the conceptual ideas and concepts instead of the exact keywords and syntax.
Traits and onMove
At it’s core, SokoMaker is a tool for making Sokoban games. Basically everything else is “fluff” around that very simple core.
As such, in your mod.json
at the root of your game, you can define an enumeration of Traits
. You, the content creator can decide what the traits are and what they mean. You might have a Phase
trait with values like Solid
Liquid
and Gas
. And/or you might have a Material
trait with Metallic
Plastic
and Wooden
.
Every entity and tile in your game is then defined by what its traits are. For instance, a “Crate” might be a Heavy
Wood
Solid
Buoyant
Brittle
entity.
What do these values mean? Whatever you want them to mean.
In your main.lua
you can define a function called onMove
that will be automatically called every time an entity attempts to move. Your implementation of onMove
can compare the Traits of the moving object to the Traits at the desired landing site.
Config Variables, Instance Variables, and State
Traits only get you so far. Sometimes you want to store actual variables inside an object instance.
Every object in SokoMaker holds a table in it called state
. The state
holds all of the ephemeral information about the object that isn’t native to that object type. This is where instructions for rendering the object are dispatched, and it’s also where unique flags and behaviors might be defined.
state
is a concept that only exists in lua at runtime. It’s obtained by first gathering up all the Config Variables, then appending (or overwriting) them with Instance Variables, and then finally converting them into the state
table realized in game.
That probably didn’t make much sense, so let’s break that down.
Config (Config Variables)
Every type of object has a configuration file defined in Json. I call these “Configs.” A Config for a “crate” might look like this:
// entities/crate.json
"config_variables": [
{
"data": {
"key": "renderer",
"value": "SingleFrame"
}
},
{
"data": {
"key": "sheet",
"value": "entities"
}
},
{
"data": {
"key": "behavior",
"value": "pushable_block"
}
},
]
(Ideally you wouldn’t be editing the json directly, but that’s how it works today)
Editor Instances (Instance Variables)
When you instantiate an instance of the object, you can select it to read its Instance Variables.
Instance Variables are a list of keys and values that will be appended to the Config Variables. If a key is reused, we will use the “latest” one.
In the above screenshot. We set the objects sheet
to entities
on the Config layer, and then (redundantly) overwrite the value as entities
. However, if we changed the value to tiles
, the final table would have sheet
set to tiles
, which would change the behavior for this particular instance of crate
.
Gameplay Instances (State)
Once the object is realized at runtime, the written and overwritten Instance Variables key value pairs are converted into a lua table (or at least, a structure that behaves a lot like a lua table).
-- assuming `entity` is our entity
print(entity.state["renderer"])
-- also valid
print(entity.state.renderer)
-- either of above logs `SingleFrame`
Up until this point, we’re limited as to what types we can load into the Instance/Config Variables. Basically we only have primitives to work with (strings, booleans, numbers). However, once we have our fully realized state
object. We can load whatever we want into it!
We’re also free to overwrite values and do all sorts of crazy things!
-- use the magic "lua" renderer
entity.state["renderer"] = "lua"
-- provide a custom render function
entity.state["render_function"] = function(painter, drawArguments)
-- custom draw code goes here
end
Sokobjects Types
There are 3 types of “objects” in SokoMaker (collectively called Sokobjects
). Tiles, Entities, and Props.
Tiles
For every grid position there is exactly 1 tile. If the level does not specify there should be a tile at a position, SokoMaker will act is though the “default” tile is there. You specify the default tile in your mod.json
, which lives at the root of your project.
Tiles all share the same state
table. This means if you change a state
table in one tile. You’ll affect the state
of all instances of that tile’s template in the game session.
Entities
An entity has a grid position, and multiple entities are free to occupy the same grid position. Each entity also has a unique state table.
In editor, you can select any entity and modify it’s Instance Variables which will be realized as a different state
. When the object is created.
Props
Props are not on the grid. They’re mainly used for non-grid-aligned decorations. Similar to tiles, they also have a shared state with anything that shares the same Config. However, you can spawn a Prop at runtime via a lua script. These spawned props do not share a state with anything else.
For example, Ernesto’s Lasso spawns 2 props, one to represent “the rope” and the other to represent the loop at the end of the rope.
Magic Keys
There are certain keys you can set in an object’s State
that will magically affect behavior. The generated documentation that comes packaged with SokoMaker explains this.
For example, if you have a key called behavior
with a value crate
. SokoMaker will look up the crate.lua
module and gives that object that behavior.
I think the most interesting ones to talk about are renderer
and sheet
.
As a mentioned before. Variables are just a set of key/value pairs that get converted in a lua table. But some of those keys get read by internal systems and result in things showing up on screen.
For example, if you have a key called renderer
and a value called SingleFrame
, and a key called sheet
and a value called entities
then the following will happen:
- When we try to draw this entity, we’ll look at the sprite sheet
entities.png
(if it exists) - If there is an additional key
frame
defined, we’ll read that key to determine which frame to draw in the sprite sheet. Ifframe
is not found, we’ll draw the first frame.
Similar to magic keys, there are also magic “correct” values for renderer
such as:
-
SingleFrame
: Draws theframe
that is in the sprite sheet represented insheet
-
LoopFrames
: Draws thepose
that is represented insheet
.- The value of
pose
is a key that is used to reach back into the variables list. If yourpose
isidle
we look for a key calledidle
that is a comma separated list in square brackets:[3,6,7]
- We’ll loop animation frames, in the above example we’ll show frame
3
then6
, then7
. - Caveat: This implementation of “lists” in Instance Variables is clunky. I’d like to do better but this is what I have right now.
- The value of
-
SmartTile
: Used for things like water tiles. I don’t want to belabor this blog post with the full details of how it works. Fundamentally, it takes a sprite sheet and some frames to figure out what sections of the sprite sheet to draw for each scenario.
You can also implement your own custom renderers if you’re not satisfied with the built-in ones.
-
lua:<script_name>
: Provided there’s arenderers/<script_name>.lua
this will run the render function defined in that script.
Or, if your desired draw behavior is truly dynamic, you can implement your entire renderer inside a lua closure.
-
lua
: Only makes sense if you have arender_function
key defined with a lambda for your custom draw code.