/sapling

A simple action game created as a learning exercise

Primary LanguageLuaMIT LicenseMIT

Sapling of Goblins

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.

I want to...

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.

...play the game.

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.

...generate a cartridge to share with people.

Open the terminal and run make cart. It will produce two cartridge files in bin/, one in TIC format and one in PNG format.

...change the starting world.

...edit a specific entity in the world.

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.

...edit an entire class of entities (e.g. all Watchers).

Entities are based on templates in the entities directory. Find the template you want to change and

...create a new class of entities.

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().

...edit the graphics.

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.

File Index

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 with main.lua
  • main.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.

Detailed Design

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. draw is a component for entities that display on screen, defence for 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
  • 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 render system looks at every entity with a draw component 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.

Startup

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.

Entities

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:

  • __id is the numeric ID of the entity. Each entity has a unique ID.
  • __type is 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.

Entity Templates

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.

Entity References

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.

Components

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

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.