While it's on my mind, I wanted to write here few lessons I learned in last few days.
- If the language supports polymorphism, USE IT.
- Design before coding.
- Having properly structured code helps. A LOT.
- Keep the objects closely related on one heap.
Those points don't seem to be related, right? Allow me to explain.
I've started to hunt coop related bugs in the game. Code, on the first look, seems to work perfectly; however, most code is structured like:
if (multiPlayer == 0){ // do single player behavior}else{ // do co op behavior}
or a variation of that code, depending on player which "activates" part of the code.
So, here's lesson 1: Why making numerous comparisons like that through the whole class - some of which are surprisingly volatile - when I can make new class which inherits the current one and override specific methods to adapt them for co-op mode of the game. With that, I'm also avoiding the problem of pointless allocation of resources (time and memory) for object related to co-op mode.
Sound simple, right? Well, lessons 2 and 3 strike here at the same time. Due to "designing on the fly", I have to rewrite and refactor big parts of the code to adapt them for polymorphism. This means a lot of wasted time, due to the though process:
"Hmm, I'd like to add . Well, I can add this code here, and that code there... *writing code* Oh cool, this works! Yaay!"
"Now, to add ... *writing* Crap, code isn't good here anymore... *moving and/or rewriting code* Good, now works."
"OK, is next... oh not again, is broken. *rewriting and/or moving code*"
"ARRRRRGH, NOT AGAIN!" (I'm now at this point)
Result: lots of wasted time trying to improve the structure of the game while trying to preserve current functionality (a.k.a. trying to NOT break it).
However, while I'm rewriting the code, I have to be careful. Some parts of the code, with best example being Draw(GameTime) method where last drawn object is on top, require executing in specific order. So, I can't simply do this:
public class A : GameScreen{ // ... public override void Draw(GameTime gameTime) { // ... }}public class B : A{ // ... public override void Draw(GameTime gameTime) { // ... spriteBatch.Draw(player2texture, player2rectangle, Color.White); base.Draw(gameTime); }}
because Player 2's sprite would end on bottom of everything, which may not be desirable.
Solution for that problem that I came up with is to extract relevant parts of A.Draw() and raise them to Protected level:
public class A : GameScreen{ // ... public override void Draw(GameTime gameTime) { PartA(); PartB(); } protected void PartA() { // ... } protected void PartB() { // ... }}public class B : A{ // ... public override void Draw(GameTime gameTime) { PartA(); spriteBatch.Draw(player2texture, player2rectangle, Color.White); PartB(); }}
This is also why properly structured code is necessary. Instead of having massive amount of code inside one method, having the same code split over several methods improves maintainability and readability.
Lesson 4 is connected to 2 and 3 as well. Inside my Gameplay class (the one having actual game logic) I had few... awkward? objects:
Texture2D player1Texture;byte player1spawnID;Texture2D player2Texture;byte player2spawnID;
Why is that?
Player player1;Player player2;public override void LoadContent(){ player1 = ScreenManager.Player1; player2 = ScreenManager.Player2;}
In other words, I have objects related to players, whose objects persist through whole game, being created, disposed and recreated inside the class having game logic. Memory problems aside, not having player sprites stored inside player classes also forces me to needlessly duplicate the code and use additional checks to decide whose texture I need to move. Luckily, I don't need to do much to restructure the code and eliminate that annoyance.
Suddenly, improving level fie doesn't seem so important.
So, what did I learn from all that for next project (which is already defined in my head)?
I wasted too much time restructuring current project to enable modifications and upgrades. In order to avoid this in next project, I need to write Game Design Document and Tech Document in order to precisely define project and save time.
I hope that I won't repeat mistakes in the next project (Snake clone with 3D camera.)
After reading some topics on GameDev.Net, especially this one, I realized I have to learn the following:
- Garbage collector
- Making unit tests
Thanks for reading, I'm returning to my code.
Actually, many experienced folks (myself included) will caution you away from too much reliance on polymorphism these days. Object attributes, behavior, etc... should be gathered together by composition rather than inheritance. (The old "favor composition over inheritance" guideline). If an object is intended to be controlled remotely, it would have a RemotePlayer controller rather than a LocalPlayer controller, for example. The game logic itself doesn't have to change that much.
Polymorphism enforces IS-A relationships, while object composition enforces HAS-A relationships. Too much IS-A in a game object structure can get tricky, or lead to serious identity problems. Consider, for example, containers. Object A wants to be a Treasure Chest to hold stuff. Object B wants to be a Backpack to hold stuff. You might think that since they are similar (container) each would derive from a base Container type, but that causes a problem. Object A can not be equipped/carried by the player, while Object B can. So then you have to fork Container into CarryableContainer and NonCarryableContainer. Now, some Treasure Chests can be trapped, so that leads to another fork: TrappedNonCarryableContainer and NonTrappedNonCarryableContainer. And so forth. Such inheritance hierarchies can [i]quickly[/i] explode into unmaintainable spaghetti.
On the other hand, using object composition instead, then Object A, which wants to be a treasure chest, would aggregate a Container component with a Renderable component. Object B would aggregate a Container, a Renderable, and a Carryable component. Object C (Trapped Chest) would aggregate a Renderable, a Container, and a Trap. You could even do a trapped backpack by aggregating a Renderable, a Carryable, a Container and a Trap. A Death Bag (looks like a backpack, explodes on touch) might aggregate a Renderable and a Trap. You don't have to worry about figuring out which node in a complex hierarchy a particular object needs to inherit from to get the desired behavior; you just aggregate together all the small behaviors and properties that, as a sum, define what your object is and how it behaves. A different aggregation results in a different object.
Typically when building such a system [i]some[/i] polymorphism is involved (especially at the system/framework level, to get your object system to behave) but usually such is limited to low-level framework code only and by the time you get to coding actual game objects the polymorphism is safely hidden away where it can't hurt anyone, and composition is used pretty much exclusively. (It is, of course, possible to sub-class particular components but that should be used carefully, since it can simply lead right back to the inheritance problems.) If you find yourself building complex game behavior/object hierarchies through inheritance, it's usually a sign that there is something wrong with your design.