Saturday, March 2, 2013

Object Orient All The Things

Hello again,

This post is going to be slightly different. Its about porting what you learn in your object oriented design class to the real world: where it works and where it doesn't. Lets get started.

   My latest game I'm working on is a retro 2D style game, mostly inspired by Legend Of Zelda : Link to the Past. Since its a 5 week project, we're not doing any dungeon crawling, but what we are doing are boss fights. And since I have another programmer, I get to do all the boss AI! Dream come true. Now, I sought out to make some simple, re-usable code from my favorite AI book (and one of my favorite game programming books in general),  Programming Game AI by Example, by Mat Buckland.
http://www.amazon.com/Programming-Game-Example-Mat-Buckland/dp/1556220782

Its a great intro to AI book, go read it! Really, its way better than this post. Go read it!

...You read it? Awesome! Lets continue.

So after watching some LOZ Link to the Past boss videos on youtube, I noticed all bosses are pretty state driven. 1st chapter of that awesome book I know you just read is about state driven AI, so that's pretty fantastic. I made a StateMachine class and an abstract State class and was all set to go! I made an EnemyBase class who owned its own StateMachine and I was pumped to start making state based behaviors, everything was so awesome and cool there, and I started on the first boss. Here's where my super object oriented world crashed.

Spoiler alert, first boss is going to be a wizard who shoots fireballs in random directions. Also, before the fight, there are going to be archers who shoot arrows. Awesome! I'll just make a shoot arrow state and...oh wait. Let's look at our abstract state class in C#

public abstract class State<T>
{
       public virtual Enter(T owner){}
       public virtual Execute(T owner){}
       public virtual Exit(T owner){}
}

So what if I made a State called ShootArrow?

public class ShootArrow : State<EnemyBase>
{
      public override Enter(EnemyBase owner)
     {
           owner.ShootArrow();
     }
}

   There's a huge issue with this approach, and that is EnemyBase doesn't have a ranged attack! Sure, I could've made one, but not every enemy needs a ranged attack, so that's not very object oriented to put one there...they could have a virtual ranged attack that's only implemented in classes that need to have a ranged attack, but what if two classes need to implement it that exact same way (like Archer and Boss1)? Then both classes would have duplicate code...no no no. Never think copied code. Always wrong. Not object oriented. You will get and F on the project. Bad bad bad.

   But even here I had already jumped ahead of myself. Without putting all functionality for every enemy I ever want to have in my base class, I can't have any state inherit from State<EnemyBase> as it won't be able to call any functions it doesn't have. So, each thing inherited from EnemyBase will need to have its own state machine, so states can call the functions they'll need. I'll need things like

public class ShootBolt : State<Boss1>
{
     public override Enter(Boss1 owner)
     {
          owner.ShootBolt();
     }
}

instead. Then Boss1 can have a function that shoots bolts! Hooray! Oh wait....have you seen the problem yet? Archer needs to shoot arrows too...but I can't give the archer a state made for Boss1, the compiler would never allow it. So what if I made a class inbetween EnemyBase and Boss1 and archer called EnemyCanShoot : EnemyBase and then do Boss1 : EnemyCanShoot and Archer : EnemyCanShoot? Then my state could be public class ShootBolt : State<EnemyCanShoot> instead? That could work....if shooting a bolt was the only thing I ever needed to share between classes. What happens if Boss1 and BasicWizard need to teleport as well? Boss1 can't inherit from EnemyCanShoot AND EnemyCanTeleport, this is C# (oh yeah, this projects being made in C# and Unity3D), multiple inheritance isn't allowed. Even if it were C++ and we could do MI, that could start to really lead to some super sloppy code.

class Boss1 : public EnemyCanShoot, public EnemyCanTeleport, public EnemyCanSlash, public EnemyCanDie, public EnemyCanBreatheFire, public EnemyCanSwim, public EnemyDropsPickup

etc...yikes. So this doesn't seem like a good idea, especially when all those EnemyCan's all inherit from from the same EnemyBase and would be overriding different functions at the same time and trying to figure out which fricking Move function you actually wanna call like the one that swims or flies or teleports or asdfbgrwhrtw. Messy. More like EnemyCant. Ha. Jokes.

   But really, maybe we could do something like an interface? Yeah, that could work, public class Boss1 : ICanShoot and public class Archer : ICanShoot that just have some sort of functionality to shoot. Problem is, interfaces can't have anything really in them, like variables or function definitions. So if Boss1 and Archer need to shoot the exact same way, you end up with redundant code. Insert Luke Skywalker "Nooooo" here.

   Is there any hope out there? Is this super StateMachine not as awesome as I originally thought? Can I not have re-usable states everywhere? What to do? Ultimately, I had to face some kinda rough truths (at least for me). Sometimes, its okay to not object orient all the things. This idea of an abstract state machine presented in the book wasn't meant for what I was trying to use it for. That's not to say its not still awesome, it takes code away from your Boss1 code and breaks it down and makes it much more manageable and readable without alot of nasty switch statements. Which is wonderful. But I had to accept it, without a major code re-structure (which I really didn't want to do),  I was going to have either some duplicate code or some enemies that had (potentially alot of) un-needed functionality. If I were to start this project over,  I probably would've done things a bit differently. I might not have used this StateMachine approach as such a godlike over any other AI approach and make it so every enemy HAS to use it.

   And you might be wondering "why didn't he just do this" or "why didn't he just do that" or some super thing that I didn't think of that magically does everything I was just trying to do. First, let me say I tried alot of things that I didn't even really mention in this article to get this approach of re-usable states to work that failed, so I may have attempted it. Secondly, this is a 5 week project where I have quite alot of other things to do, both in this project and outside of it too, so spending a week trying to get this perfect for all cases just wasn't in the cards. That being said, if someone has some way or technique that does let this work the way I was trying to, please share!!!! I'm very curious, and I'm sure there's a solution out there I didn't have time to try that works great and is magical and awesome and lovely. But that being said, here's how my story ends now:

There are 5 lines of duplicate code copied and pasted in my project now. It happened. And you know, I'm pretty okay with that. I made alot of object oriented code in this that I'm quite proud of (next post maybe) and some AI that's working nicely. And if the alternative is spending hours and hours getting an approach to do something its probably not truly meant for, only 5 line is great to me. Spending the time to completely object orient (assuming you are doing an object oriented approach, not something like data oriented) is usually worth it, but if you don't do it to its fullest because the game needs to get done, or because the approach you took is awesome and perfect except for one or two little instances, trust me: its not the end of the world, and your teacher isn't going to give you an F if your game is awesome.

--daviD Ward