Friday, December 20, 2013

Embedding sprites in a dfLabel with DFGUI

One of DFGUI's cool features is the ability to inline-embed images in a dfLabel to avoid having separate sprites layered on top of the dfLabel.


To embed sprites in a dfLabel, you must first make sure that the Process Markup checkbox (Label Properties->Formatting) is ticked.

Then, embedding sprites is done using the sprite tag: Press [sprite "xbox_a"] to Select

The sprite that is specified in the sprite tag (in my case it's "xbox_a") is fetched from the Atlas that is specified on the dfLabel.

Friday, December 13, 2013

A 2D Freeform Directional Blend Tree for locomotion in Unity 3D

This video demonstrates a root-motion driven, 2D Freeform Directional Blend Tree for locomotive states in Unity 3D.

Using such a blend tree, a character can aim and walk in different directions independently and simultaneously.


With a gamepad, I'm moving the character with the left stick and rotating with the right stick.

I am using a total of 9 animations for the blend tree. Four for each axis and the middle one for idle:
  1. Idle
  2. Forward Walk
  3. Backward Walk
  4. Left Strafe Walk
  5. Right Strafe Walk
  6. Forward Run
  7. Backward Run
  8. Left Strafe Run
  9. Right Strafe Run

Here's a closer look at the blend tree and its motions:


This locomotive blend tree exists on an Animator Layer called LowerBody which has has an Avatar Mask applied with only the legs selected (including IK). By having layers representing different sections of the body, I can combine upper-body animations with lower-body animations. In my case, the character can walk/run and aim at the same time using a single animation for AIMING (which exists on the upper-body layer).

This is the Avatar Mask I use on the Lower-Body layer on which the locomotion blend tree exists:


I am controlling the blend tree with two parameters representing a direction vector: VelX and VelZ.

This direction vector determines the locomotive state of the character i.e. which direction his legs should be moving.

  • If (VelX, VelZ) is (0, 1), the character runs forward in a straight line.
  • If (VelX, VelZ) is (-1, 0.5), the character walks forward while strafing to the left.
  • If (VelX, VelZ) is (0.5, -1), the character runs backwards while slightly strafing to the right
  • etc...

This is the gist of the code which both a) sets the (VelX, VelZ) direction vector [Left Stick] i.e. LOCOMOTION and b) rotates the model to aim [Right Stick] i.e ROTATION:

private Vector3 lastLeftStickInputAxis;  // stores the axis input from the left stick between frames

private void Update()
{
    /* START LOCOMOTION */

    // Get the axis from the left stick (a Vector2 with the left stick's direction)
    var leftStickInputAxis = inputManager.LeftAxis;
    
    // Get the angle between the the direction the model is facing and the input axis vector
    var a = SignedAngle(new Vector3(leftStickInputAxis.x, 0, leftStickInputAxis.y), model.transform.forward);
    
    // Normalize the angle
    if (a < 0)
    {
        a *= -1;
    }
    else
    {
        a = 360 - a;
    }
    
    // Take into consideration the angle of the camera
    a += Camera.main.transform.eulerAngles.y;

    var aRad = Mathf.Deg2Rad*a; // degrees to radians
    
    // If there is some form of input, calculate the new axis relative to the rotation of the model
    if (leftStickInputAxis.x != 0 || leftStickInputAxis.y != 0)
    {
        leftStickInputAxis = new Vector2(Mathf.Sin(aRad), Mathf.Cos(aRad));
    }
    
    float xVelocity = 0f, yVelocity = 0f;
    float smoothTime = 0.05f;

    // Interpolate between the input axis from the last frame and the new input axis we calculated
    leftStickInputAxis = new Vector2(Mathf.SmoothDamp(lastLeftStickInputAxis.x, leftStickInputAxis.x, ref xVelocity, smoothTime), Mathf.SmoothDamp(lastLeftStickInputAxis.y, leftStickInputAxis.y, ref yVelocity, smoothTime));
    
    // Update the Animator with our values so that the blend tree updates
    animator.SetFloat("VelX", leftStickInputAxis.x);
    animator.SetFloat("VelZ", leftStickInputAxis.y);
    
    lastLeftStickInputAxis = leftStickInputAxis;

    /* END LOCOMOTION */



    /* START ROTATION */

    // Get the axis from the right stick (a Vector2 with the right stick's direction)
    var rightStickInputAxis = inputManager.RightAxis; 
    if (rightStickInputAxis.x != 0 || rightStickInputAxis.y != 0)
    {
        float angle2 = 0;
        if (rightStickInputAxis.x != 0 || rightStickInputAxis.y != 0)
        {
            angle2 = Mathf.Atan2(rightStickInputAxis.x, rightStickInputAxis.y)*Mathf.Rad2Deg;
            if (angle2 < 0)
            {
                angle2 = 360 + angle2;
            }
        }
    
        // Calculate the new rotation for the model and apply it
        var rotationTo = Quaternion.Euler(0, angle2 + Camera.main.transform.eulerAngles.y, 0);
        model.transform.rotation = Quaternion.Slerp(model.transform.rotation, rotationTo, Time.deltaTime*10);
    }

    /* END ROTATION */
}

private float SignedAngle(Vector3 a, Vector3 b)
{
    return Vector3.Angle(a, b) * Mathf.Sign(Vector3.Cross(a, b).y);
}

The soldier model and animations are from Mixamo.

Thursday, December 12, 2013

Keeping track of cooldowns with a helper class in Unity 3D

For my current project, I wrote a simple class to keep track of my cooldowns.

So for example, my character can fire every 2 seconds, jump every 4 seconds and sprint every 3.142 seconds; and this class takes care of the cooldown timers for me.

Here's how I use it:

public class Player : MonoBehaviour 
{
    private CooldownTimer firingCooldown;
    private CooldownTimer sprintCooldown;

    private void Awake() 
    {
        firingCooldown = new CooldownTimer(2f); // the player can fire every 2 seconds
        sprintCooldown = new CooldownTimer(3.142f); // the player can sprint every 3.142 seconds
    }

    private void Update() 
    {
        if (Input.GetButtonDown("Fire") && firingCooldown.CanWeDoAction()) 
        {
            firingCooldown.UpdateActionTime();
            // Do firing logic            
        }

        if (Input.GetButtonDown("Sprint") && sprintCooldown.CanWeDoAction()) 
        {
            sprintCooldown.UpdateActionTime();
            // Do sprinting logic            
        }
    }
}

And this here's the CooldownTimer class:

using System;

public class CooldownTimer
{
    private float? LastActionDone { get; set; }
    private float IntervalSeconds { get; set; }

    public CooldownTimer(float intervalSeconds)
    {
        IntervalSeconds = intervalSeconds;
    }

    public void UpdateActionTime()
    {
        LastActionDone = Time.time;
    }

    public bool CanWeDoAction()
    {
        var canWeDoAction = true;
        if (LastActionDone.HasValue)
        {
            var secondsSinceLastAction = Time.time - LastActionDone;
            canWeDoAction = secondsSinceLastAction > IntervalSeconds;
        }

        return canWeDoAction;
    }
}

Update 1

With a tip from Petr Kolda (Facebook), I've now modified the class to use Time.time instead of DateTime.Now.

Wednesday, December 11, 2013

Opening multiple instances of Unity 3D

To open multiple instances of Unity 3D, go to Edit > Preferences, and from there tick the "Always Show Project Wizard" checkbox:

Pixel Perfect sprites in Unity 2D

In Unity 4.3, native 2D tools were introduced in Unity 3D. So, armed with a bunch of 1024x768 sprites from an old game I wrote, I decided to try out the new 2D tools by trying to port the old game to Unity 2D.

But the first issue I encountered when dragging in my first sprite to the scene was this:


Notice how even though my game resolution is set to 1024x768 (top corner) and my water image is 1024x768 and centered on the screen, the sprite (SpriteRenderer) doesn't fill the screen as it should.

The way to fix this is by changing the Size of your orthographic Camera.

To calculate the Size for your Camera, use this: Camera Size = (GameTargetHeight / 2) / PixelsToUnits

By default, Pixels To Units is set to 100.

So, since my game targets a resolution of 1024x768, my Camera Size would be: (768/2)/100 = 3.84


And as you can see, that fixes the problem beautifully: