It’s been a few months since the last posting for the snake game, Slither, that I wrote originally as a school project and thereafter open sourced the project.
I’ve taken the time to finally document the building blocks that drive the game. Since the game’s development, several awesome HTML5-esque games have exploded onto the scene, each with its own share of unique abilities. As such, many improvements could be made to Slither at this point, but for now, I will merely focus on its current design.
The game is composed of several components, which can be grouped under two main ones: core and driver. We’ll first dive into the core portion of the code and then reference it when talking about the driver.
The core consists of the code that represents the game framework and facilitates a basic API for the components to employ to drive the game. At a basic level, it defines all the useful data types, including game frame rate, auto-loading script paths, headings, upgrade/poison item types, wall types, and utility functions that are frequently used.
The rest of the core can be explained through its set of components: Objects, ObjectManager, EventManager, LevelManager, and Game.
The Objects component provides a basic base class called Base that handles the basic needs of an object, including drawing/erasing blocks, registering for collisions, adjusting for wrapping around game arena edges, heading and speed adjustments, etc. With a simple extension from this base class, both the food and poison items are provided with custom logic for drawing the body of the objects. The walls and snake objects also extend from this class, but use more custom logic to employ their abilities. The key takeaway is that any objects could be added to the game by simply extending the Objects.Base class and implementing some custom logic for the draw method. If simply a colored square is needed, then the Base class could be used with merely a change to a color attribute. With the flexibility of the EventManager component (mentioned below), these objects could register for more game information and employ predictive artificial intelligence to make more challenging level maps. Another idea is to add more snake objects and allow the user to select his/her snake for play (or have upgrades to unlock them). Simply changing the PLAYER_CLASS constant upon game load will adjust the snake used (could be swapped with a callback to set this property and reload on-the-fly).
The ObjectManager component handles everything about objects within the game and is interwoven into the game timer mechanism. This allows for dispatching of object rendering according to the game timer and the provisioning and removal of objects from the game arena. The setup makes it fairly trivial to allow individual objects to control their rendering based on a general timing mechanism that the ObjectManager employs.
The EventManager component handles all event dispatching within the game. This allows for easy intercomponent communication as well as individual game object communication and event delegation. The setup allows for less complicated interfaces to data passing, then would possibly be needed elsewhere. Each component has global access to the Core.EventManager component, so they can trigger events which will be dispatched to any of the listeners that are registered for that particular event at the time of triggering. EventManager handles the rest of the process along the way.
The LevelManager component handles all level advancing within the game. This allows for an easy setup of levels as merely a list of callback methods that are sequential in nature and triggered arbitrarily by a call to advance. If the game were to be extended, this component also supports swapping and inserting of level callbacks on-the-fly, should it be found useful.
The Game component handles the majority of the logic that drives the game and handles utilitizing the other components to support levels, per-level object variations and difficulty items such as poison items. Since this is undoubtedly the most complicated component in the core, we’ll go into a further detail regarding how it functions to make the game play as it is today.
When the game starts, the component sets up all the event handlers within the EventManager, sets the game arena boundaries, and provides all the level generation details within the LevelManager. Finally, it initializes the main player object as defined by the PLAYER_CLASS constant (e.g. Slither.Objects.Snake). If successful, it instructs the LevelManager to run the first level’s logic.
During the game play, when the snake player object intersects any other game objects, the Game removes the object via the ObjectManager and a ‘collide’ event is fired, which is registered with each object. If the object is something that is edible (food), then the object fires an ‘eat’ event for which the game listens. Likewise, if the object is an upgrade item, then the object fires an ‘upgrade’ event. Poison item objects instead fire a ‘poisoned’ event and wall objects fire a ‘die’ event. Additionally, if the snake runs into itself, the snake will fire a ‘die’ event. Depending on the secondary event that the objects fire upon collision, the Game component will perform a specific set of actions:
- Eat – Game checks the number of objects left in the game arena based on their type. If all food objects are eaten, then the LevelManager will be triggered to advance the game to the next level. The Game component then fires event handlers for score keeping and level advancement so the driver is informed to update the display area. In the case that the current level is the last one for the game, a ‘gameover’ event will be triggered, which the driver will in turn handle since the game will no longer proceed.
- Die – Game checks if the snake has run out of lives. If so, then the ‘gameover’ event will be triggered, which the driver will in turn handle since the game will no longer proceed. Otherwise, the Game component checks if the sender of the event was a wall object. If the player has a wall breaker upgrade applied, then removes the upgrade and disregards the event. Otherwise, all upgrades are removed, the snake is reset to the original size, and the game is paused until the user restarts the current level again.
- Upgrade – Game applies the upgrade to the snake, offering a number of benefits, such as an extra life, wall breaking ability, poison prevention, frozen rats, etc.
- Poisoned – Game poisons the snake by either reducing the game score or killing the snake. All poison events cause upgrades to be removed.
The other main component is the Driver:
The Driver component is the set of code that drives the user interface for the game, including handling the initialization of the Game instance and setting up some custom callbacks that receive notification of game events. The main idea of splitting out the game core and the driver is to separate the concerns of the game – one of the game logic and the other, the surrounding presentation. The Driver handles the inputs (keyboard/mouse) and the presentation of the area around the game area (interaction with the HTML/CSS) by using jQuery to perform effects and the score area’s display. With this setup, really we could swap out the entire presentation logic with something else that is aware of the Game instance and has a canvas element that can be written to (with adjustments for boundaries). One would just need to be aware of the core game objects rendering, so their code could be updated accordingly to match the needs for presentation.
Overall, the game can be optimized slightly from this setup to perhaps combine some of the components into a unified model; however, for the sake of the class project it made more sense to isolate them to cite for the class project. However, given the structure it allows for a wide variety of changes to the game without a complete rewrite to the structure. That was among some of the original design decisions I had for the game. Hopefully, with any luck I can implement some new ideas to refresh the game for Slither 2.0.
If you would like to check out the game again, check the original post for Slither.