Dev Diaries - Building a JRPG - Part 3

Alrighty, busy week, busy week... Where to begin?

Work

Well, for starters, I'm finally "done" with the work project I've been working on for the past month! Through lots of back and forth, rewriting, and lapses which made me look incompetent (I assume)... it's all done! Yay! Best of all, I didn't break production upon release. That's always something to celebrate about!

Elmo ceelbration

It makes me sad, or more likely, disappointed that I had a few "brain farts" while working on the project. I personally feel like they could have been avoided if I had taken more time to understand the system I was working with deeper. I think this is something I need to reflect really hard on, and then see what I can do to improve it.

I'm going to probably buckle down and start tracing how each component in the system works as a side project. My company's engineering documentation is in need of more detail, and I feel I can contribute to making it better.

Then, there is that challenge in actually finding time to do all that...

School

I've also kept even busier with school. Computer Graphics is more intense than I had initially estimated. The class has exposed the fact that I've forgotten a lot of the math I learned through my college years. Linear algebra, and vector calculus look so foreign to me now, but it hasn't been too bad in terms of picking it back up again.

I did manage to finish the first assignment for the course. The assignment was to construct a shape using vertex, and fragment shaders in GLSL using WebGL. I created this guy here:

WebGL Sword

My inspiration was Brotherhood from Final Fantasy X.

Brotherhood

I plan to actually introduce a new section in my site which will cover some basics of computer graphics, and the math involved. It would be a good way for me to review what I learn as I progress through this course.

Maybe I'll call it Roger's Online Graphics Notes similar to Paul's Online Math Notes. 😁

My other course, Cloud Computing Applications isn't too bad. It isn't very difficult yet as I have been working in cloud within the industry for a few years now, so I can appreciate the material being taught while at the same time, understand the significance in some of the concepts being presented.

I think that's the difference about being in school after working in a relevant industry for a few years... One gains so much more appreciation towards the topics being taught in lectures. Nothing so far as been "boring". Everything has been very interesting and exciting to me regardless of whether, or not, I am already familiar with the material.

Refurbishing a Gameboy Advance SP

My wife recently destroyed her AGS-101 Gameboy Advance SP. How? I don't know... lol. I am aware that it is probably just due to wear, and tear since she does use it daily!

I decided to give myself a little project, and repair the thing! No, in fact, I decided to refurbish it! I bought one of these guys on Amazon to replace the broken shell.

I must say, I think I did a pretty good job!

Before 0

Before 1

After 1

After 2

Other Responsibilities

HEY! Remember I had a list of these health-related responsibilities I had to take care of? Well, I actually started the process by actually making appointments to get them done!

  • I rescheduled the Doctor's appointment I had missed.
  • I scheduled a teeth cleaning for myself, and my wife for March. She's equally as bad as I am when it comes to being on top of our dental work. At least we brush, and floss twice a day!
  • I refilled my glaucoma drops!
  • Week 26, Day 1 of the pregnancy. Only about 14 weeks to go until the arrival of my daughter. ❤️ I can't wait to have a new player in my party.
  • The home gym has most of the flooring done. Now putting together the dumbbell shelf for... dumbbells... Dang... I need to order the dumbbells too...

Finally, let's get game dev'ing.

Game Dev!

This week we will write code that is necessary to get our character, Cloud moving around the scene. 😄 Once we have that, we'll build out some collision detection between us (Cloud), and the Goblin in view.

To remind ourselves of the goal... the goal is to get some rudimentary battle system working to appreciate the efforts of the labor we had put in to creating our character system.

As one of the business operations manager at my work would say...

Let's get started!

Make Cloud Walk - State Transitions

I think one of the features I appreciate most about Unity is the ease of how I can implement character movement into a scene.

In all the other demos, I did not dive much detail into how animation states tie into the implementation of character movement. I think this is my chance to though!

We currently have the following parameters used in our animation states: WalkingUp, WalkingDown, WalkingLeft, and WalkingRight. These are boolean variables which will indicate the current direction of movement Cloud is performing.

00-walk-parameters.png

Now, let's actually make use of that in code. FIrst, select the Cloud GameObject, and create a new component which is a Script. I am calling my component WalkController. You'll see that a new C# file will be created in the Assets folder.

Creating the script

Double-clicking it should bring up Visual Studio, allowing us to edit the script -- which is essentially a C# class.

Our file is pretty bare at the moment:

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

public class WalkController : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

First, some boiler plate code is needed. We'll need references to the components to update our sprite appropriately:

  • The SpriteRenderer component
  • The RigidBody2D component
  • The Animator component

Also what is needed is a variable to store the previous animation state, and a constant, MaxSpeed to determine how fast Cloud will move on screen.

Here's the code so far:

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

public class WalkController : MonoBehaviour
{
    private SpriteRenderer SpriteRenderer;
    private Rigidbody2D Rigidbody;
    private Animator Animator;

    private static float MaxSpeed = 1.0f;
    private string PreviousState;

    // Start is called before the first frame update
    void Start()
    {
        Rigidbody = GetComponent<Rigidbody2D>();
        Animator = GetComponent<Animator>();
        SpriteRenderer = GetComponent<SpriteRenderer>();
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        
    }
}

We're interested in performing a FixedUpdate rather than an Update.

What is the difference?

The difference between FixedUpdate, and Update in Unity is that FixedUpdate is called when a physics event occurs. Whereas, with Update, it is just called once per frame in the application lifecycle.

FixedUpdate here can be called multiple times per frame. The number of physics steps determines how much FixedUpdate is called. Normally here, the RigidBody2D object will dictate the number of FixedUpdate calls per frame.

Important! Take note on how FixedUpdate is called before Update.

Now let's hook up some input handling! Input.GetKey(..) is an easy way to listen for key events, and acting accordingly. Here's our key map:

Key Function
LeftArrow Left
RightArrow Right
UpArrow Up
DownArrow Down
A Left
D Right
W Up
S Down

The gist of the loop to handle input is like this -- in pseudo-pseudo code:

var keyLeftPressed = Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A);
...

if(keyLeftPressed)
	WalkLeft();
else if(...)

Our handlers will be called WalkLeft, WalkRight, etc. In each of these handlers, we will set the corresponding boolean parameter we had defined in our animation controller. (WalkingLeft, WalkingRight, etc).

We can do that by calling the Animator.SetBool() method. So, if WalkLeft is called, then the code will set the boolean variable, WalkingLeft to true, and all others being false:

    private void WalkLeft()
    {
        Animator.SetBool(DirectionFlagNames.WalkingLeft, true);
        Animator.SetBool(DirectionFlagNames.WalkingRight, false);
        Animator.SetBool(DirectionFlagNames.WalkingUp, false);
        Animator.SetBool(DirectionFlagNames.WalkingDown, false);
    }

I have abstracted the parameter names into string variables for flexibility.

    public class DirectionFlagNames
    {
        public static string WalkingUp = "WalkingUp";
        public static string WalkingDown = "WalkingDown";
        public static string WalkingLeft = "WalkingLeft";
        public static string WalkingRight = "WalkingRight";
    }

What about the idle state? Certainly, you're not always moving, right? We'll need an idle state, and that's just to set all the animation parameters to false.

    private void Idle()
    {
        Animator.SetBool(DirectionFlagNames.WalkingLeft, false);
        Animator.SetBool(DirectionFlagNames.WalkingRight, false);
        Animator.SetBool(DirectionFlagNames.WalkingUp, false);
        Animator.SetBool(DirectionFlagNames.WalkingDown, false);
    }

Once implemented, all this code is immediately demonstrable:

Walk Demo

We can verify with the animation controller, seeing how states are being switched depending on the values of the state parameters:

Flag Demo

Make Cloud Walk - Translation

Well, we can now transition from state to state, but we need to make this more interesting. Let's actually translate the sprite location wthin the scene!

Let's introduce a new method called UpdateVelocity() to update the velocity of the RigidBody component.

    private void UpdateVelocity()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        float newHorizontal = h * MaxSpeed;
        float newVertical = v * MaxSpeed;

        Rigidbody.velocity = new Vector2(newHorizontal, newVertical);
    }

Now, to make sure that Cloud doesn't start drifting away in the screen, we will need to explicitly call the Idlemethod before acting upon a specific direction in FixedUpdate. The check here is to just see if any of the directional input keys are currently being held down, or not. Then we just set the displacement of the RigidBody2D component attached to Cloud as a [0, 0] vector.

        if(!keyLeftPressed && !keyRightPressed && !keyUpPressed && !keyDownPressed)
        {
            Idle();
            Rigidbody.velocity = new Vector2(0, 0);
            return;
        }

Then we can call UpdateVelocity() at the very end of FixedUpdate().

Cloud Walks!

Okay, I think we're making good progress here, but you'll notice something peculiar. After releasing an input key, whether it be left, right, up, or down, Cloud always faces the same direction (down) when idle. This looks pretty awkward.

So, let's fix that by keeping track of the previous state whenever the input key is released, and has transitioned to the idle state. We can then use this state to render out the sprite of this "previous state" through updating it in LateUpdate(). We use the SpriteRenderer component here to set the specific sprite.

In order to do that, let's create some sprite references by declaring public instance variables of Sprite which will reference the "idle direction".

    public Sprite cloudIdleDown;
    public Sprite cloudIdleUp;
    public Sprite cloudIdleLeft;
    public Sprite cloudIdleRight;

Now, in the inspector window for Unity, you'll see that we can set the sprites:

Inspector Sprites

We can now assign the specific sprites we want to display depending on the previous state. All we need to do is drag, and drop the sprite into the slot.

Assigned Sprites

How do we make all this work in code? Well, let's first declare the PreviousState instance variable so that it gets updated whenever we transition to an idle state. Let's call it SetPreviousState().

    private void SetPreviousState()
    {
        var currentState = Animator.GetCurrentAnimatorStateInfo(0);

        if (currentState.IsName(StateNames.CloudUp))
            PreviousState = StateNames.CloudUp;
        else if (currentState.IsName(StateNames.CloudDown))
            PreviousState = StateNames.CloudDown;
        else if (currentState.IsName(StateNames.CloudLeft))
            PreviousState = StateNames.CloudLeft;
        else if (currentState.IsName(StateNames.CloudRight))
            PreviousState = StateNames.CloudRight;
    }

We make use of another internal helper class called StateNames to reference the string names of the animation states we have created.

The SetPreviousState() method is then called in FixedUpdate() whenever we transition to an idle state.

        if(!keyLeftPressed && !keyRightPressed && !keyUpPressed && !keyDownPressed)
        {
            SetPreviousState();
            Idle();
            Rigidbody.velocity = new Vector2(0, 0);
            return;
        }

Now, we actually want the idle versions of the sprites to render whenever we are in the Idle state. How we do this ist hat we use the previous state information in LateUpdate to overwrite the sprite renderer attached to the Cloud GameObject.

    private void LateUpdate()
    {
        var currentState = Animator.GetCurrentAnimatorStateInfo(0);

        if (!currentState.IsName(StateNames.CloudIdle))
            return;

        if (PreviousState.Equals(StateNames.CloudUp))
            SpriteRenderer.sprite = cloudIdleUp;
        else if (PreviousState.Equals(StateNames.CloudDown))
            SpriteRenderer.sprite = cloudIdleDown;
        else if (PreviousState.Equals(StateNames.CloudLeft))
            SpriteRenderer.sprite = cloudIdleLeft;
        else if (PreviousState.Equals(StateNames.CloudRight))
            SpriteRenderer.sprite = cloudIdleRight;
        else
            SpriteRenderer.sprite = cloudIdleDown;
    }

Now, the behavior of our translation looks like this:

Walking improved

I also adjusted the MaxSpeed to also be of value 1.5 to make the walk a bit less clunky feeling.

Phew, that was pretty long, but now we have a character that can move around the scene. Here's what we have written so far in WalkController.cs. You'll see that I have DRY'd some code to make it more concise:

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

public class WalkController : MonoBehaviour
{
    public Sprite cloudIdleDown;
    public Sprite cloudIdleUp;
    public Sprite cloudIdleLeft;
    public Sprite cloudIdleRight;

    private SpriteRenderer SpriteRenderer;
    private Rigidbody2D Rigidbody;
    private Animator Animator;

    private static float MaxSpeed = 1.5f;
    private string PreviousState;

    public class StateNames
    {
        public static string CloudIdle = "CloudIdle";
        public static string CloudUp = "CloudUp";
        public static string CloudDown = "CloudDown";
        public static string CloudLeft = "CloudLeft";
        public static string CloudRight = "CloudRight";
    }

    public class DirectionFlagNames
    {
        public static string WalkingUp = "WalkingUp";
        public static string WalkingDown = "WalkingDown";
        public static string WalkingLeft = "WalkingLeft";
        public static string WalkingRight = "WalkingRight";
    }

    // Start is called before the first frame update
    void Start()
    {
        Rigidbody = GetComponent<Rigidbody2D>();
        Animator = GetComponent<Animator>();
        SpriteRenderer = GetComponent<SpriteRenderer>();
    }

    private void WalkDirection(string flagName)
    {
        Idle();

        Animator.SetBool(flagName, true);
    }

    private void Idle()
    {
        Animator.SetBool(DirectionFlagNames.WalkingLeft, false);
        Animator.SetBool(DirectionFlagNames.WalkingRight, false);
        Animator.SetBool(DirectionFlagNames.WalkingUp, false);
        Animator.SetBool(DirectionFlagNames.WalkingDown, false);
    }

    private void SetPreviousState()
    {
        var currentState = Animator.GetCurrentAnimatorStateInfo(0);

        if (currentState.IsName(StateNames.CloudUp))
            PreviousState = StateNames.CloudUp;
        else if (currentState.IsName(StateNames.CloudDown))
            PreviousState = StateNames.CloudDown;
        else if (currentState.IsName(StateNames.CloudLeft))
            PreviousState = StateNames.CloudLeft;
        else if (currentState.IsName(StateNames.CloudRight))
            PreviousState = StateNames.CloudRight;
    }

    private void UpdateVelocity()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        float newHorizontal = h * MaxSpeed;
        float newVertical = v * MaxSpeed;

        Rigidbody.velocity = new Vector2(newHorizontal, newVertical);
    }

    private void LateUpdate()
    {
        var currentState = Animator.GetCurrentAnimatorStateInfo(0);

        if (!currentState.IsName(StateNames.CloudIdle))
            return;

        if (PreviousState.Equals(StateNames.CloudUp))
            SpriteRenderer.sprite = cloudIdleUp;
        else if (PreviousState.Equals(StateNames.CloudDown))
            SpriteRenderer.sprite = cloudIdleDown;
        else if (PreviousState.Equals(StateNames.CloudLeft))
            SpriteRenderer.sprite = cloudIdleLeft;
        else if (PreviousState.Equals(StateNames.CloudRight))
            SpriteRenderer.sprite = cloudIdleRight;
        else
            SpriteRenderer.sprite = cloudIdleDown;
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        var keyLeftPressed = Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A);
        var keyRightPressed = Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D);
        var keyUpPressed = Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W);
        var keyDownPressed = Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S);

        if(!keyLeftPressed && !keyRightPressed && !keyUpPressed && !keyDownPressed)
        {
            SetPreviousState();
            Idle();
            Rigidbody.velocity = new Vector2(0, 0);
            return;
        }

        if (keyLeftPressed)
            WalkDirection(DirectionFlagNames.WalkingLeft);
        else if (keyRightPressed)
            WalkDirection(DirectionFlagNames.WalkingRight);
        else if (keyUpPressed)
            WalkDirection(DirectionFlagNames.WalkingUp);
        else if (keyDownPressed)
            WalkDirection(DirectionFlagNames.WalkingDown);

        UpdateVelocity();
    }
}

Adding the Goblin Enemy and Collision Detection

Simply add the Goblin GameObject into the scene. For more of an aesthetic look, we'll also add a couple of components to the Goblin GameObject:

  • Animator

    • This will animate the Goblin in a in-place-still manner (walking down in place)
  • Box Collider 2D

    • For collision detection between it and Cloud.

We'll also need to add a Box Collider 2D onto Cloud.

I'll leave it up to you on how to animate the Goblin in a stand-still way. 😄 Now, let's actually modify the Box Collider 2D so that Cloud is not walking through the Goblin.

Walking through the goblin

Edit both game objects so that the box colliders wrap around just enough of the sprite to collid with each other.

Box Collision

Do the same for Cloud, and now we have basic collision detection within our scene!

Collision fixed

I'm not going to document it here, but I've also gone ahead and added collision detection for the water by simply adding Box Collider 2D components in auxiliary game objects.

Stopping Point

Keeping this fun, I'm going to stop here this week. Next week, let's finally add some logic to build out a basic battle sequence between Cloud, and Goblin. Wow, we're on our way to a Rogue-like!