So how do you manage creating something as complex as a real-time strategy game? Especially if you’re a single programmer doing the whole thing with only seven days?
I did mine with standard software engineering principles.
The idea is to segregate your classes from low-level details to high-level concepts.
I’d usually explain this by terminologies like encapsulation, abstraction, loose coupling, etc. but I’ll explain by example.
Here are the set of classes just for a unit. You’ll see there are quite a lot of them.
These classes deal directly with the Unity system (the game engine that I use). One class does one thing only.
Generally, no other classes should directly call Unity’s innards except these (but there are exceptions).
UnitMovement class: Deals with moving and rotating the unit. The only class that directly controls the unit’s rigidbody. Also handles movement collisions and obstacle avoidance.
UnitAnimation class: The only class that controls Mecanim (via the Animator class). Mecanim is Unity’s animation system.
UnitNetwork class: The only class that deals with NetworkView and Network. Sends and receives network data for the unit. For the unit, this is the only class that has RPC’s and network state syncs.
Health class: Manages this unit’s health, very basic piece of code.
The idea is that these classes are the bridge between Unity’s systems and my code. That means my code more or less doesn’t directly access Unity classes (except the aforementioned ones).
Note: the names of these levels are just something I made up
On top of the Lowest Level is the Basic Problem-Domain Level. It makes use of the classes of the Lowest Level (specifically, via the public functions they provide) to create higher-level convenience functions. All levels above this continue to deal in terms of the problem domain.
Ok, but what the hell does problem-domain mean? It means since my game is an RTS, my functions are now structured in terms of TurnToTargetUnit(), instead of RotateToVector().
I now think in terms of an RTS game, not as a basic 3d system. That’s because my problem-domain is real-time strategy. So my code deals in terms of real-time strategy concepts. It provide functions where the caller doesn’t need to know the underlying 3d world and physics system, or at least, lessen the need to know that.
This level contains only one class, the Unit, which acts as the façade class to the various Lowest Level classes. There is no inheritance here. Unit simply has variables to the existing UnitMovement, UnitAnimation, UnitNetwork, etc.
The Unit class has public functions such as:
MoveToPosition or MoveToUnit: uses UnitMovement to actually move, and UnitAnimation to play the move animation for you
DoMeleeAttack: plays melee attack animation (via UnitAnimation), and turns on melee collisions. It also ensures this action is replicated across the network when in a multiplayer game (via UnitNetwork)
IsNearPosition or IsNearUnit: uses UnitMovement to check if this unit is near enough a position or another unit
IsFacingUnit: uses UnitMovement to check if this unit is facing another unit
IsNearAndFacingUnit: convenience function that combines the two
IsDead/IsAlive: uses Health class to check if unit is alive or dead
GetAllEnemiesNearMe: returns an array of enemy units that are within specified radius, sorted by proximity
As you can see, Unit merely defers the actual work to the Lowest Level classes. Unit is like the conductor in an orchestra, he doesn’t really play any instruments, but he coordinates the ones who do. He tells them what to do, and at what time.
Adds basic behaviour on top of the Basic Problem-Domain Level.
The Medium Level now directly corresponds to what actions a unit does when you right-click on something. Since I want different unit types to do different things, I separated this per class.
So I have different types of Actions that the unit can do. I created an abstract base class Action, and the Unit class has two variables of these, one it uses when you right-click the ground, and one it uses when you right-click an enemy.
Then I have different subclasses of Action:
Will simply move to the requested destination (using Unit.MoveToPosition).
Take note that the Unit class handles both the physics of moving, and playing the move animation for us with that single function call.
Will move near enough the targeted enemy (calls Unit.MoveToUnit), and once it reaches it (continually checking the return value of Unit.IsNearAndFacingUnit), will keep attacking it (calls Unit.DoMeleeAttack) until the target dies (checks return value of Unit.IsDead)
Again, take note that calling all those functions ensure that the animations are played properly for us, and that movement and attacks are synced across the network without us needing to worry about it, because the Lowest Level handles that for us. At this level, we only care about designing the behaviour of our unit, not low-level details like setting the speed, or blending between two animations.
There would be other things, like ActionRangedAttack, or ActionDashAttack (the one that Bone Wheel uses in my game), perhaps an ActionBuffAlly, etc.
So now I provide higher-level behaviours by combining the use of public functions that the Unit class provides. The code in my Action classes is like a MacGyver that combines different low-level tools to finally do useful stuff.
Also, I can assign different Actions to units. This means I can turn a melee unit into a ranged unit by swapping what Action it uses.
(How those Action classes can get what the requested destination or target is, is handled by my Order singleton class. It simply gets the 3d x,y,z position of where the mouse cursor is, or the actual Unit that is under the mouse, if there is any. It communicates with myUnitSelector singleton to tell all currently selected units that a command was issued (when a right-click is detected). But what each unit exactly does when such command is issued, is determined by which Action subclass it uses.)
Adds behaviours that players normally expect. I actually did not have time to implement this for the #7dRTS. Here are some ideas:
Go to the nearest enemy it can find, attack until it dies. Then go to the next nearest enemy again, and kill it. Repeat the whole process, until it can’t find any more enemies.
Sounds complicated to code, but really, all it does is keep on using ActionMelee on every enemy it sees, until it can’t find any more enemies in its vicinity.
Retaliate against any enemy that engages it, but do not pursue if the enemy retreats or goes out of range of my attacks. I would have to create a Unit.OnBeingAttacked(Unit attacker) for this.
(for ranged units only) Maintain farthest distance possible from any enemy and attack it from afar. This means it will move backwards if it is too close the target, or move forward if it gets too far.
Maybe I should have combined my Medium Level and the planned Highest Level into one, actually. I also probably should have coded it so that I can use my behaviour tree plugin to implement such high-level behaviours, but of course, I only had 7 days for this, and time is running out, so there is also merit to hard-coding the behaviours into those Action classes in the meantime.
There are actually a few more fundamental things lacking here, like ranged attacks, unit formations, pathfinding, special abilities (i.e. spells), and perhaps more depending on what design I want Strat Souls to move towards.
I also have not added structures/buildings in the game yet, and I imagine I may need to change the code once more to accommodate that.
I should also have made a separate UnitAttack class as part of the Lowest Level. It would deal with melee collisions and launching of projectiles. I did not have time to implement proper ranged attacks though, so I left it at that.
The idea is each level provides convenience functions to the one above it.
There are two benefits to this:
Minimize the impact of change: Changing code in one level, shields the other levels from needing to be changed too much (sometimes, not at all), minimizing re-writes, human mistakes, and bugs. And with the nature of game development being iterative (you keep on refining the prototype), you will deal with having to change code constantly.
For example, what if you decided you want to switch to CharacterController instead of Rigidbody? You’d know that the only class that needs to be changed is the UnitMovement class; it’s the only one that handles the physics system. So you don’t need to worry if your other classes need to be changed. What if you want to switch to the Photon networking library? You’d only change the UnitNetwork class. And so on.
This makes it easy to swap out different sub-systems for your game. Heck, if you really need to, you can port your code to a 2d game engine and only the low-level classes need to have drastic changes.
Manages complexity: Structures like this allow you to think easier because you usually only need to deal with one level at a time. When you’re coding, say, a patrol behaviour, you don’t need to worry about rigidbody.velocity or OnCollisionEnter or anything like that. You just use the convenience functions that you made in the Unit class.
In case you need to create new convenience functions, then likewise you don’t need to worry (too much) about higher-level behaviours while you deal with changing the low-level ones.
There will be times where you need to zip back-and-forth changing code in all levels, most likely when your system isn’t stable yet (when you’re still iterating on ideas). But once you’re definite with how you want things to work, you’d usually only deal with one level at a time. And that is a huge load off your brain.
Why not? Units are the central feature of the game, so it makes sense to devote a big system around it.
Generally, no. The concept itself shouldn’t impact your game’s framerate, but if you do your particular implementation where, for example, you’re doing an expensive calculation in a tight loop, then obviously you’ll see some slowness.
As with anything, measure your code’s performance first, before you start pointing fingers anywhere. And even then, remember, you structured your code where things are segregated by levels. When you find the bottleneck, you can optimize that problematic part without impacting the other levels of your code too much.
Oh, you’ll learn to see why this is worth it.
Just like I did.
In any case, the code I explained is only one way of achieving a clean structure for your game’s system. It could be that your design for your game is simpler, and you’d arrive with a far more simple implementation.
But whichever particular implementation you make, the underlying concepts should always stay the same: loose-coupling & modularity (the idea that you can easily swap one sub-system for another), abstraction (hiding low-level details by the use of convenience functions), and encapsulation (disallowing your high-level classes from directly messing with the variables of low-level classes).
Disclaimer: I actually have no formal education on software engineering, and I’m still learning. I got all that I know from the Internet, discussions with friends, and really good books. I recommend Code Complete.
Check out the whole dev journal for Strat Souls here.