Páginas

terça-feira, 11 de novembro de 2014

Unity and Components Orientation Tutorial - Part Three

This is the third part of this series about Component Orientation and Unity. In part two, I've made a very simple system so that things could automatically aim at anything tagged with a TargetableBehavior component. In this part, I'll make a very simple movement system, and add components that allow the player and AI to move their ships.

The movement I want is very simple. The object can be accelerated and decelerated, and asked to turn facing towards some angle. Of course, there's also attributes like current direction (or angle) and current speed.

To make things nicely separated in components, I will need three behaviors. The first one, and most important one, is a movement behavior. It will receive commands and move objects as needed. This behavior is the basis of our moving things. The other two behaviors the player and AI, that sends movement orders to this movement behavior. Even thought I could use inheritance here and make the player controller and AI inherit from the base movement, I won't do this. With components, it is easier to simply change the movement orders issuer for whatever need I might have (i.e give player control of another ship, changing the AI movement style at runtime, etc).

Naturally, to allow components to communicate with each other without needing to know their existence or types, SendMessage will be needed. As always, since SendMessage takes a string as the message to be sent, it is easy to make invalid code, or break some scripts when we rename methods. This time I'll go a little bit further in safety measures than I did in part two. I'll use C# Interfaces!

/// <summary>
/// Interface needed by moveable objects. Theres not much
/// sense for this Interface, since it is more likely there will
/// be one moveable behavior. The main reason for this interface,
/// in this case, is to make SendMessage less error prone. For
/// example, if we decide to rename or change parameters of these
/// methods.
/// </summary>
public interface IMovable {
    void TurnToAngle(float degrees);
    void Accelerate(float accelerationPower);
    void Decelerate(float decelerationPower);

    float CurrentSpeed { set; get; }
    float CurrentAngle { set; get; }
}

public class MovMsgs {
    public const string TurnToAngle = "TurnToAngle";
    public const string Accelerate  = "Accelerate";
    public const string Decelerate  = "Decelerate";
}


In the script above, there is an Interface that will indicate all the methods needed by any “mover” component that might be done. The class bellow, MovMsgs, simply contains constant strings that are the ones to be used within SendMessage parameters. This way, if I ever need to rename the methods and messages, I'll do it in the Interface and change the related constant strings in the MovMsgs, thus reducing the likelihood of breaking things when bug-fixing or code re-factoring. I modified the “targetables” scripts to use Interfaces as well, but I won't list the code here, for space. You can try it as an exercise or download the source code.

Now, there's another trick I'll use for the movement behaviors. I plan to have a single movement behavior, but I can't be sure that this will always be true in the lifetime of this project. With this in mind, I'll do some “flexibility” code that will make it easier to add more movements in the future. I want all movement behaviors to be seen as the same type. I'll be using the generalization concept of Object Oriented Programming, where child classes are the same type of their parent.

using UnityEngine;
using System.Collections;



/// <summary>
/// Base component for movement behavior. The main reason for
/// this component to be inheritable is that I want that other
/// components can find any type of movement behaviors with a
/// simple GetComponent.
/// </summary>
abstract public class MovementBehaviorBase : MonoBehaviour, IMovable {
    abstract public void TurnToAngle(float degrees);
    abstract public void Accelerate(float accelerationPower);
    abstract public void Decelerate(float decelerationPower);

    abstract public float CurrentSpeed { set; get; }
    abstract public float CurrentAngle { set; get; }

    // Yes, no virtual or abstract methods for Unity methods such as
    // Awake or Update. This class is not meant to hold any behavior
    // or initialization by itself.
}


The MovementBehaviorBase is our model to any movement behavior that might be done. The component itself is abstract, blocking you from instantiating or putting it in GameObjects. Also, the abstract methods need to be defined on child components so, actually, this is a “model” component for future others to follow. If you don't know what abstract classes and methods means, or don't know about inheritance and interfaces, I advise to do some researching. These concepts are quite “big” and would require a some hours of study.

About the movement itself, it is coded on another component:

using UnityEngine;
using System.Collections;



/// <summary>
/// Basic movement simply mave forwards with the "current"
/// angle and speed.
/// </summary>
[AddComponentMenu("Scripts/Movement/Basic Movement Behavior")]
public class BasicMovementBehavior : MovementBehaviorBase {
    // Movement attributes
    [SerializeField] protected float m_MaxSpeed;
    [SerializeField] protected float m_MinSpeed;
    [SerializeField] protected float m_MaxTurnSpeed;
    [SerializeField] protected float m_MaxAccel;
    [SerializeField] protected float m_MaxDecel;

    // Movement variables
    protected float m_CurrentAngle; // Degrees per second
    protected float m_CurrentSpeed; // World units per second

    // Small optimization by caching
    protected Transform m_Transform;
    protected Rigidbody2D m_RigidBody2D;




    public override float CurrentAngle {
        get { return(m_CurrentAngle); }
        set { MoveRotation((m_CurrentAngle = value)); }
    }

    public override float CurrentSpeed {
        get { return(m_CurrentSpeed); }
        set { m_CurrentSpeed = Mathf.Clamp(value, m_MinSpeed, m_MaxSpeed); }
    }



    protected void Awake() {
        m_Transform = this.transform;
        m_RigidBody2D = this.rigidbody2D;
    }



    protected void Update() {
        // Move the object
        Vector3 pos = m_Transform.position;
        pos.x += Mathf.Cos(m_CurrentAngle * Mathf.Deg2Rad) * m_CurrentSpeed * Time.deltaTime;
        pos.y += Mathf.Sin(m_CurrentAngle * Mathf.Deg2Rad) * m_CurrentSpeed * Time.deltaTime;
        MovePosition(pos);
    }



    /// <summary>
    /// Turns the object towards an angle
    /// </summary>
    public override void TurnToAngle(float degrees) {
        m_CurrentAngle = Mathf.MoveTowardsAngle(m_CurrentAngle, degrees, m_MaxTurnSpeed * Time.deltaTime);
        MoveRotation(m_CurrentAngle);
    }



    /// <summary>
    /// Accelerate with the given acceleration power
    /// </summary>
    public override void Accelerate(float accelerationPower) {
        accelerationPower = Mathf.Clamp01(accelerationPower);
        m_CurrentSpeed += accelerationPower * m_MaxAccel * Time.deltaTime;
        m_CurrentSpeed = Mathf.Clamp(m_CurrentSpeed, m_MinSpeed, m_MaxSpeed);
    }



    /// <summary>
    /// Decelerate the specified deceleration power
    /// </summary>
    public override void Decelerate (float decelerationPower) {
        decelerationPower = Mathf.Clamp01(decelerationPower);
        m_CurrentSpeed -= decelerationPower * m_MaxDecel * Time.deltaTime;
        m_CurrentSpeed = Mathf.Clamp(m_CurrentSpeed, m_MinSpeed, m_MaxSpeed);
    }



    protected void MovePosition(Vector3 position) {
        if(m_RigidBody2D != null) {
            m_RigidBody2D.MovePosition((Vector2) position);
        } else {
            m_Transform.position = position;
        }
    }



    protected void MoveRotation(float rotation) {
        if(m_RigidBody2D != null) {
            m_RigidBody2D.MoveRotation(rotation);
        } else {
            Vector3 angles = m_Transform.eulerAngles;
            angles.z = rotation;
            m_Transform.eulerAngles = angles;
        }
    }
}


The code is straightforward and very simple to understand. It allows the object to be turned around, accelerated and decelerated, there's a maximum turning speed, and maximum and minimum movement speeds. Finally, some specialized code to treat rotation and movement differently if there was a RigidBody2D in the object at awake. As a rule of thumb, if your GameObject has one of either RigidBody or RigidBody2D component, you're better off moving it with physics or the rigid body methods MovePosition and MoveRotation, instead of directly changing the transform component of the object. Directly changing the transform in their presence might cause unexpected behavior, wrong or broken physics, and even miss collision detection.

The new stuff introduced in the code above is the AddComponentMenu attribute. It says to Unity where you want your component to be listed in the menus when trying to add it to a GameObject. This is solely to keep things organized, and help other team members (i.e designers) to quickly find components that they need and can use. The right ones, mind you! You should make use of the opportunities to make the non-programmers developers life easier.

The code to move the player is pretty straightforward now, too.

using UnityEngine;
using System.Collections;



/// <summary>
/// Detect input from the player and pass over to a MovementBehaviorBase
/// child instance.
/// </summary>
[AddComponentMenu("Scripts/Movement/Player Movement Controller")]
public class PlayerMovementController : MonoBehaviour {
    protected MovementBehaviorBase m_Movement;



    private void Awake() {
        m_Movement = this.gameObject.GetComponent<MovementBehaviorBase>();
    }



    private void Update() {
        if(m_Movement != null) {
            // TODO : GET RID OF MAGIC VALUES
            if(Input.GetKey(KeyCode.W)) m_Movement.Accelerate(1.0f);
            if(Input.GetKey(KeyCode.S)) m_Movement.Decelerate(1.0f);

            float currentAngle = m_Movement.CurrentAngle;
            if(Input.GetKey(KeyCode.A)) m_Movement.TurnToAngle(currentAngle + 90.0f);
            if(Input.GetKey(KeyCode.D)) m_Movement.TurnToAngle(currentAngle - 90.0f);
        }
    }
}


At Awake, I attempt to find a movement behavior within the same object. I do a component search for MovementBehaviorBase, but any component that inherits it will be returned – and right now, it can only be an instance of BasicMovementBehavior.

On Update I simply check if any movement behavior was found in the Awake and, if true, issue movement orders accordingly to the current user input. I could, of course, have reduced the coupling between the PlayerMovementController and BasicMovementBehavior components and used SendMessages instead, but my desire here was to show that the inheritance works as expected. This is important for components that might need more information about their movement, such as angle and speed (I.e an aiming AI that attempts to shoot where stuff will be, not where they are). To fetch data from other components, its really tricky to do so without a specific reference...

Lastly for this part, a very simple AI movement code for the enemy ships.

using UnityEngine;
using System.Collections;



/// <summary>
/// Simple AI movement behavior that attempts to circle around a target
/// at max speed. Please note that turn speed and max speed will affect
/// the result of this AI, as the movement attributes might or might not
/// make the desired movement possible
/// </summary>
[AddComponentMenu("Scripts/Movement/AI/AI Movement Circle Target Behavior")]
public class AIMovCircleTargetBehavior : MonoBehaviour, ITargeteer {
    public GameObject objectToMove;
    public SendMessageOptions messagesOptions;

    public float minDesiredDistance;
    public float maxDesiredDistance;

    [Tooltip("Position 'in circle' that the AI attempts to reach. " +
     "Different values might result in interesting behaviors")]
    public float angleLookAhead;
    
    private TargetableBehavior m_CurrentTarget;
    private Transform m_TargetTransform; // Small optimization.

    // Small optimization
    private Transform m_Transform;



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



    private void Update() {
        if(m_CurrentTarget != null && objectToMove != null) {
            Vector2 dist = (Vector2) (m_Transform.position - m_TargetTransform.position);
            float angleFromTarget = Mathf.Atan2(dist.y, dist.x) * Mathf.Rad2Deg;
            float desiredDistance = Mathf.Clamp(dist.magnitude, minDesiredDistance, maxDesiredDistance);

            angleFromTarget += angleLookAhead;

            Vector2 desiredPosition = (Vector2) m_TargetTransform.position;
            desiredPosition.x += Mathf.Cos(angleFromTarget * Mathf.Deg2Rad) * desiredDistance;
            desiredPosition.y += Mathf.Sin(angleFromTarget * Mathf.Deg2Rad) * desiredDistance;

            // We want to move to the desired position
            Vector2 moveDist = desiredPosition - (Vector2) m_Transform.position;
            float moveAngle = Mathf.Atan2(moveDist.y, moveDist.x) * Mathf.Rad2Deg;

            objectToMove.SendMessage(MovMsgs.TurnToAngle, moveAngle, messagesOptions);
            objectToMove.SendMessage(MovMsgs.Accelerate, 1.0f, messagesOptions);
        }
    }



    public void SetTarget(TargetableBehavior target) {
        m_CurrentTarget = target;
        if(target != null) {
            m_TargetTransform = target.transform;
        } else {
            m_TargetTransform = null;
        }
    }
}


Now isn't this a plain simple AI? It is, indeed! It will wait for a SetTarget message to define a target object related to the AI movement – right now, it can only be the player. Then it will calculate the current angle and distance from this object to the target, and calculate a desired destination by offsetting a bit the angle either clockwise or counter-clockwise, and defining a min and max distance from the target. The movement itself is a problem to the movement behavior being used!

This AI is so minimalist and simple that the values for an interesting behavior is kind of like finding magic values by trial and error. Try it, and you'll find a few interesting patterns. A little more coding and I could have something boids-like, with emergent behavior. When planning for AI, you can often first try simple things and see how they behave in groups. Emergent “complex” behavior is always interesting.

Enough coding for this part! I did some quick artwork for the ships to make the testing more pleasant. Since now I have a movement behavior that rotates the objects, I need to restructure the player and enemies prefabs. I'll not get in details here, as this is very simple – but you can download the code for this part if you want to see how I did.

In this part I've introduced a way to use SendMessage in a less error prone way. By using C# interfaces and constant strings, I was able to drop the main reasons that I hated SendMessage and refused to use Component Orientation widely, attempting to use Objects Orientation in all possible situations. I'll say again. OOP is not bad. It's not evil. But in a components system, it makes it a lot easier to not choose optimal choices. Both paradigms can work together, but the programmer must not be biased to either one of them, and use the best tool that solves his problems!

So far, the components do still feel kind of meaningless. In the next part, the cannons will be able to shoot some placeholder bullets, concluding the basic framework for the game. From there, in part four and onwards, it's all about making some new behaviors to the existing systems.

The project for this part can be downloaded here.

Nenhum comentário:

Postar um comentário