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.Â