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.

4 thoughts to “IslandWar: Animating Turrets”

  1. Hey,

    you can do it easier by using Transform.Find if you know the bone structure.


    // We normally use Awake for initialization, as it's called when the object is created for the first time.
    // Start is called before the first frame gets rendered after the object was instantiated.
    // Start is usually used if you want to look for other Objects, i.e. on level start, because it's called
    // after Awake() and we know that all objects were created
    public void Awake() {
    rotorTransform = transform.Find("/Armature/Base/Rotor");
    }

    Alternatively you can make the transform visible in the Unity Inspector buy adding [SerializeField] Attribute to it

    [SerializeField]
    private rotorTransform;

    and then assign the bone from the hierarchy tab to it (doesn’t seem to work from project view). Basically you place the object in the scene, assign a bone (child transform) in the inspector and hit “apply” to save it back to the prefab.

    — Tseng

  2. Cool! So if the field is serializable, it will be saved in the prefab and even correctly assigned to the instance when the prefab is instantiated.

    I’ll stay with my script for the turrets since then I don’t have to assign so many things by hand (some of them got about 12 barrels + muzzles), but this will definitely come in handy for all those cases where I don’t have to take care of a dozen or so almost identical models :)

    — By the way, following your website link I saw you actually published your Unity game on Android. Nice! My first game was a freebie, too, so I guess we’re in similar situations right now :D

    Are you trying to get a foot into the indie scene, too, or just as a hobby and a few cents on the side?
    Anyway, ping me if you’ve got anything new!

  3. Well first was kind of an experiment to get warm with Unity. Trying to get a 3D Artist for something more advanced, I suck at modeling.

    @Unity:
    By default Unity3d will serialize all public variables (it’s aware of, like Transform, GameObject, MonoBehaviours, int, float, Lists/Arrays (even uninitialized int[]). Properties can’t be serialized in the Inspector :( [SerializeField] is useful for private and protected variables to serialize them. Don’t like public too much.

    I’ve seen you’re starting to use G+ too? \o/ Hate Twitter ^^

  4. Same here, my modeling skills are barely enough for a chair or a table. The turret from this post is part of 3drt’s Wargear Turrets collection

    I noticed that properties aren’t serialized when I had wrapped my angles in properties so the transforms would only be updated if the properties changed. The orientations could no longer be adjusted in the inspector and the turret jumped back to its original pose all the time. Oh well :)

    G+… well, I signed up because I had received an invitation in its beta phase and a friend wanted in. I could give it a try, but I’m not really the Facebook kind of guy ^^

Leave a Reply

Your email address will not be published. Required fields are marked *

Please copy the string wqm3EB to the field below:

This site uses Akismet to reduce spam. Learn how your comment data is processed.