Ah, summer. Our weekends are packed. Last weekend was a Saturday mountain climbing adventure + a Sunday Father's Day celebration. This weekend is a massively compressed trip to Alaska to tour Denali National Park and go whitewater rafting. Next weekend is a 4-day trip to Indiana. And so forth.
The Unity project has not been forgotten. On the contrary almost every free moment during my weekday evenings has been going into designing and building the class infrastructure which will either make or break the project.
I've been spending a lot of time on how to ensure a clear separation of concerns. Here's what I have so far.
The Gameboard class is the stage on which the action takes place. The Gameboard object gets instructions on how to set itself up initially from some type of Level object. So far, it only reads CC1Level objects, which are parsed from DAT files. In the future I plan for it to be able to set itself up from CC2Level classes which will be parsed from C2M files, and eventually a 3rd level format which we have yet to define.
Gameboard objects include the time and chips counters, global states for toggles and tanks, the current replay sequence, etc. They also contain a 2D array of MapCell objects which are defined later in this post.
The most important rule here is that "A Gameboard is not a Level". This means:
- You don't save Gameboard state (even for checkpoints I'm hoping to load the level up from starting position and then use a replay to get back to current state).
- You don't modify Gameboard objects from any level editor. You modify Level objects, which are instructions for how to set up a Gameboard.
- You don't reuse Gameboard objects. It would be error-prone to try to reset each stateful field. Instead, actions like level restart or checkpoint reload should always set a new Gameboard up from scratch.
Classes that implement the Engine interface contain a set of rules for how to modify/mutate the Gameboard object. The Engine interface defines a single method:
void call(Inputs inputs);
It is expected that normal gameplay will call this method at 60 times per second (to accommodate CC2 electricity at a later date), with the Engine incrementing a tick variable internally and filtering out ticks as needed to achieve movement ticks at the desired rate.
IMPORTANT: The Engine and everything below it (Gameboard, MapCell, Element, Level, etc) are intended to be written in pure C#, without any reference to Unity-based classes.
In theory this means that the C# code is fully decoupled from the Unity platform, which could allow a couple of advanced concepts:
- Running a server that handles levels/levelsets/ratings/high scores/replays, which could potentially validate replays by running the game engine server-side.
- Running a server that handles the gameplay serverside, and sends graphics updates to a browser-based implementation. (this seems far-fetched, but it's a cool idea).
The MapCell contains all the elements that are at a given x-y coordinate on the Gameboard. As described on the Discord server, the MapCell is a hashmap. The keys are the Layer enum (Terrain, TerrainPlus, Static, Pickup, Creature), and the values are the object at the given layer. This allows for two important things:
- A single object can occupy more than one layer (e.g. the green toggle bomb, which needs to occupy the Pickup layer as a green chip, but the Static layer as a green bomb.)
- Adding new layers (thin walls, canopies, electricity) requires minimal code changes.
The MapCell is smart enough to deduplicate such items when returning its contents for processing Enter/Exit rules.
All Elements inherit from the Elem class. The Elem class is abstract and contains a lot of useful default code, mostly related to 3 concepts:
- CalcMove actions. These are typically the standard testEnter, testExit, startEnter, and startExit methods that determine whether an ICreature object can enter or exit the given element.
- PostMove actions. These are typically the standard finishEnter and finishExit methods that determine what happens on move completion. For MS-like engines, the PostMove actions will occur immediately after the CalcMove actions, while for Lynx-like engines, they may occur 1 or more movement ticks later.
- Occupies rules. These are the layers in the MapCell (almost always just one) that an element occupies.
All moveable elements currently implement the ICreature interface (name subject to change). I'm still figuring out how this should work with the Engine for processing movement logic.
This class has read-only access to the Gameboard. (Honestly, my code isn't clean enough yet to ensure it doesn't have write access also, but that must never, ever happen! I think the answer is to use the internal keyword for methods that mutate the Gameboard, such that the Engine has access but the GraphicsManager does not.)
The graphics manager reads in the contents of the Gameboard (within the viewing window) and builds/reuses and positions Unity GameObject classes. These classes contain the bitmaps and animations needed to view the game window.
Reads user input and supplies it to the Engine at 60Hz.
Handles the layout of the screen including text boxes, inventory, positioning of the game window, menus, and navigation.
My immediate goal is to get CCLP1 levels working with just 5 elements (Floor, Wall, Player, Chip, Exit). All other elements will be processed as a NotImplementedElement and behave as acting floors or something.