For a while I’ve been meaning to talk a bit about the engine design I’m using in Fatal Attractions. The astute reader may recall me mentioning previously that the game is built upon the LÖVE framework. One of the reasons I went with this lesser known framework instead of a full-fledged and commonly used engine such as Unity is that it offers me as the developer a lot of freedom; I can pretty much design the game exactly the way I want it. I’m not forced to follow anyone’s conventions or opinions — ‘cause that’s how I roll.
I’ve opted to go with an entity-component-system-based approach. Everything you see in the game (as well as a lot of stuff you don’t see) is considered an “entity”.
Of course an entity by itself is meaningless and devoid of function; it’s just an empty entry in a Lua table. Only by adding components we can fill entities with life. And depending on which combination of components we apply, an entity can become something entirely different. For example, you take a simple sprite, add a “follow mouse” component and voilà: You’ve created a mouse cursor!
Once you’ve come up with a reasonably sized library of components, you can mix and match them to combine all sorts of functionalities for your game. In Fatal Attractions a Player object gets initialized with the following set of components:
Player: -> with Entity.new! \add Components.Actor \add Components.Animation \add Components.Color \add Components.Direction \add Components.Pathfinder \add Components.PersistentStorage \add Components.Pivot \add Components.Player \add Components.Render \add Components.Shader \add Components.Sprite \add Components.Transform
Now you must know that components are just containers. They’re pure data and do not provide any functionality of their own. An obvious example of this is the ‘Transform’ component I’m using above. Internally it looks like this:
Transform: x: 0.0 y: 0.0 z: 0.0 rotation: 0.0 scale: 1.0
Now it would pretty neat if we could get these bundles of data to do something actually useful. That’s where the Systems come in. Each System defines a combination of components it can operate on. Here’s the header of my Sprite system:
class Sprite extends System requires: Color: true Pivot: true Sprite: true Transform: true
An entity may have many more components. But as long as it has a Color, Pivot, Sprite and Transform component, it will be processed by the above-defined Sprite system. And as you’ve probably guessed, the sprite system is in charge of drawing sprites to the screen. Each system reads the data from its required components and does something with them. In this case, the Sprite system reads a texture from the Sprite component and draws it at the coordinates stored in the Transform component.
The drawing happens once every frame. But not every system can draw things. Some systems just want to do something every second frame. Or after a new stage was loaded. Or perhaps only when the mouse hovers over an object. Or when the player tried to talk to another actor…
These various cases are handled by an event queue. In fact the entire engine is based around this concept: Whenever something happens in the game, an event is fired. Any and all subscribers are then informed that this event has taken place. Depending on the event they may be provided some extra details — e.g. which character just moved, which stage was loaded or which object was picked up by the player.
Subscribing to events is super-easy inside a system. Simply define a method with the name of the event:
onMouseUp: (x, y, button) => -- do something onStageLoaded: (stage) => -- do something onExitStage: (stage) => -- do something
The same idea also works for the game’s actors in the story description file. For example, say we have a dog on the screen and the player is supposed to be able to interact with said canine beast. Then in the story script I might add something like this:
Dog.onTalkTo: => Player\say "Who’s a good boy?" Dog\say "Arf!" Player\say "That's right, you're a good boy!" await Dog\animate "Excited" Cat\say "Meh." Bone.onPickUp: => Dog\say "GRRRRR!" Dog.onGive: (item) => if item.name == "Snack" Dog\say "Arf! Arf" wait "1s" Dog\goto Bed else Dog\say "GRRRR!"
Of course there are way more commands available to pick from in the actual story engine. But this should give you an idea of how complex interactions can be stringed together by using only simple commands.
The beauty of it is that the story script reads less like your typical programming language and almost like a movie script. This simplicity makes it really easy to prototype new ideas or just try something out to see if it works without having complex syntax get in the way.
You may be wondering what strange programming language I’ve shown above. All code examples are in a language called MoonScript. This is a dynamic scripting language that compiles into Lua. I don’t think I’ve ever come across a more concise yet easy to use language. It’s an absolute joy to develop games in.