Páginas

sábado, 1 de novembro de 2014

Unity and Components Orientation Tutorial - Part Two

Infinite Sail part two: the turrets.

In part 1 of this series, I've introduced what it is about: trying to convince you that Component Orientation is a good way to go within Unity, and helping newcomers to think in component terms. And for this, I would make a game called Infinite Sail. In this part, I'll finally get some code! Please, do remember that I will consider that you already know the basics of the Unity and C#, as this is not a really “new programmers” tutorial series. Even though I don't think things will get so complex, I won't go on details about some things, and trust that you'll google or research when needed.

Sooooo... Let's get started! First, I created a new project in Unity, set it to default as 2D (I never used 2D collision system, so I'll adventure on this). Created the basic folders structures that I'm used to work, set up my Unity layout, and finally created a new empty scene.

I've set the background color to an ocean blue, the camera depth to 1000 and position z = -500, and the orthographic size to 100. In the end, I got something like this:


As in every new project, I have no idea on where to start coding... even more on a tutorial. To make matters worse, I have no idea of what this game will be, either! Since it's a tutorial and the game does not to be fun, let's go with something simple. Enlightenment Age ships sailing on a waveless ocean shooting each other with its cannons. Or shooting monsters... Let's stay with ships, for now...

I have a problem when starting new projects, that's my inability to make something playable right from the start, so it might be a while until we have “a game”. For this game, I want that a group of cannons can pick a target by themselves and start shooting without the player needing to issue an order. I can change this later. So for now, I'll do the initial automatic aiming system.

For this, I'll need one enumerator and a few components:

  • TargetTypes: PlayerShip or EnemyShip;
  • TargetableBehavior: means that something can be aimed at;
  • TargetablesLister: to keep lists of our targets;
  • TargetFetcherBehavior: fetches a target for shooting;
  • TargeteerFullRotationBehavior: aims at a target.

That's enough for part two, I guess! When I had my first contact with Components Orientation, I had the feeling that I needed to do much more code than needed to fulfill a task. Actually, this is often true! But there is a benefit that outweighs this cost really fast, as your projects grows more and more complex. It's low coupling between components and game objects. If you've already developed a game in OOP, you'll feel that later, this extra code now will have saved you from hundreds, if not thousands, of lines of code created or changed.

The aiming system will work in a very simple fashion. Any GameObject with a TargetableBehavior component is supposed to be a target that can be aimed at. Don't care what the GameObject is. It could be a barrel, an alien, the player, enemies, an immortal rock - anything. Components like TargetFetcherBehavior will search for appropriate TargetableBehavior instances and set them as a target. Then components like TargeteerFullRotationBehavior will try to aim at them.

Enough talking! First, we do the TargetTypes enumerator. Its fast!

// Types that a TargetableBehavior can be. Values are defined to avoid
// elements reordering to affect the Prefabs. It is ugly, but better safe
// than bug, in this case...
//
// As a game grows, we might reorder the enum to group elements and make
// it easier to know what exists and what does not. In this case is
// not likely to happen but again, better safe than bug
//
// Last Value: 2
public enum TargetTypes {
    None = 0,

    PlayerShip = 1,
    EnemyShip = 2
}


Next, comes the TargetableBehavior, and it's related behavior, the TargetablesLister. The “lister”, in this case, is here only to make it easier to find instances of TargetableBehavior. I didn't call it “manager” because, in reality, it doesn't manage anything. For us, it will be better that way, since TargetableBehavior is not a key component for prefabs or objects pooling.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;



/// <summary>
/// Keeps lists of TargetableBehavior. The behaviors themvelves are
/// responsible in keeping the lists updated. This Lister only
/// stores the lists.
/// </summary>
public class TargetablesLister : MonoBehaviour {
    // We want this to be a "singleton".
    static private TargetablesLister m_Instance;
    static public TargetablesLister Instance { get { return(m_Instance); } }

    // Lists for storing instances references. Could be a dictionary,
    // but I'll keep as separates variables for now
    private List<TargetableBehavior> m_PlayerShips;
    private List<TargetableBehavior> m_EnemyShips;



    private void Awake() {
        // "Singleton" setup
        if(m_Instance == null) {
            m_Instance = FindObjectOfType<TargetablesLister>();
        }
        if(m_Instance != this) {
            Debug.LogWarning(
                "Multiples instances of TargetableLister detected. Deleting this component instance.",
                this.gameObject
            );
            TargetablesLister.Destroy(this);
            return;
        }

        // Allocate the lists
        m_PlayerShips = new List<TargetableBehavior>();
        m_EnemyShips = new List<TargetableBehavior>();
    }



    /// <summary>
    /// Returns a list of targets by type.
    /// </summary>
    public List<TargetableBehavior> GetTargets(TargetTypes targetType) {
        List<TargetableBehavior> targetsList = null;
        
        switch(targetType) {
        case TargetTypes.EnemyShip:  targetsList = m_EnemyShips;  break;
        case TargetTypes.PlayerShip: targetsList = m_PlayerShips; break;
        }

        return(targetsList);
    }



    /// <summary>
    /// (Unsafe) Registers a new target in the lists.
    /// </summary>
    public void Register(TargetableBehavior target, TargetTypes targetType) {
        List<TargetableBehavior> targetList = GetTargets(targetType);
        ValidateRegisteredStatus(false, target, targetList);
        targetList.Add(target);
    }



    /// <summary>
    /// (Unsafe) Unregister a given target from the lists
    /// </summary>
    public void Unregister(TargetableBehavior target, TargetTypes targetType) {
        List<TargetableBehavior> targetList = GetTargets(targetType);
        ValidateRegisteredStatus(true, target, targetList);
        targetList.Remove(target);
    }


    

    #region Debug Methods

    /// <summary>
    /// Validates if a target is registered in a given list or not.
    /// Won't fix anything, but will emit errors if possible.
    /// </summary>
    [
       System.Diagnostics.Conditional("DEBUG_GAME"), 
       System.Diagnostics.Conditional("UNITY_EDITOR")
    ]
    private void ValidateRegisteredStatus(
        bool correctStatus, TargetableBehavior target, List<TargetableBehavior> list
    ) {
        if(target == null) {
            Debug.LogError("[Error] Registered validation failed. Target is null");
            return;
        }

        if(list == null) {
            Debug.LogError("[Error] Received null list!");
            return;
        }

        bool registered = list.Contains(target);
        if(registered != correctStatus) {
            Debug.LogError(
                "[Error] Registered validation failed. Expected [" + correctStatus +
                "] and found [" + registered + "].",
                target.gameObject
            );
        }
    }

    #endregion
}


As you can see, the code is very simple. New C# programmers might learn two new things in this code. The first one, is the Summary comments before the methods and class. These comments will appear on the code completion screen, and this can be quite helpful as the project grows with more and more lines of code, and we forget what “old” methods did.


The second one is the System.Diagnostics.Conditional. Conditional methods are normal methods with some limitation as in that they cannot return values. They are tied to compiling directives, as macros defined with #define. Conditional Methods will always be compiled, but if the needed directives are removed, the compiler will remove the calls for those methods as well. This is a clean way of calling methods to validate the game (in order to detect bugs) and easily remove these validations calls for the release build, and thus making the release build slightly faster.

In the code, I made a method to validate if a given reference exists or not within a list. This method will be called as long as either UNITY_EDITOR or DEBUG_GAME macros are defined. UNITY_EDITOR is defined by default while on the editor. DEBUG_GAME I have to define it myself, in case I want these validation methods to be called in stand alone builds of my game. You can find a lot more on Conditionals on google and in the MSDN C# Reference.

The TarbetablesLister component is supposed to behave like a “singleton”. The quotes are here because the singleton is actually a component. If you're new to the Singleton concept (in or out of Unity context), I'd advise some research on it. It's very simple but interesting pattern. The remaining of the class code is pretty straightforward, and I believe it doesn't need any explanation.

Next, comes the TargetableBehavior class. For now, this one will also be really, really simple.

using UnityEngine;
using System.Collections;



/// <summary>
/// Component used to flag that this object is a target.
/// </summary>
public class TargetableBehavior : MonoBehaviour {
    // Target type. Only change this with the correct method
    [Tooltip("Sets the target type to be used at OnEnable. Don't change on inspector at runtime!")]
    [SerializeField] protected TargetTypes m_CurrentType;



    /// <summary>
    /// Current type of this target. At runtime, it is advised to change its
    /// type by using this property.
    /// </summary>
    public TargetTypes CurrentType {
        set {
            if(this.gameObject.activeSelf && this.gameObject.activeInHierarchy) {
                if(m_CurrentType != value) {
                    Unregister();
                    m_CurrentType = value;
                    Register();
                }
            } else {
                m_CurrentType = value;
            }
        }
        get {
            return(m_CurrentType);
        }
    }



    private void OnEnable() {
        Register();
    }



    private void OnDisable() {
        Unregister();
    }



    // Methods for registering on lister
    private void Register() {
        TargetablesLister.Instance.Register(this, m_CurrentType);
    }

    private void Unregister() {
        TargetablesLister.Instance.Unregister(this, m_CurrentType);
    }
}


All that the TargetableBehavior component does is to register itself in the lister when enabled, and unregister when disabled. Beginners to Unity might notice I made a protected member variable, m_CurrentType with the SerializeField attribute. I made the member variable non-public so I, or any other programmer, will not attempt to change it directly on code. This might avoid some bugs in the far future. However, I still want to be able to set it up on Inspector and save its value in prefabs or scene objects. Public variables are serialized and set to appear on Inspector by default, but I need the SerializeField attribute to serialize a protected or private member.

There's a nice property to change the type at runtime. I can't guarantee it is bug proof right now.

Now, to tie up our “automatic targeting system”, we need the TargetFetcherBehavior. This behavior will be stupid and inefficient. Why? Because I am making it solely for testing purposes. I want that different objects have the ability to own a target fetching algorithm that matches its behavior. But I don't have any special object right now, so it doesn't make sense to go around making customized targeting algorithms yet...

using UnityEngine;
using System.Collections;
using System.Collections.Generic;



/// <summary>
/// Dummy component that will be removed later. Here for testing
/// purposes.
/// </summary>
public class TargetFetcherBehavior : MonoBehaviour {
    // Message sent when a target is fetched
    public const string MsgSetTarget = "SetTarget";

    // Message target and options. We don't really need the option here
    // (could use DontRequireReceiver). But lets give this decision power
    // to our designers!
    public GameObject targetFetchedReceiver;
    public SendMessageOptions messageOptions;

    // Current type of this targeter
    public TargetTypes currentType;



    private void Update() {
        List<TargetableBehavior> targetsList = null;
        switch(currentType) {
        case TargetTypes.EnemyShip: 
            targetsList = TargetablesLister.Instance.GetTargets(TargetTypes.PlayerShip); break;
        case TargetTypes.PlayerShip:
            targetsList = TargetablesLister.Instance.GetTargets(TargetTypes.EnemyShip); break;
        }

        if(targetsList != null) {
            // We will simply search for the closest target
            TargetableBehavior currentTarget = null;
            float smallestSqrDist = float.PositiveInfinity;
            Vector3 pos = transform.position; // Small costless optimization

            for(int i = 0; i < targetsList.Count; ++i) {
                Vector2 dist = (Vector2) (pos - targetsList[i].transform.position);
                float sqrDist = dist.sqrMagnitude;
                if(sqrDist < smallestSqrDist) {
                    smallestSqrDist = sqrDist;
                    currentTarget = targetsList[i];
                }
            }

            // Send the target message. Yes, we will set a new target (even
            // if the same) every frame. This could be better, I know, but
            // this is a dummy class that most likely will be deleted later.
            // Or, at least, rewritten. Let me save some effort right now.
            if(currentTarget != null && targetFetchedReceiver != null) {
                targetFetchedReceiver.SendMessage(MsgSetTarget, currentTarget, messageOptions);
            }
        }
    }
}


The TargetFetcherBehavior simply searches for the nearest target available. Every frame. Don't place too many of them in the scene! Anyway, you can see that I added a constant string called MsgSetTarget. My constants starting with “Msg” means that they are messages used with SendMessage or BroadcastMessage. There're a few good things about making them a “named” constant instead of “magic values”, but the most important here is to make it clear to the coder that this object send this message.

Component Orientation works better when the component does not know about other components. The less each component needs to know about others, easier it is to reuse components on different objects. The problem then is how to call methods of other components, that you don't know what they are, or even if they exist? For solving this problem, Unity give us the SendMessage and BroadcastMessage methods. They are a blade that cut to either side, though. Any typo on the message or the receiving method name, and the message won't get through. When a component receives a message, it is very hard to know WHICH GameObject and component sent it – you'll need to use a stack trace and the Debug.Log ability of “flagging” objects (second argument). The MonoDevelop “FindReferences” won't help much, either.

You'll need to come up with small tricks and coding practices to reduce these errors. Things like SendMessageOptions.RequireReceiver for testing behaviors, making messages on code as named constants, and so on. Just like we need to know many “defensive” measures when programming inheritance and design patterns, we will also need to learn quite a few for components orientation.

Now, finally, our targeteer to aim at stuff!

using UnityEngine;
using System.Collections;



/// <summary>
/// Rotates the shortest distance to aim at a given target.
/// </summary>
public class TargeteerFullRotationBehavior : MonoBehaviour {
    // Related messages:
    // MsgSetTarget (in)

    // Targeting
    private TargetableBehavior m_CurrentTarget;
    private Transform m_CurrentTargetTransform; // Small optimization

    // Rotation settings
    [Tooltip("Speed of rotation, in degrees per second")]
    public float rotationSpeed;
    private float m_CurrentAngle;

    // Small optimization
    private Transform m_Transform;



    private void Awake() {
        m_Transform = this.transform;
        m_CurrentAngle = 0.0f;
    }



    private void Update() {
        // Aim at the target
        if(m_CurrentTarget != null) {
            Vector3 distance = m_CurrentTargetTransform.position - m_Transform.position;
            float targetAngle = Mathf.Atan2(distance.y, distance.x) * Mathf.Rad2Deg;
            m_CurrentAngle = Mathf.MoveTowardsAngle(
                m_CurrentAngle, targetAngle, rotationSpeed * Time.deltaTime
            );

            Vector3 currentRotation = m_Transform.eulerAngles;
            currentRotation.z = m_CurrentAngle;
            m_Transform.eulerAngles = currentRotation;
        } else {
            // Simple cleanup
            m_CurrentTargetTransform = null;
        }
    }



    /// <summary>
    /// Sets a target for this targeteer.
    /// [Possible Message]
    /// </summary>
    public void SetTarget(TargetableBehavior newTarget) {
        m_CurrentTarget = newTarget;
        m_CurrentTargetTransform = newTarget.transform;
    }
}


Very simple, right? This component receives a target with a SetTarget message sent by a target fetcher, and then takes the shortest turning angle to aim towards it. You'll see that I cached the targeteer owner transform, and the transform of the target as well. Every time you use “transform” (equivalent of this.transform, or somecomponent.transform), Unity will actually retrieve it as a GetComponent every time. So I just cached them since this is a very simple optimization. Lastly, if you look at the SetTarget method, I “flagged” it as “Possible Message”, so that I think it twice before renaming the method, or changing the parameters.

You can now place a few objects in the scene and move around to see their behavior.


This part got long already, so I'll do no more coding for now. As you can see, we created a simple targeting system that can work on its own. In the next part, we'll make some moving code for the player, as well as for the enemy ships. Again, we will try to make it component based. It will feel like we're writing more code than needed, but soon it will start to be worth it. Meanwhile, you can go on drawing a few nice sprites, heh? Till then!

The source of part two can be downloaded here.

Nenhum comentário:

Postar um comentário