We’re headed into the #7DRL Challenge. Last post I described what I was going to make, how I was going to make it, and what I hoped to get out of it. Today I want to describe things that were done terribly in my first game that I hope to do better this time.
I’ve seen a few of these posts popping up. They mostly say things like, “I tried to do too much,” or “I tried to put non-essential things in before the core was done.” I don’t want to get into any of those vague issues. I actually want to get into some hardcore implementation errors I made.
I’m going to use the term “game engine,” so I want to make sure people understand what that means in this context. Roughly speaking a game is a finite state machine simulation that either live updates at a certain rate or updates upon user input. The engine refers to the main pieces that loop to make that machine continue.
To big game developers, there’s probably a lot more parts (like preprocessing the 3D animation pipeline, etc), but for my 7DRL it consists of initializing/loading the pixel art and starting statistics, an update loop, and a draw loop. The way the monogame engine works is that the draw loop will attempt to be called at a fixed fps (I’d guess 60 is default, but have never looked it up).
The update loop is always called at that base fps. User input is captured in a queue and then handled in the update loop. For example, if the player presses the up arrow, the update loop catches this by going through a bunch of if statements. One of those will be “if user presses up: increase the y-position by 1” (actually decrease due to how the screen coordinates go, but let’s not get into that). Then since the draw is independently updated with the most current positions of everything, the avatar will move.
All of this goes on forever unless something causes the big game engine loop to break (e.g. “if player health <= 0: break"). This brings me to my first error. I thought that because my game was turn-based, i.e. nothing on the screen would move or update until the player pressed a key, I didn't have to separate the update logic from the draw logic and I didn’t have to call either until the queue caught new user input.
This is a pretty big error. This meant that my entire engine stalled until the player pressed a key. It didn't cause any issues in the game per say, but it is very bad form. You need at least one moving part, some people would say two, at all times. What if you wanted to implement something that relied on real time and not just game time? What if you wanted to have a resting animation for the sprite? The ways in which this limits the creator are endless, and it is sort of hard to see until you make the mistake yourself and try to expand the game.
Also, keeping the draw and update loop separate is very important when graphics are involved. For example, if you have a lot of processing power needed to render your 3D animations, then you might need to drop frames to prevent lag. If the loops were tied together, then the game engine itself will experience lag and maybe even miss user input. If they are separate, the animation loop is free to lag without the rest of the engine lagging.
My next set of mistakes were object-oriented ones. My hierarchy of objects was poorly designed. Actually, there was no hierarchy. This caused excessive amounts of repeated code. The player had a position, health, attack damage, attack method, takeDamage method, and so on. The enemies did, too. I should have made a superclass with those attributes and methods from which both could inherit.
A related mistake was that I broke encapsulation … a lot. Because I had all these separate objects that needed to know about all the other objects and alter them, I kept passing all these raw values around and changing them. This is very bad, and caused many headaches when something went wrong. A few times I had trouble tracking down how a particular value changed in an incorrect way, because everything had permission to change everything.
I should have made a well-planned, clean hierarchy, and then let the main engine handle making all of the updates and changes that needed to happen.
The last thing I want to talk about is a little subtle. The object-oriented approach at first glance seems well suited for this type of finite state machine. You have a bunch of instances of enemies, each have a bunch of attributes, and the changing of these is essentially the game.
For a small scale project this is fine. What you will find as your game gets more and more diverse is that your hierarchies will get out of control. If you subclass every time there is a split, you will end up with insane code: new Character.Enemy.EnemiesThatFly.FireEnemy.NoMagicSkills….
I know you're thinking that these were just poorly chosen subclasses. It is an art to balance when to subclass. Many of these could be combined, and then leave an attribute null or unused when that particular enemy doesn't use the attribute. But remember, we're talking about a large scale game with lots of diversity.
It is a simple application of the pigeonhole principle that your unused attributes will also get out of control if you don’t subclass enough. This isn't good either. There is simply no way to create a set of well-balanced classes in a pure hierarchy once the attribute size gets large enough.
There is another way! It is called an Entity Component System (ECS). Instead of thinking in terms of objects, think in terms of the components. One component might be health. So you pull this out into a struct or class that has the health attributes and methods (not to mention, if you make your components structs of integers, you can work on the stack which will speed up CPU time rather than allocating on the heap with those classes).
Now everything in the entire game is an Entity, and you make an Entity Interface to interact with these components. Usually people will tag each Entity upon creation with a unique ID. This turns the game machine from updating a bunch of abstract, separate instances of classes to something more like updating a database.
Not only does this provide a cleaner approach from a coding standpoint, but it also makes many things easier. For example, in an RPG, maybe you have an ice spell that only affects flying enemies that are fire based (see subclass issues example above). It could be hard to find these if you've been instantiating them in the level at random and just have a big array of instances of enemies.
With the ECS framework you can query all Entities where the flying and fire based components are set to True. Or maybe you have a weapon which damages the wings of a flying enemy and it no longer possesses the special qualities of a flying enemy. Would you take the same enemy and create a whole new instance of a whole new class to do that? It would no longer be the "same" enemy in the code.
Again. This is really subtle, and probably is hard to tell why it works better from just thinking about it. You have to try the other way first to see what a pain it is to realize why the ECS framework simplifies this.
Since the 7DRL is under such tight time pressure, I'm not going to try a whole new coding framework for the first time during it. But I think I'll try to implement a hybrid OO/ECS approach whenever some obvious component jumps out at me.
Starting Saturday, you can expect this blog to turn into a Dev Log of the game. Hope to see you there.