My daughter really liked Code Combat Game Grove, but got frustrated with how locked-down and poorly documented it is, wanting to do things that it didn't allow like change the stats of enemies produced by generators.
So, I decided to write something inspired by it. Similar capabilities, but it runs locally, written in Lua and using the TIC-80 fantasy console.
It's also an exercise for me in writing an entity-component-system engine; the game world is populated by entities, each of which is made up of a bunch of different components that give it its appearance and behaviours, and each frame a bunch of systems run to process user input and update the state of the game.
This section gives quick pointers on how to accomplish certain common tasks. For a detailed discussion on how each part of the game works, see the Design section further below.
Open the terminal and run make run to start it up in TIC-80. You can also use
make load to load the game in TIC-80 without starting it yet.
Open the terminal and run make cart. It will produce two cartridge files in
bin/, one in TIC format and one in PNG format.
The starting world is defined in init.lua. This controls the world size,
terrain, and which entities (monsters, treasure, etc) it contains. It is also
where the win/loss conditions are defined.
Individual entities placed in the world can be customized using placeCustom.
Entities are based on templates in the entities directory. Find the template
you want to change and
Create a new file in entities with the name of your new class, and fill it
with the entity template (possibly copying an existing template to use as a
starting point). Then you can create it with world:place() and
world:placeCustom().
Open the terminal and run make load to load the game into TIC-80. You can then
use the builtin sprite editor (F2). When you're done, run this command (in
TIC-80) to save:
export tiles sapling/resources/tiles.png
export sprites sapling/resources/sprites.png
You can also just edit those files directly without involving TIC-80 at all, if you'd prefer to use an external editor.
If you're adding new sprites, or moving sprites around on the sprite sheet, you
also need to edit resources/sprites.lua to teach it about the new sprite IDs
before you can use them.
This section is just a quick tour of all the different directories and files that make up the project.
bin/- generated cartridge files.components/- definitions for individual components like "can be drawn on screen" or "moves around". An entity is made by sticking components together.components/init.lua- support code not specific to any one component.components/base_component.lua- skeleton and documentation for how components are structured.components/trivial_components.lua- components that don't actually do anything; having them just changes how some other part of the game treats the entity in question.
entities/- templates for predefined entities, collecting a bunch of different components and their settings together, so you can just say "create a Watcher" and the game will use the template.entities/init.lua- support code not specific to any one template.
game/- core code for things like how the game world keeps track of what entities exist and what order the systems execute in.lib/- general supporting code that provides useful functions used throughout the project.resources/- artwork (and maybe someday sounds and music as well).resources/sprites.lua- information about the contents of the sprite sheet, so that other code can refer to sprites by name rather than by numeric ID.
systems/- definitions for the systems that actually make up the behaviour of the game: reading input, making decisions about what to change, enacting those changes, and displaying output.dataloop.lua- TODO: merge withmain.luamain.lua- the first file loaded; this defines the functions that TIC-80 calls to load and run the game, and is also responsible for making sure all the other needed code is loaded.setup.lua- this is run when the game first starts and is responsible for creating the world and placing entities in it.
Sapling uses an entity-component-system design:
- the world is populated by entities
- each entity consists of a unique ID and a collection of components
- component behaviour can be customized per-entity (e.g. movement speed)
- a component adds a behaviour or capability for every entity that uses it
- e.g.
drawis a component for entities that display on screen,defencefor entities that can take damage - the specifics of the behaviour can be customized by each entity using the component, via component settings
- components can contain both settings and code
- e.g.
- every frame, the game runs systems to update the state of the game
- systems are the high-level code that glues everything together
- e.g. the
rendersystem looks at every entity with adrawcomponent and displays it on screen
There is also some game behaviour that lives outside the ECS ecosystem; I've tried to minimize this, and it's mostly related to game startup and terrain.
On boot the game loads main.lua. This is responsible for loading all of the
supporting libraries -- many of which either define globals (like the palette
and sprite tables) or modify existing ones (like the new math functions, which
merge with the builtin math library) -- and then defining BOOT and TIC, the
functions TIC-80 will run on game startup and every frame respectively.
Everything in the game except the underlying terrain is an Entity. An Entity is just a table containing a bit of metadata and the state for all the components that make up that entity.
There are two metadata fields:
__idis the numeric ID of the entity. Each entity has a unique ID.__typeis the name of the template the entity was created from. This is used only for debugging.
Every other field in the entity corresponds to the state of the corresponding
component (see below), e.g. the position field holds the state of the position
component. The components are what actually store useful data and enable
interesting behaviours.
The entities/ directory contains entity templates, named entity
configurations which can be used as-is or further customized when placed into
the world. This allows you to create (e.g.) Watchers without needing to
re-specify every component that makes up a Watcher each time.
There is not currently any way to create a template based on another one other than copy-pasting it.
When creating an entity from a template, it starts with the default configuration for the components that make it up, then merges in the contents of the template to produce the final configuration.
Functions that return entities (including create and place) actually return
an EntityRef. This is a convenient handle for the entity. In general you can
use the EntityRef just like the underlying entity. In addition, if the
components that make up the entity define any helper functions, you can call
those though the Ref; for example, instead of this:
local x,y = components.position.getPosition(goblin, goblin.position)
You can write this:
local x,y = goblin:getPosition()
Refs also incorporate some additional safety checks and give more meaningful error messages when misused. You should not generally need to worry about the underlying entity unless you are working on the engine core.
Each entity is made up of components. Each component stores some state inside each
entity that uses it; for example, the position component stores information about
the entity's location in the world.
Components also have data which is not specific to any one entity, such as the default value of the component's state when a new entity is created, or helper functions that make operating on that state easier.
The state for a component is usually a table, but it can be any simple data.
Some components store no state at all, like player_controlled, but their
presence in an entity changes how it is processed by the game logic.
The components/ directory contains component definitions. A
component definition is a table containing __default and __deps fields and
an __init function; it may also contain helper functions which become
available as methods on entities using that component at runtime. Most of the
files in that directory define and return a single component.
Systems are where the actual game logic lives. Each system is responsible for looking at the entities in the world and doing something with them, such as changing their state or displaying them to the user.
Systems will generally only consider entities with certain components. For
example, the movement system will only consider entities with the movement
and intent components (the former describing how it moves, and the latter
where it wants to move to); the render system will only consider entities with
the draw component.
Each system file defines and returns a function that operates on the game world (and changes it in doing so). They may also have internal helper functions.
The process of actually calling the systems in the correct order is handled in
game/systems.lua.