Taming the complexity of Weapons Systems in Unity3d using C# Interfaces August 12, 2009
I confess: A lot of the programming I do for unity is in UnityJS -- the bastard language of Unity and ECMAScript that lets you get things working without a lot of overhead. It is great for prototyping. We've all taken a prototpe too far and regretted the bird's nest of code that ensues. Here's an example of a refactoring that I recently did to accommodate the increasing complexity of the code base, to help transition from prototype to product. It takes advantage of a C# language feature that UnityJS lacks completely.
So, our weapon would look like this:
Finally, here is the character script
So, this lets us switch weapons easily, and maintains the nice separation of concerns between the character script and the weapons script. Now we can be free to implement the weapons however we like, but can use the type system to know that the weapon will have an appropriate response for everything I want to do with it.
The Complexity Infects
Halt Yablonski, our hero, was prototyped with "just" a shotgun. The shotgun was instantiated and parented to the character, and a script on the character had the script that controlled firing. What all is involved with firing? Well, there is the game-level stuff ( removing ammo, rate-limiting, ) and the world-level stuff (activating animations, instantiating particle effects, throwing collider.) With just one weapon, the character script simply had a ShootMe() method. Then it was time to play with fire, and add in the flamethrower. Well, I could just put in a whole bunch of if statements, which would be fine for two weapons, but what about when I want to add in the baseball bat? And the next weapon and the one after that? A whole crapton of if() statements? does UnityJS have a `case` or `switch` statement? Ew, it just stinks. The character object suddenly is implementing the behavior for a great many other domain objects. What we want to do is pull that behavior out of the character script and divide it among the appropriate weapons. Cool. Okay, wait a minute. That sounds great, but how do we do that, really? Well, we could have a script for each weapon, and then send a message to the weapon on an action. As long as we implement support for all the messages (which we make easier with some inheritance), then we should be o-k. Using SendMessage is really slow and what if we want to do things like ask a weapon if it is fireable or how much longer until the reload is done -- that kind of synchronous communication is all but impossible with SendMessage. Also, what if we decide to add more messages later, how can we ensure that all of our weapons support all the messages?Interfaces to the rescue!
In its simplest form, here is the weapon interfacepublic interface IWeapon{ void Shoot(); }
public class Shotty : MonoBehaviour, IWeapon { private Controller controller; void Awake(){ //in practice, i use the singleton technique instead of finding the game object. much faster ^_^ controller = (Controller) gameObject.GetComponent("Controller"); controller.Switched(this); // let the controller know we are done loading. } public void Shoot(){ Debug.Log("Bang"); } }
using UnityEngine; using System.Collections; public class Controller : MonoBehaviour{ public IWeapon weapon; public void Switched(IWeapon newWeapon){ Debug.Log("New Weapon"); weapon = newWeapon; weapon.Shoot(); //just for demo purposes, //normally this would be called elsewhere } }