Dev Diaries - Building a JRPG - Part 6

Wow, it's already March 22, 2020, and I haven't written since February 15! It's been over a month, and a ton has happened since then. I really mean it.

COVID-19

First, the whole world has been suffering from the COVID-19 pandemic. The US has just begun experiencing the pandemic just last week. There are many infections in the states now, and lots of places have begun taking precautions by instituting a shelter in-place policy. For those unfamiliar with the term, it really means that it is preferable for citizens to:

  • Practice social distancing by staying at least 6 feet away from each other
  • Stay home as much as possible to not risk infecting others if perhaps asymptomatic, or unknowingly infected

I'd really like to drive home the second point. A lot of young people may be having thoughts in that they won't get infected, or misunderstand that the shelter in-place rule is pointless because we'll all get infected anyway.

That's not the point.

Regardless of what the stance is, the most important thing is that we have to really respect social distancing, and the shelter in-place rules right now.

We do not want to overload hospitals, and cause a stituation where there are not enough healthcare workers to treat those who are sick.

There are high-risk individuals if infected, will need this type of care (e.g., the elderly), because their life will depend on it. So we should be cognizent in staying as healthy as possible, and lowering our own personal risk to flatten out the curve.

I've been following the Johns Hopkins Coronavirus COVID-19 Global Cases dashboards, and I highly recommend it as a resource to see how well we are doing to slow down the spread.

I hope after all this has passed, that we as a society think about how we can improve our public health practices to actually prevent something like this from happening again. It's kind of weird that we're scrambling in this modern age -- especially in country which is said to be the best in the world. 😦

Life

Shelter In Place

That aside, life has been extremely busy. My wife, and I have been working from home due to California's shelter in-place policies. We've been strict with our schedule, keeping an 8-5 as much as possible. Our lifestyle hasn't changed much at all. We actually exercise in our home gym now, and we've always been able to entertain ourselves through reading, video games, and personal projects anyway.

Our routines have been thrown off just slightly when it comes to running errands. Grocery shopping for example, takes a bit longer than usual now. It used to be a quick 30 minute thing, which has now doubled to an hour. Not a huge deal, but I hate wasiting time when it comes to errands.

Things that have bothered me have just been not being able to purchase the necessary groceries when I need them. Normally, I would not mind if I was a guy in a my early-20s. I think my 22 year old self in this pandemic would just be fine with eating ketchup, eggs, and rice all day. Now that I'm a little older, I find myself to be very particular in what I eat.

Luckily when it comes to toilet paper, I seem to be fine here. My wife had told me to restock on toilet paper, and paper towels just before the pandemic, to be ready for our baby. That's a bit of clairvoyance on her part right there.

I'm also a bit annoyed at how some of the elderly aren't able to buy their necessities in life because of all this panic buying! Please buy what you need, everyone. It's not the apocalypse. Grocery stores are still open, ya know. 🍌

Wife's Pregnancy

We're at 34 weeks pregnant now. Wife's been healthy, and we're both excited to meet our daughter in the next few weeks. I've slowly been purchasing the things that we will need to be able to... er... keep a newborn alive?

  • A carseat
  • A stroller
  • Bottles for formula
  • Some newborn diapers

I just need to figure out this baby formula stuff.

I personally didn't account for how some of this stuff can get expensive rather quickly. Strollers are expensive, and they are only meant for a certain age range. For example the one we had bought for a couple hundred dollars is only meant to fit up to a toddler of 1 years old. Hm... So that means 1 year, 11 months, and 30ish days, right? 😆

Something that has been stressing me out lately has been us planning our parental leave -- which will happen in the next coming weeks.

My wife's company doesn't provide a maternity leave program, and instead provides disability insurance in conjunction with the PFL (Paid Family Leave) insuranec which the state provides. This means... lots of paperwork... It's stressful since it's super confusing.

It's a nice reminder that if those working in Big-Tech tend to have it really nice. In the real world, a lot of workers out there don't have the same type of benefits. It's made me more sensitive.

With that, and the shelter in-place rules in effect -- it's going to be an awkward transition going into leave...

On top of that stress, we still need to find the right infant care for our daughter for when she is finally old enough (~3 mo)!

A lot of care centers are closed right now because of the pandemic, and we've had our tours cancelled (rightfully so), so now the timeline to get the right care has been pushed back.

My workplace has told me that they will try to accommodate for this through additional remote work, allowing me to provide the care for our future infant. My wife's company seems to be much more strict when it comes to this, so it makes more sense that I would be the one to ask.

It's funny how America still isn't really up to some of the standards some other countries have with regards to parental leave, and bonding. It's even more shocking when you compare rights of the mother, and her options in the US with other countries.

School

The primary reason why it has taken me so long to write a new post is because of school. A part from the pandemic, and getting ready to be a new dad, I've just got off midterms I did pretty well for the time I had to study. I'm still on track for As. 😄!

I just need to maintain this level of discipline for the rest of the semester (until May).

I still really regret taking 2 classes while trying to work full time. I thought I would be able to manage, but wow... it is killer.

I am definitely going to pace myself better in the future. For the Summer semester, I've committed myself to just taking 1 class, and focusing on my newborn, and balancing my home, work, and personal committments (these series of blog posts included).

I've decided that I want to pursue Computer Graphics more seriously. Since the last time I've written, I have transferred some of the notes I've taken while studying CG into a digital version:

I want to keep these posts top-notch as I want to also teach others the basics of CG as I go along.

I'm currently learning more WebGL at the moment and it's been fun!

Work

Work's been crazy again. Lots of projects to do, and the general load of a start-up. Still loving what I do, and I've been lucky that the pandemic hasn't affected my company as much as others. Very. Lucky.

Fun

Well, my Radeon R9 380 graphics card on my Windows PC died. 😢. I am now displaying it on top of my bookshelf as a memorial piece.

Luckily I still have my old GTX 750 Ti which was the original card my PC had before I had upgraded to the 380. It's a lot slower than my 380, but it can at least drive my 4K Ultrasharp display.

Is this a sign that I may need to upgrade? Hm, I don't know, I don't know... My Xeon 1231v3 CPU from 2014 doesn't feel that slow yet, and I still have plenty of RAM (32 GB DDR3) for modern day standards. Perhaps I'll just buy a new GPU when I truly need it, and ride the machine longer.

I joke that I might need to come up with a grant proposal to my wife in order to get the funds to do the upgrade -- especially with a daughter coming, and finances being a little tigheter due to the economy being affected by the current circumstances of the world.

I joke, but I am very fortunate to be in the situation I am financially. Very fortunate.

Game Dev!

I'd like to warm up with something small while getting back into the swing of things. How about some little features to polish the little Diablo-style game we've been working on so far?

Blink!

Let's make the Goblin flicker whenever we attack! The basic approach is to just create an animation where the sprite will flicker a set number of times within a time frame. (e.g., 5 times per 0.3 seconds)

We'll create a new controller called BlinkController that will receive a SpriteRenderer object, and toggle the forceRenderingOff flag every fixed period of time to create an illusion of flickering of the sprite.

The number of flickers is the number of times a sprite should toggle its rendering in every period of time. I have chosen the number of flickers to be 5 with each flicker occuring every 0.0625 seconds for this example.

If we do the math, then our Goblin should flicker on, and off for a total of 5 * 0.0625 = 0.3125 seconds every time it is attacked.

The Goblin can only be attacked in the battle state, and the trigger of the flicker will happen when the sprite of the monster is clicked.

The flicker should be active until the number of flicker reaches the count limit, which is in this case, 5.

Implementation

For our implementation, I've decided to use a more marketable term, which I will probably ultimately regret down the line... blink. It sounds pretty cool, and less boring than flicker. Ugh, I am so going to regret this, but it sounds so cool! Anyway...

There are three variables to keep track of here:

  1. InBlink - Since the Unity FixedUpdatecalls can happen faster than a single frame, this flag is used to prevent the sprite from flickering in an unpredictable manner. This flag Indicates that the monster sprite is currently in the process of performing the blink routine, and so it should not be operated on.

     

    Note: See https://docs.unity3d.com/ScriptReference/MonoBehaviour.FixedUpdate.html with respect to how often FixedUpdate is called. The default is 0.02 s, but can be overridden.

     

  2. FrameTime - The current cumulative time which has passed since the last flicker. If it reaches the Period, then we reset the counts of flicker to start a new cycle of flickering animation.

  3. Count - The number of completed flickers the monster sprite has performed within the current time period.

Here are the transitions:

  Current forceRenderingoff New forceRenderingOff
0️⃣ ➡️ 1️⃣ Untoggled Toggled
1️⃣ ➡️ 0️⃣ Toggled Untoggled

 

BlinkController

The BlinkController will be a new class in which can be reused.

using UnityEngine;

public class BlinkController
{
    private static int CountLimit = 5;
    private static float Period = 0.0625f;

    public bool InBlink = false;

    private float FrameTime = 0;
    private int Count = 0;
    private SpriteRenderer Renderer;

    public BlinkController(SpriteRenderer spriteRenderer)
    {
        Renderer = spriteRenderer;
    }

    public BlinkController(SpriteRenderer spriteRenderer, int numberOfBlinks, float period)
    {
        Renderer = spriteRenderer;
        CountLimit = numberOfBlinks;
        Period = period;
    }
}

This controller class has 2 constructors which can be called to initialize an instance.

The first constructor takes a single sprite renderer parameter, which in this case will be the SpriteRenderer game component attached to the Goblin. It will then construct the instance to have a default CountLimit of 5 flickers for every Period of 0.0625 seconds.

The second constructor allows the developer to override the number of flickeres, and period each flicker should occur.

This class also has an instance variable InBlink which can be accessed to help determine whether, or not the BlinkController is currently in the process of performing a flickering animation.

Our goal is to expose as little API as possible for consumer of the BlinkController. Ideally, the BlinkController should be easy to use. All flickering animation is managed for the caller. With that being said, it makes sense to just publicly expose a single Tick method which will handle all the tracking of flickers, time tracking, and resetting of all variables.

public void Tick()
{
  Blink();
  FrameTime += Time.fixedDeltaTime;
}

BlinkController::Tick should be called on FixedUpdate for the GameObject which holds the instance of BlinkController.

When BlinkController::Tick is called, an internal method Blink is called to delegate the handling of all the flickering animation required. At each tick, we also need to update the current FrameTime of the controller by the delta time at which we had last invoked the BlinkController::Tick method.

When FrameTime has reached the Period, then it is known that we must "flicker" the sprite.

Given this information, I think we can deduce that a lot of our logic will be in Blink 😉.

What should Blink do?

  1. Determine whether we should even flicker our sprite! Is it the right time to toggle our sprite to render off/on? Have we flickered enough times in the period of time?

We should be able to detect if it is appropriate to toggle the forceRenderingOff flag in our sprite renderer. The conditions which will allow us to toggle is if:

  • Our current time since the last flicker has reached the period limit.
  • If we have not reached our maximum number of flickers.

We can write a helper method ShouldBlink that gives us the result of that:

private bool ShouldBlink()
{
  return FrameTime >= Period && Count < CountLimit;
}
  1. If we're going to flicker, then should we turn off sprite rendering, or should we turn on to create this flicker effect?

Thinking about this, we want to toggle on, and off the SpriteRenderer.forceRenderingoff flag on alternating flickers. The best way to do that is to maintain the Count instance variable, and rely on that as a reference on what to do.

  • For each odd value of Count, we can turn off sprite rendering.
  • For each even value of Count, we can turn on sprite rendering.

Doing this repeatedly in small periods of time will create the "flicker" animation effect we are hoping to achieve. The logic looks something like this:

// We have reached the period, so now we simulate the flicker.
if (Count % 2 == 0)
{
  // disappear!
  Renderer.forceRenderingOff = true;
}
else
{
  // reappear!
  Renderer.forceRenderingOff = false;
}

Count++;

Pretty light, eh? We just need to remember to keep Count up to date by incrementing it after every toggle!

  1. Reset the current FrameTime since we know that we have performed a flicker, and now we will wait for the next period to pass before starting the flicker again.

If we have performed a flicker by toggling SpriteRenderer.forceRenderingoff flag, then that means that we had also reached the period of time to wait for the flicker to happen.

We need to set the BlinkController to "wait" for the next opporunity to flicker again by resetting the FrameTime to 0. From here, each Tick call will increment the value returned by Time.fixedDeltaTime, or essentially the time period which FixedUpdate is called.

FrameTime = 0;
  1. Have we done enough to restart the whole flicker animation cycle?

This is an important check in that we need to reset the state of the BlinkController to a state in which it can perform like it was just initialized. Meaning, it has no idea it is currently performing a flicker, and that it is not waiting to perform a flicker.

This means that we must reset the sprite render to have it always be rendered, specify that the BlinkController is currently not performing a flicker, and the count of flickers, and time passed since the last flicker are 0.

A helper method Reset will help us do all this in a single call.

private void Reset()
{
  Renderer.forceRenderingOff = false;
  InBlink = false;
  FrameTime = 0;
  Count = 0;
}

Another helper, ShouldReset is helpful to allow us to invoke Reset when appropriate. It will just tell us when it is time to Reset the whole controller instance if we have performed the total number of flickers for a single animation.

private bool ShouldReset()
{
	return Count >= CountLimit;
}

Putting all this together, we have our BlinkController class now written like this:

using UnityEngine;

public class BlinkController
{
    private static int CountLimit = 5;
    private static float Period = 0.0625f;

    public bool InBlink = false;

    private float FrameTime = 0;
    private int Count = 0;
    private SpriteRenderer Renderer;

    public BlinkController(SpriteRenderer spriteRenderer)
    {
        Renderer = spriteRenderer;
    }

    public BlinkController(SpriteRenderer spriteRenderer, int numberOfBlinks, float period)
    {
        Renderer = spriteRenderer;
        CountLimit = numberOfBlinks;
        Period = period;
    }

    public void Tick()
    {
        Blink();
        FrameTime += Time.fixedDeltaTime;
    }

    private void Reset()
    {
        Renderer.forceRenderingOff = false;
        InBlink = false;
        FrameTime = 0;
        Count = 0;
    }

    private bool ShouldBlink()
    {
        return FrameTime >= Period && Count < CountLimit;
    }

    private bool ShouldReset()
    {
        return Count >= CountLimit;
    }

    private void Blink()
    {
        // We should not blink if the current total frame time has not
        // reached the time specified in Period
        if (!ShouldBlink())
        {
            return;
        }

        // We have reached the period, so now we simulate the flicker.
        if (Count % 2 == 0)
        {
            // disappear!
            Renderer.forceRenderingOff = true;
        }
        else
        {
            // reappear!
            Renderer.forceRenderingOff = false;
        }

        Count++;

        // Reset the FrameTime to begin tracking of the next blink for the
        // new period.
        FrameTime = 0;

        if (ShouldReset())
            Reset();
    }
}

Using BlinkController

Great! Now that we have our BlinkController written up, and ready to go, it's time to use it in our game!

Let's recall that MonsterCollision was used to manage the transition to battle state, with logic to detect, and handle attack events from the player.

I know it isn't the most ideal solution to make this class even bigger by adding logic to place the BlinkController, and manage flicker here, but I think it's the best place for instant feedback with respect to seeing how our controller performs.

Within MonsterCollision, we can create an instance variable called blinker.

private BlinkController blinker;

When figuring out the best moment to instantiate the blinker object, it is not so obvious.

The temptation is to instantiate it as soon as possible in the MonsterCollision::Start method, but that is not possible. At that point, we may not have the SpriteRenderer associated with the Goblin game object ready yet. This will lead to an exception being thrown.

The next best place to instantiate the blinker object, and as early as possible is when we know that the Goblin game object exists, and thus its SpriteRenderer component as been initialized. This is in our OnCollisionEnter2D handler when we set the ActiveMonster in our game store to be the Goblin game object.

Here, we can reference the SpriteRenderer, and initialize the blinker object with the default values. In pesudo code building upon what we already have, this will look like:

private void OnCollisionEnter2D(Collision2D collision)
{
  var gameStore = GetGameStore();

  // Transition to battle state, and set the active monster

  // Get the monster sprite renderer, and initialize the BlinkController instance
  var monsterSpriteRender
  	= GameObject.Find(GameStore.Get("ActiveMonster").ToString())
  			.GetComponent<SpriteRenderer>();
  		
  blinker = new BlinkController(monsterSpriteRender);

  // Set the BGM
}

We can then update FixedUpdate to call Tick so that the Goblin sprite can flicker when it is appropriate to.

Let's wrap that logic in a helper method called MaybeFlickerMonster.

private void MaybeFlickerMonster()
{
  if (!blinker.InBlink)
  {
  	return;
  }

  blinker.Tick();
}

The blinker will only flicker the active monster (Goblin) if the monster isn't already performing a flicker. Remember, that FixedUpdate can run multiple times before it is appropriate to flicker again, we need to make sure that we don't inadvertently flicker during the FixedUpdate calls. Otherwise, we may not even see a flicker if things are happening too fast!

So, at the end of FixedUpdate we simply just need to call MaybeFlickerMonster:

void FixedUpdate()
{
  if (GameStore.GetGameState() == Jrpg.GameState.GameStateValue.Battle)
  {
    // Handle click event (player is attacking)
    ...
    ...
    ...
    ...
    // End handling click event

    // Create the flicker effect. Let the BlinkController handle whether
    // or not it is appropriate to actually flicker.
    MaybeFlickerMonster();
  }
}

Let's appreciate the fruit of our labor with a quick demo:

Flicker

Soooo beautiful. 🎉!

More Feedback

The flicker is cool, and all, but it isn't enough to indicate that the monster is taking damage. Let's make it better by adding a red tint to the monster when it is flickering. This will let the player know that the monster is taking damage!

We'll need to make some small adjustments to the BlinkController. We will add a Colorinstance variable to keep track of the original color of the SpriteRenderer, and then for each flicker, set the color of the sprite render to be a tint of red.

The red tint should be replaced by the original color within the Reset call, or when the animation cycle is over.

private Color OriginalColor;

// For both constructors, after setting Renderer to be sprite renderer,
// set the original color
public BlinkController(...) {
	OriginalColor = Renderer.color;
}

private void Reset() {
	...
	OriginalColor = Renderer.color;
	...
}

private void Blink() {
  // We should not blink if the current total frame time has not
  // reached the time specified in Period
  if (!ShouldBlink())
  {
    return;
  }
  
  // Set the color of the sprite to be a red tint
  Renderer.color = new Color(1.0f, 0, 0, 0.9f)
  ...
}

Once we have that, we can see that there will be a red tint on the sprite whenever it is attacked:

Flicker Red Tint

Much better! For someone with pretty slow reaction time, and feedback to the environment, this gives me a better indicator that I'm actually doing something to the monster!

Next Time...

The chapter was on the shorter side, and I'm just getting back into the swing of things. Here are some plans I have for the next post:

  • Add BlinkController to a new module in Jrpg.System. Maybe call it Jrpg.Unity?
  • Create enemy AI?

Those two things should be enough. I don't want to over commit myself, given I haven't achieved a lot of my promises in what I would like to get done lately. 😆

See ya next time!