summaryrefslogtreecommitdiff
path: root/posts/2022-10-25-catbird.md
blob: 28afdbf2d5d99ec406dd4048014611f4b8897f94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
title: Catbird: An experimental game engine for Scheme programmers
date: 2022-10-25 18:00:00
tags: guile, gamedev, chickadee, catbird
summary: An design overview of an experimental Scheme game engine named Catbird.
---

I've been participating in the Lisp Game Jam for several years now,
and [the next one is starting on
10/28](https://itch.io/jam/lisp-game-jam-2022), and with each attempt
I've been accumulating code that forms something resembling a game
engine.  I'm now attempting to solidify some of the concepts I've been
exploring in order to make a game engine worthy of releasing in case
someone else wanted to use it, too.  I've named this engine Catbird,
continuing the bird theme established by my game programming library,
[Chickadee](/projects/chickadee.html), which the engine is built on.

Game engines are opinionated machines, and no single engine is
suitable for all types of game or developer.  So, the first step for
designing an engine is to determine *what* kind of games to target and
*who* might want to use it.  I have decided to target small-scale 2D
games (though I've left the door open for adding 3D support later)
made by developers who love Emacs, love Scheme, and for whom having a
pleasant, REPL-driven workflow is more important than raw performance.
In other words, I've designed it for *myself*, but I know there's a
small community out there that shares my preferences.  It takes a
billion dollars and a ton of developers to make the Unreal Engine, but
a small, niche engine can be made by a hobbyist in their spare time.

These are my design goals in more detail:

* REPL-driven development model AKA live coding.  This means that
  everything needs to be modifiable at runtime.
* Good enough performance for solo developers and small indie teams
  making games on a small scale.  It's okay to use a language
  implementation with garbage collection!
* Game objects are composable.  Simple objects can be combined to form
  complex ones.
* Game objects can run asynchronous scripts.
* A state machine abstraction breaks down games into small, manageable
  chunks.
* A well-defined user input layer cleanly separates game state
  modifications from the input devices and buttons that trigger them.
* Linux and MacOS as the initial target platforms.  Windows and
  Android would be nice future additions.

I've taken design inspiration from several places:

* [Godot](https://godotengine.org/), a project that has shown that a
  FOSS engine can compete with the likes of Unity.  If I didn't have
  such Lispy tendencies and didn't enjoy implementing engine stuff so
  much, I'd just use Godot.  I like their take on the scene node
  abstraction.
* [Emacs](https://www.gnu.org/software/emacs/), the extensible,
  self-documenting text editor and the greatest developer tool ever
  created.  Modifying Emacs at runtime to suit your needs/preferences
  while you work on your projects was a transcendent experience for me
  when I first tried Emacs just over a decade ago.  Not only do I want
  to develop games within Emacs, I want my engine to share some of
  that Emacs spirit.
* [Xelf](https://gitlab.com/dto/xelf), the Common Lisp game engine
  that combines concepts from Smalltalk and Emacs with great results.
  Common Lisp isn't really my thing, but if it's *your* thing then you
  should really try making a game with it.

In order to implement the design goals, one of the first big decisions
that needed to be made was about the programming paradigm.  Scheme is
often referred to as a functional language, but it is really a
multi-paradigm language.  Different layers of a program can choose the
paradigm that bests suits the domain.  I decided to copy Xelf and use
an object oriented architecture using Guile's OOP system: GOOPS.
GOOPS closely resembles the almighty CLOS: The Common Lisp Object
System.  If you're unfamiliar with CLOS, the most import thing to know
about it is that methods do not belong to classes.  This separation of
class and method unlocks the ability for methods to dispatch on all of
their arguments instead of traditional single dispatch on only the
first argument.  Combine that with support for multiple inheritance
and the metaobject protocol (which I will not go into here, but it
rocks) and you have an OOP system that I actually enjoy using.
Classes and methods can be redefined at runtime and it's a much better
experience than modifying record types (which do not update existing
instances) and procedures (which often have old versions referenced
elsewhere that are not updated without re-evaluating that code, too.)
So, GOOPS provides the flexible foundation for Catbird's REPL-driven
development model.

Here's a simple diagram of the most important classes in the engine:

![Catbird class diagram](/images/catbird/class-diagram.png)

## Nodes and modes

There are two fundamental types in Catbird, and they rhyme: Nodes and
modes.  Nodes encapsulate objects in the game world, such as the
player character.  Modes encapsulate states of interactivity, such as
moving the player character around a map with the arrow keys.  Catbird
nodes are similar to Godot nodes, and modes are similar to Emacs
modes.

Nodes encapsulate the state of an object in the game world.  Nodes can
be rendered, updated, and run asynchronous scripts.  Nodes can have
one parent and zero or more children, forming a tree structure often
called a scene graph, though in this case it is a true tree because
nodes cannot belong to more than one parent.  This structure allows
for composing complex objects from simpler ones.  A player character
node might contain an animated sprite node to handle its various
animations.  The state of a parent node affects child nodes.  If
sprite A is at position (2, 3) and contained within sprite B at
position (3, 5), then sprite A will be rendered at position (5, 8).
If sprite B is paused, then neither sprite A nor B will have their
state updated.  If sprite B is hidden, then neither sprite A nor B
will be rendered.

Modes encapsulate pieces of a game's state machine and serve as the
input handling layer.  There are two types of modes: Major and minor.
Major modes are considered mutually incompatible with each other and
are used to represent different states within a game scene.  For
example, map traversal in an RPG could be represented by a major mode
for moving the player character around the map and another major mode
for interacting with objects/talking to NPCs.  Minor modes implement
smaller, composable pieces of game logic.  For example, text field
editing controls could be contained within a minor mode and composed
with every major mode that has the player type something.  All modes
have an input map, which translates keyboard/mouse/controller input
events to calls to their respective event handlers.

## Scenes

Nodes and modes are fundamental but have no relation.  Nodes do not
contain modes, and modes do not contain nodes.  A new type is required
to link the two together: Scenes.  In Emacs, buffers contain text.  In
Catbird, scenes contain nodes.  Both buffers and scenes have modes.

The scene type is a subclass of node that is used to encapsulate a
coarse chunk of a game's state machine.  For example, an RPG could be
divided into several scenes: world map, inventory, and battle.  Modes
are attached to scenes to form a playable game.  Scenes always have
one active major mode and zero or more minor modes.  Scenes and modes
together form the state machine abstraction, handling coarse and fine
grained states, respectively.

## Cameras and regions

Okay, so scenes have nodes, but how is a scene rendered?  How do you
move around within a scene?  With a camera, of course!  Cameras
provide a view into a scene.  They have a projection, position, and
orientation.  The same scene can be rendered using multiple cameras,
if desired, such as in a split-screen multiplayer game.  This is like
how an Emacs buffer can be viewed from many different scroll positions
at the same time.

Cameras need a place to render, and I wanted to make sure that it
wasn't always assumed that rendering should cover the whole screen, so
I adapted another Emacs concept and renamed it.  Emacs has windows
(which are *not* desktop windows, Emacs calls those frames because it
predates windowing systems!) and Catbird has regions.  Regions
represent a sub-section of the game window, defining a viewport to
which a scene can be rendered.  Regions can be associated with one
scene and one camera.  When both a scene and camera are present, the
scene is rendered to the region's viewport from the perspective of the
camera.  In a typical game, one region that covers the entire window
is all that's needed.  A split-screen multiplayer game, however, could
divide the window into two regions and render the same scene using
different cameras.  The scene associated with a region can be changed
to transition from one scene to another.

## Assets

Assets are containers for data that is loaded from the file system,
such as images or audio files.  They are meant to be defined as
top-level variables in modules and referenced by whichever nodes need
them.  Assets keep track of the file(s) from which the data was
loaded.  Assets are lazy loaded when they are first dereferenced, but
they can also be manually loaded ahead of time.  When developing, the
files associated with assets are watched for changes.  When a change
is detected in an asset file, the asset is automatically reloaded.
Nodes keep references to assets, not the data within, so that the
freshly reloaded data is automatically used by all nodes with a
reference to that asset.  With code and data modifiable at runtime,
there is rarely a reason to stop and restart the game.

## Conclusion

So yeah, this engine design isn't novel by any means.  I'm just
combining some traditional game engine design with concepts from
Emacs, some of which have already been applied in engines like Xelf.
I think the result is quite nice, though, and I don't know of any
other Scheme project that's quite like it.  I will be continuing to
develop Catbird here, if you want to check out the code:
<https://git.dthompson.us/catbird.git/>