IslandWar: Animating Turrets

Given that a lot of my game involves turrets attacking and defending, after setting up my water plane and a small height-mapped island in it, the next thing I attempted was to import an animated turret in a way that lets me control its orientation and elevation from code.

To start with, I already used a consistent naming scheme for the bones in all my turrets:

Screenshot of Blender showing my bone naming scheme

In short, all turrets have a bone to change their yaw that is called "Rotor". The pitch of the gun barrels is changed by "Elevators" (there can be more than one in case the turret has multiple weapon arms). Finally, each gun barrel has a bone whose name contains the word "Barrel", i.e. "UpperLeftBarrel" or "Barrel1" and an associated muzzle bone exists named identically but with "Muzzle" instead of "Barrel":

  • Base
    • Rotor
      • LeftElevator
        • UpperLeftBarrel
          • UpperLeftMuzzle
        • LowerLeftBarrel
          • LowerLeftMuzzle
      • RightElevator
        • UpperRightBarrel
          • UpperRightMuzzle
        • LowerRightBarrel
          • LowerRightMuzzle

So all I had to do was to somehow identify these Bones in Unity and remember their Transforms so I could adjust them. It took some digging to find a way to access the bones of the GameObject the script has been attached to as there’s no explicit hierarchy (game engines often only sort bones in the order they need to be updated in, there’s no need to fragment memory with a tree).

In the end, I found what I was looking for by going through the SkinnedMeshRenderer. I don’t know if this is a good way of accessing the bones, but it works reliably. The rest was just a matter of writing down the code:

/// Enables easy access to the bones relevant for posing a turret
public class AnimatedTurret : MonoBehaviour {
  
  /// 
  ///   Called before the script's Update() method is executed for the first time
  /// 
  public void Start() {
    SysDebug.Assert(gameObject != null);
    identifyBonesOfAttachedGameObject();
  }
  
  /// 
  ///   Called once before rendering a frame
  /// 
  public virtual void Update() {
    if(this.rotorTransform != null) {
      this.rotorTransform.localEulerAngles = new Vector3(
        this.CurrentYawDegs - 180.0f, 0.0f, 0.0f
      );
    }
    for(int index = 0; index < this.elevatorTransforms.Length; ++index) {
      this.elevatorTransforms[index].localEulerAngles = new Vector3(
        0.0f, this.CurrentPitchDegs - 90.0f, 0.0f
      );
    }
  }
  
  /// Current orientation of the turret in degrees
  /// 
  ///   Zero degrees means towards positive Z, positive rotates counter-clockwise
  /// 
  public float CurrentYawDegs = 0.0f;
	
  /// Current elevation of the turret in degrees
  /// 
  ///   Zero degrees means perfectly horizontal, positive goes towards sky.
  /// 
  public float CurrentPitchDegs = 0.0f;
  
  /// Transform for the turret's rotor joint
  protected Transform RotorTransform {
    get { return this.rotorTransform; }
  }

  /// Transforms for the turret's elevator joints
  protected Transform[] ElevatorTransforms {
    get { return this.elevatorTransforms; }
  }

  /// Transforms for the turret's barrels and muzzles
  protected BarrelWithMuzzle[] BarrelAndMuzzleTransforms {
    get { return this.barrelAndMuzzleTransforms; }
  }
  
  /// 
  ///   Picks the bones used to transform the turret the script has been
  ///   attached to and remembers them for later
  /// 
  private void identifyBonesOfAttachedGameObject() {
    var renderer = GetComponentInChildren();
    if(renderer != null) {
      var elevatorBones = new List();
      var barrelAndMuzzleBones = new List();

      // Search the model's bones for things we're interested in
      Transform[] bones = renderer.bones;
      for(int index = 0; index < bones.Length; ++index) {
        Transform bone = bones[index];

        // Pick the first rotor transform (only one rotor allowed), collect elevator
        // and barrel transforms. For each barrel, find its associated muzzle.
        if((this.rotorTransform == null) && isRotorTransform(bone)) {
          this.rotorTransform = bone;
        } else if(isElevatorTransform(bone)) {
          elevatorBones.Add(bone);
        } else if(isBarrelTransform(bone)) {
          Transform muzzleBone = findMuzzleForBarrel(bones, bone);
          if(muzzleBone != null) {
            barrelAndMuzzleBones.Add(new BarrelWithMuzzle(bone, muzzleBone));
          }
        }
      }
      
      this.elevatorTransforms = elevatorBones.ToArray();
      this.barrelAndMuzzleTransforms = barrelAndMuzzleBones.ToArray();
    }
  }
  
  /// Determines if the specified transform is the rotor transform
  /// 
  ///   Transform that will be checked for being the rotor transform
  /// 
  /// True if the specified transform was the rotor transform
  private static bool isRotorTransform(Transform transformToCheck) {
    return transformToCheck.name.ToLower().Contains("rotor");
  }
  
  /// Determines if the specified transform is an elevator transform
  /// 
  ///   Transform that will be checked for being an elevator transform
  /// 
  /// True if the specified transform was an elevator transform
  private static bool isElevatorTransform(Transform transformToCheck) {
    return transformToCheck.name.ToLower().Contains("elevator");
  }

  /// Determines if the specified transform is a barrel transform
  /// 
  ///   Transform that will be checked for being a barrel transform
  /// 
  /// True if the specified transform was a barrel transform
  private static bool isBarrelTransform(Transform transformToCheck) {
    return transformToCheck.name.ToLower().Contains("barrel");
  }

  /// Locates the muzzle associated with a barrel, if any
  /// 
  ///   Bones that will be searched for the associated muzzle
  /// 
  /// 
  ///   Transform for the guns barrel for which the associated muzzle will be located
  /// 
  /// The muzzle associated with the barrel or null if not found
  private static Transform findMuzzleForBarrel(Transform[] bones, Transform barrelTransform) {
    string nameWithoutBarrel = barrelTransform.name.ToLower().Replace("barrel", string.Empty);
    for(int index = 0; index < bones.Length; ++index) {
      if(bones[index].name.ToLower().Contains("muzzle")) {
        string nameWithoutMuzzle = bones[index].name.ToLower().Replace("muzzle", string.Empty);
        if(nameWithoutBarrel == nameWithoutMuzzle) {
          return bones[index];
        }
      }
    }
    
    return null;
  }
  
  /// Transform of the turret's rotor (controls the yaw)
  private Transform rotorTransform;

  /// Transforms for the turret's elevators (control the pitch)
  private Transform[] elevatorTransforms;
  
  /// Transforms for the turret's barrels and muzzles
  private BarrelWithMuzzle[] barrelAndMuzzleTransforms;
  
}

Then I could use this as a base class, giving deriving classes the option to simply assign the CurrentYawDegs or CurrentPitchDegs angles in code.

Yes, I’ve worked with angles here. Normally my recommendation is to go with matrices 100% of the time because stupid Euler angles just cause more work and annoying problems, but in this case, each joint rotates on only one axis, so I can easily use Mathf.Atan2() on the directory of the enemy relative to the turret and come up with that angle. I also don’t need to involve quaternions to interpolate between orientations.

So here’s the result in Unity:

Screenshot of Unity using my script to pose a turret
    title=

I can now adjust the turret’s orientation and elevation on the fly in Unity’s editor and watch it change live in the game. Pretty cool :)

My plan is to spread the turret AI using a set of derived behavior classes:

  • AnimatedTurret
    • OffensiveTurret
      • RocketTurret
      • ArtilleryTurret
    • DefensiveTurret
      • GatlingTurret
      • SniperTurret

I’m not sure yet if this will work out well, but it seems to offer the best ratio of code reuse and will hopefully keep the code manageable.