diff options
Diffstat (limited to 'posts')
-rw-r--r-- | posts/2022-10-25-catbird.md | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/posts/2022-10-25-catbird.md b/posts/2022-10-25-catbird.md new file mode 100644 index 0000000..28afdbf --- /dev/null +++ b/posts/2022-10-25-catbird.md @@ -0,0 +1,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/> |