Jump to content

  • Log In with Google      Sign In   
  • Create Account





The best in this kind are but shadows

Posted by Oberon_Command, 16 August 2013 · 522 views

Introduction

With the "Week of Awesome" coming up, and given that I'm likely to use the Unity game engine for my entry, I thought I'd write my first-ever journal post about a little pattern I've found useful in a larger project (which also uses Unity) that I've been working on over the last few months. I'm reluctant to give details on this larger project at this time since much of its design and implementation is still in flux, but I will reveal that it is a space combat simulator. I will furthermore reveal the intended use case of the idea I discuss herein, provide a sample implementation of the same, and moreover document my thought process while implementing it.

On an unrelated note, yes, both the title of this post and of my journal are references to A Midsummer Night's Dream, as is my username. I think I'm going to keep to Shakespeare references as a theme where appropriate, though I rather expect to discuss subjects for which such quotes would be inappropriate at some point.


Purpose

One feature that a lot of games utilize is a sort of "radar" display. This idea manifests in many different forms, but as far as I can see, its most common incarnation is that of a secondary display which provides an abstract overview of the player's situation without them needing to keep track of all the details themselves. Consider (for instance) the "motion tracker" in the Halo series, which allows the player to track both allies and opponents without actually needing to see them directly. Likewise, the concept of a "mini-map" as used by many real-time strategy games provides an overview of the battlefield that frees the player from needing to watch over the entire battlefield all at once.

In the project I'm working on, these displays are actually the primary method of interacting with the player's spacecraft, both in manoeuvring and in combat. There is an "external viewer," which corresponds to a third person perspective, but it is mostly there for eye-candy purposes and has little in the way of game-play value. Those readers who have played a submarine combat simulator like the Silent Hunter series or Dangerous Waters will be familiar with the idea of a "navigation map" or "tactical display" which is used to display the player's ship (referred to hereafter as "ownship") and other objects of interest in a way that shows their relative distances and other important tactical information.

Let's pretend we're implementing one of these displays ourselves. So we want a simple display where ownship and enemy vessels are "blips" on a radar display. To implement this, it appears that we need to have some kind of object that lives on the tactical display that will represent other objects in the "real world", without actually being coupled too closely to them. Let's pursue this idea and see where we end up.

On Unity

Since the rest of this post is going to be fairly heavily Unity-centric, this seems like a good place to review how Unity's object and scripting systems work for those readers who may be unfamiliar with Unity.

All entities in a Unity game are represented by "game objects" and all behaviour is implemented by "components," which are attached to game objects. Game objects essentially behave simply as containers for these components. Game objects can be marked as "children" of other game objects to form a hierarchical scene graph. Components can be bolted onto game objects either at design time (via Unity's editor) or attached to game objects by other components at runtime. The only component which all game objects carry is a "Transform", which represents the position, rotation, and scale of a game object in Unity's game world. All other behaviour must be attached to game objects in any given scene or onto "prefabs" which can be instantiated into a scene. Users may either use components which come with Unity (such the RigidBody component or MeshRenderer component) or components which are provided by the user in the form of JavaScript (actually "UnityScript"), C#, or Boo scripts.

Note that all code presented herein will be in C#, though I'd imagine that UnityScript or Boo users would have few if any problems porting it to their preferred language.

Refinement of Requirements

It seems like setting our "radar blip" objects to be children (in the scene graph) of the objects they represent would be the simplest way to accomplish what we want, since this will ensure that, assuming we don't move them around, the blips will have the same position relative to the objects they represent - ie. they will mirror the movements of the simulated game objects. For a lot of cases, this will work just fine. However, what happens if we want to rotate, zoom into, or pan through our radar display? Sure, we could move the camera around, and that might work pretty well, but what if we just want to change the distance between the radar blips without changing their apparent scale? Suppose we want to change the way the scale is represented on the display, such as if we want to keep all the blips on the display and have them get "stuck on the edge" as FPS-MAN (which coincidentally also uses Unity) does? What happens if we want to introduce some uncertainty in the position of our little radar blips, such as to represent the fact that most kinds of sensors are not 100% accurate at long distances?

It seems to me that it may be helpful to rephrase and refine our requirements somewhat. What we actually want here is an object which represents another object, which mimics its movements and consistently represents the distance between it and other objects, but in a different coordinate system, namely the coordinate system of the radar screen/tactical display. We would also like to be able to tweak this coordinate system somehow from the editor as well as from other components in the game world.

Introducing the TransformShadow

To accomplish this, we'll need to create a custom behaviour which, for descriptive purposes, we will call a "TransformShadow." To do this, we'll create a C# class which inherits from Unity's MonoBehaviour class, as we usually do when implementing custom Unity scripts. We'll give this class a field that the user can set to whatever game object they want the shadow to follow, plus a set of fields describing the coordinate system that the TransformShadow lives in. Furthermore, we'll have the shadow update its own Transform component on every update so that it mimics the movements of the object it shadows in real time.

Here is the code for this extremely basic TransformShadow:
public class TransformShadow : MonoBehaviour
{
    // note that these fields are public because the editor at least will need to
    // set these explicitly and other objects may as well.
    // if we only wanted these to be modified from the editor, we could make them
    // private and stick a [SerializeField] attribute in front of each of the fields
    // we want to be visible to the editor.
    public Transform shadowCaster = null;
    public Vector3 translationFactor = Vector3.zero;
    public Quaternion rotationFactor = Quaternion.identity;
    public float scaleFactor = 1.0f;

    // note that since all code that updates the parent transform will be in 
    // corresponding FixedUpdate() methods, so to prevent stuttering we'll put
    // our shadow update code in FixedUpdate(), too.
    void FixedUpdate()
    {
        if (shadowCaster != null)
        {
            // note that we also rotate the translation before we apply it and scale the position
            // as if it were the distance from the origin
            transform.position = (shadowCaster.position + transform.localRotation * translationFactor) * scaleFactor;
            transform.rotation = shadowCaster.transform.rotation * rotationFactor;
            transform.localScale = shadowCaster.localScale * scaleFactor;
        }
    }
}
More Features

The above code works, but it doesn't get us everything we want. In fact, it doesn't really get us anything more than we could already get by attaching our shadow objects to the shadow caster object and moving the camera around to simulate rotating, scaling, and translating the shadows together. Let's make this a little more flexible.

First of all, one of the things mentioned above was the idea of scaling the distance between shadow objects without actually scaling their form. This is quite useful for something like a radar or tactical display - we want to zoom into the display, increasing the apparent distance between "blips", but we don't necessarily want to zoom into those blips, themselves. So, we'll modify this code to actually have two different "scale factors" - one for distance, and one for form. Furthermore, for cases where we want to scale distance, but not form, we'll want some way to specify from the editor that this is the case. In fact, we can go even further and let users switch off and on specific types of shadowing if they like!

Here is the updated code to take all of this into account:
public class TransformShadow : MonoBehaviour
{
    // note that these fields are public because the editor at least will need to
    // set these explicitly and other objects may as well.
    // if we only wanted these to be modified from the editor, we could make them
    // private and stick a [SerializeField] attribute in front of each of the fields
    // we want to be visible to the editor.
    public Transform shadowCaster = null;
    public Vector3 translationFactor = Vector3.zero;
    public Quaternion rotationFactor = Quaternion.identity;
    public float distanceScaleFactor = 1.0f;
    public float formScaleFactor = 1.0f;

    public bool mirrorTranslation = true;
    public bool mirrorRotation = true;
    public bool mirrorScale = true;
    public bool scaleDistance = true;
    public bool scaleForm = true;

    // note that since all code that updates the parent transform will be in 
    // corresponding FixedUpdate() methods, so to prevent stuttering we'll put
    // our shadow update code in FixedUpdate(), too.
    void FixedUpdate()
    {
        if (shadowCaster != null)
        {
            if (mirrorTranslation)
            {
                transform.localPosition = TransformPosition(shadowCaster.position);
            }
            if (mirrorRotation)
            {
                transform.localRotation = shadowCaster.transform.rotation * rotationFactor;
            }
            if (mirrorScale)
            {
                transform.localScale = shadowCaster.localScale * formScaleFactor;
            }
            else if (scaleForm)
            {
                // note that this second cases is needed for when we are NOT mirroring scale, but
                // want to scale the form of our shadow object anyway.
                transform.localScale = formScaleFactor * Vector3.one;
            }
        }
    }

    // we extract this out for clarity's sake.
    public Vector3 TransformPosition(Vector3 srcPosition)
    {
        // note that we also rotate the translation before we apply it and scale the position
        // as if it were the distance from the origin
        Vector3 scaledPosition = srcPosition;
        Vector3 scaledOffset = (transform.localRotation * translationFactor);
        if (scaleDistance)
        {
            scaledPosition *= distanceScaleFactor;
        }
        if (scaleForm)
        {
            scaledOffset *= formScaleFactor;
        }
        return scaledPosition + scaledOffset;
    }
}
Update Ordering

Now, astute readers may have noticed a subtle problem with doing things this way. That problem is that it is not necessarily guaranteed that the shadow caster will be updated before TransformShadow.FixedUpdate() runs! For most cases, this can be fixed by changing the update ordering in the Unity editor to guarantee that every frame, other components will run their FixedUpdate methods before any TransformShadows run theirs. However, what happens if the shadow caster is another TransformShadow? And yes, that can happen! There's nothing in our code prohibiting it and I'm sure the reader can imagine cases where doing it would be useful. So how do we deal with it?

Personally, I can think of a couple of ways to do it. One way would be to put all the TransformShadows that do this into a tree and then update them manually. This would require creating a separate class that would bypass Unity's update loop to only update these specific TransformShadows at specific times, namely after all other TransformShadows have been updated. The way I'm using in my own code is somewhat simpler than this and requires much less code: all we do if we encounter a TransformShadow that's mirroring another TransformShadow is make sure the shadow caster TransformShadow updates first by manually calling its FixedUpdate(). This causes the shadow caster TransformShadow to be updated more than once per frame, however. To keep TransformShadows from being updated more than once per frame, we track whether or not a particular TransformShadow has updated that frame and only update it if it hasn't.

This necessitates adding an extra field and two extra methods to our TransformShadow class:
    private bool updatedThisFrame = false;

    // note that LateUpdate is called immediately after Update. This means that it
    // actually runs LESS often than FixedUpdate(), so it's guaranteed to ensure that
    // we don't do any calculations that we don't need to do.
    protected void LateUpdate()
    {
        updatedThisFrame = false;
    }

    // we'll call this from the FixedUpdate() method
    private void ForceTargetUpdate()
    {
        // if the shadow caster has a TransformShadow component attached to it, update it first
        TransformShadow targetCaster = shadowCaster.GetComponent<TransformShadow>();
        if (targetCaster != null)
        {
            targetCaster.FixedUpdate();
        }
    }
Then we need to modify FixedUpdate to the following:
public void FixedUpdate()
    {
        if (shadowCaster != null && !updatedThisFrame)
        {
            // if the target is another shadow caster, make sure it gets updated before we do!
            ForceTargetUpdate();
            if (mirrorRotation)
            {
                transform.localRotation = shadowCaster.transform.rotation * rotationFactor;
            }
            if (mirrorScale)
            {
                transform.localScale = shadowCaster.localScale * formScaleFactor;
            }
            else if (scaleForm)
            {
                transform.localScale = formScaleFactor * Vector3.one;
            }

            if (mirrorTranslation)
            {
                transform.localPosition = TransformPosition(shadowCaster.position);
            }
        }
        updatedThisFrame = true;
    }
Now we can allow TransformShadows to mirror each other.

Final Thoughts

While the initial use case for this component was the development of a "radar screen" like tactical display, there are other uses that can be found for TransformShadows. For instance, one could attach one to a camera object to have the camera track the player's character object without having to write a custom component for this. One could use TransformShadows to decouple the renderer from the physics engine - simply have Unity render the TransformShadows while doing the physics on their shadow casters. I'm sure readers can come up with other uses.

Feel free to use the code I have presented as you see fit, though I'd ask that if you find a bug, that you send me a message as a courtesy - I don't like presenting code with bugs in it! I welcome comments and critique.




October 2014 »

S M T W T F S
   1234
567891011
12131415161718
192021222324 25
262728293031 
PARTNERS