Dev Diaries - Building a JRPG - Part 7

Hello, everyone! I'm currently on paternity leave, and staying busy with my new daughter. I'm also studying for finals for Spring 2020 at UIUC.

Emerging

It has been a couple of months already, but I'd like to give an update on what's been happening since then, and then at the end we'll continue off with the JRPG project. Does that sound good? No? Don't want to read, and just code?

Okay, then click here!

Riley

Riley

My daughter, Riley was born April 29, 2020 at 9:08 AM. She came to this world at 19", 6 pounds, and 8 ounces. I am now a proud, and lucky father! ☘️ Riley came to this world a little earlier than expected -- right at 39 weeks, and 5 days.

Here's a summary on what went down happened:

  • 1:00 AM - My wife had suddenly started feeling intense contractions early this morning. At about 1:30 AM, I heard her groaning and asked if she was okay, and if she thought she was going into labor. She had the feeling that she was indeed going into labor, but being the type to hate wasting effort and time, she told me not to take her to the hospital yet. Apparently she was in fear that our 25 minute drive to Palo Alto (the hospital) would be a waste of time if we were to get sent home. Seriously.
  • 2:00 AM - My wife still didn't stop the groaning. At this point, I couldn't go back to sleep anymore, so I asked if she wanted to get up and start our day earlier so that she can get a sense whether or not, she was ready to go into labor. We did. She decided to shower, put on make-up, eat breakfast, and finally start packing for the hospital.
  • 3:00 AM - We had breakfast. It was odd watching her eat in-between contractions. Our breakfast conversations would be paused with her saying "BRB", followed by meditative breathing.
  • 3:45 AM - Wife could not handle the contractions anymore, so she makes a phone call to the hospital. We finally start making our way there.
  • 4:30 AM - We arrive at the hospital. I asked my wife if she would like me to grab a wheelchair so that I can push her to the lobby. She looks at me, waves her hand with a resounding "NO!"... and proceeds to walk across the parking lot to the lobby while clutching her belly. ಠ_ಠ
  • 5:00 AM - We are checked into the hospital, and her contractions were getting worse, but amazingly, my wife has kept her cool all this time. Speaking to the nurses casually, describing her symptoms and repeatedly telling the nurses that she did not want any pain killers (epidurals) during the process.
  • 5:30 AM - After a test to see the dilation, we find that my wife is almost 6cm dilated. The nurse tells us we're going to have the baby today. She also told us that by now, most women would be wanting some epidural to relieve their pain. My wife responds with "oh, really?" -- casually. Seriously. The nurse steps out, and yet she still has the cool to thank the nurse for the help. Me? ಠ_ಠ
  • 6:00 AM - Wife gets a COVID-19 test. Not a great experience.
  • 7:30 AM - Wife gets checked into a room. At this point she is 9cm dilated. Almost there! At this point she has stopped talking to everyone, and is just meditatively breathing. My role here was to be the body-language translator for my wife to the nurses. I was told by her after the fact that I did a fantastic job here. ⭐️
  • 8:00 AM - Wife is wanting to push now, but the Doctor hasn't made it to the office yet, and she is only 9.5cm dilated.
  • 8:30 AM - Doctor finally gets in, and begins setting up the area for labor. My wife at this point has not said anything to the new nurses that have came in on shift, and to the Doctor. I still serve as the body-language translator.
  • 9:00 AM - Doctor is finally ready to give my wife a set of directions on what to do since now at this point, she is fully dilated. The Doctor tells my wife that to push properly, she must think of it as a PR for a bench press. When I heard this, I was ecstatic. In my mind I was like "Oh, then she's got this..." (Spoiler alert -- I was right.)
  • 9:02 AM - My wife begins pushing. At this point there was no time to set up the bed for pushing. So the nurse, and I just held onto my wife's leg as she seemingly looks like she's about to squat 300 pounds.
  • 9:08 AM - My wife finally says something! F:#️⃣#️⃣ K!!!. Bloop! Out comes Riley! That was quick! ❤️

We stayed at the hospital for a day. We left the next day at noon. Both my wife, and Riley were extremely well, and that's all I could have hoped for that day. 😄

As of now, I'm spending most of my days in between studying, and being sheltered in place due to COVID-19, just bonding with my new family.

My little girl can be terrifying at times, because she can explode with a huge cry any moment at night, but hey who can blame her? She just spent the last 9 months in her version of paradise -- warm, dark, and constantly being fed through a tube without needing to expend any calories sucking on a nipple to get nutrition. That's the life. Compared to the real world, that's like going back to your parents to visit and not having anymore freedom. 😆

One more thing, we have a baby registry in Amazon, and for those who have purchased something for us -- I'd like to thank you all for contributing. It's been crazy during the whole pandemic situation where essentials have been extremely hard to find -- wipes, diapers, baby formula, etc. It's been tough. I personally started gathering up baby formula in March, when shelter in place took effect. It might not have been enough through. So, thank you all! 😊

COVID-19

Wow, how scary has all this been? I would have never imagined myself being able to live through a pandemic prior to this all even happening. Of course I have read stories in history books about the Spanish flu pandemic of 1918, but it's strange to experience one that is analogous to it in this present day.

Every day, I just hope for everyone to just start getting healthy again, but that's not reality. Shelter in place is still in effect as of May, 2020 in Northern California, but some rules have been relaxed. I just hope that the disease doesn't start spreading aggressively now that some businesses are being allowed to be reopened.

Now that I'm a father to a very young child, I really appreciate it that everyone does their best in keeping the public health of our communities well.

The scariest part is that I believe that all our lives will be difference once the curve begins to flatten. I'm sure we'll be wanting less physical contact with one another going forward. I even think that we'll also have thoughts whether or not having someone deliver our food is sick, or not.

Things about health that we used to never think about will become a daily thought in the future. I just hope one day that we'll all be able to relax at our local park, and watch fireworks in the Fourth of July as a community without having to be fearful of one another with respect to being sick.

School

The Spring 2020 semester is finally wrapping up at UIUC. I learned something new this past semester. Taking 2 graduate level courses while holding a full time job at a start-up is definitely not the most intelligent thing for me to do. 🦆

By now, I'm feeling quite burnt out, and although I will likely finish the semester with strong grades, I really gassed myself out at the end. Just before taking my paternity leave, work had become 5x harder because of diminished energy levels. I lucked out this time in that I was able to at least maintain the same level of effort in studying I had in the beginning of the semester, while staying productive at work.

The price of all this was that I was not able to find time for my own hobbies. 😦 Neglecting the blog, not doing any game development, playing video games, and not reading for fun has been pushed to the backseat for these more important priorities.

So, now I have a rule for myself. Going forward, until the end of my program, I am going to take only 1 course per semester until my graduation. This summer, I plan to take Data Visualization -- which I am excited for.

For my CG class, I was able to complete a pretty big project involving shading a Utah Teapot using Phong, reflective, and refractive shading within a skybox. I am really proud of it.

Reflected Teapot

Check it out here: Shaded Teapot in WebGL

Should I get a New Computer?

I've been doing more, and more development in Windows 10 recently. It's not bad. The last time I used Windows extensively was Windows 7, back in college. I guess I missed all the pain points in earlier Win10 releases, because I actually think it is a pretty solid OS.

Anyway, I have found that Unity runs much faster on Windows than on Mac. It might be the primary platform I do my game development on now. My 2014 desktop runs Unity3D slightly faster than my 2019 Macbook Pro. What!

To get more speed, I'm thinking about upgrading my desktop... So, this means that I might be in the market for a new desktop computer soon. The system configuration I'm desiring is something which should be able to do a lot of computer graphics work, game development light gaming, and general web development.

My budget is fixed at about $1,500, and here are some of the requirements I'm looking for in a workstation:

DescriptionValueDetail
Form FactorSmall form factorI would like something that can sit on my desk. Ideally something which I can place my 27" monitor on top of.
CPU4-8 CoresAMD, or Intel, it does not matter. In the modern day, I feel that CPUs tend to be "fast enough". More cores is something I would prefer over the brand, or SKU.

I don't plan to overclock.
GPUDiscrete which can drive a 4K resolution monitorNo preference between AMD, or Nvidia. I would like something powerful enough to do CG programming, game development, and some 3D gaming. CG programming will mostly be in OpenGL, and game development will be mostly through Unity3D. I am not a hardcore PC gamer, so a fast gaming card isn't necessary.
RAM32 GBMy current desktop from 2014 has always had 32 GB of RAM. I cannot imagine working with any less as that would be a down-grade!
Storage500 GB+ SSDNVMe is ideal, but I can live with slower interfaces. The most important thing here is that I must have an SSD. 😄
Network I hate wires. Wireless 802.11ac, or a WiFi 6 compatible adapter is fast enough to do network file transfers from my home file server.
Operating SystemWindows 10 ProI have a Mac already, and I don't use Linux much.

I already have a 4K monitor, wireless keyboard, and mouse, so most of the budget should be going into the best PC possible!

You may be wondering why I haven't decided to get a Mac desktop. I have no need for a Mac desktop since I already use a 13" MacBook Pro as my main machine. I split my time between Mac, and Windows at about 70/30, so I don't need the Windows desktop to be extremely powerful. Form factor with being able to fit a discrete GPU is more important to me. 😄

Which is cheaper nowadays? Building, or buying? I am currently looking at a Dell Precision 3431, but it is looking like the configuration I want is going to break the bank. 💰 Anyway, there no rush for this as it is not a huge priority. It's fun to spec out the components though, but for now my GeForce 750 Ti, and Haswell Xeon will hold over. 😉

Self Studying

Here are a few things I'm doing to keep my mind sharp, and constantly improve:

CGPP

  • Learn more computer graphics. I'm currently reading through Computer Graphics - Principles and Practice, 3rd Ed. As I am doing this, I am writing small CG-related programs, and tools to reinforce my knowledge.
  • Brush up my math. I am currently refreshing my calculus using the Larson Calculus textbook.
  • Finish reading Radical Candor. I started this book a few months ago, but never got around to finishing it.

Gaming

Some gaming here and there, but nothing worthwhile mentioning. 🐸 Life's been pretty busy recently. 😉

But here's my current list of games I'm playing:

  • Final Fantasy 7 (Switch Remastered)
  • Final Fantasy X (with Wife -- Switch Remastered)

I dropped playing Myst, because I just honestly feel like that game has not aged well. I know, I'll be getting a lot criticism for saying that!

Game Development

Finally, it's the section you're finally here for, right? This blog is about my adventures in game development, after all.

Here is the agenda for today:

  1. We will add BlinkController to a new module in the Jrpg.System framework. A new library called Jrpg.Unity will be created to contain all related Unity-dependent components.
  2. We will enhance our existing demo from Part 6, and add some enemy AI to counter the player's attacks when in battle.

Jrpg.Unity

BlinkController is going to be useful for multiple sprite renderers belonging to other GameObjects. It is better to have the BlinkController be reusable. Therefore, we will add BlinkController to our Jrpg.System framework , specifically a new module within the solution -- Jrpg.Unity.

Jrpg.Unity will be our library which will contain all Unity related code in which will be reusable for future projects.

  • Create a new project Jrpg.Unity within the Jrpg.System framework. This will be a .NET Standard 2.0 library. The first file we will create is BlinkController.cs. This will be a direct port of what we have already written in our Unity demo.

Unity Library

  • We can then compile the Jrpg.Unity library, and add it to our Unity project.
  • The BlinkController.cs script is no longer needed within our Assets folder, so we can remove that.
  • Now, we can modify MonsterCollision.cs in our game to import the Jrpg.Unity library.

Now, BlinkController will reference the class found within Jrpg.Unity transparently when we declare using Jrpg.unity; at the top of the MonsterCollision.cs file.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Jrpg.Unity;

public class MonsterCollision : MonoBehaviour
{
	...
}

Enemy "AI"

Note that I am using the term "AI" loosely here in the context of the enemy being able to make decisions on what actions to take in battle against the player. As of now, our battles are pretty boring in the demo. The Goblin is too passive, and only takes damage from the player. That's too easy! It would be awesome if our enemy, the Goblin, actually did something while in battle.

Enemy Techniques

Our enemies currently have a class associated with them. Since the EnemyClass object inherits BaseCharacterClass, we get a few things for free. The most noteworthy feature is that a number of techniques which can be defined for a particular enemy object.

Taking advantage of this, we can add a few techniques to our Goblin so that it can attack the player, Cloud.

To keep things simple, I think the Goblin should have two variations of attacking.

  1. The first, being a standard melee attack. This is not a technique.
  2. The second should be a technique. Since we are keeping things simple, let's make this a special melee type of attack. A technique which is simple called Bite will do. It will simply be a stronger melee attack from the Goblin.

In summary, the Goblin will have several actions which it can take in battle:

  • Perform a standard melee attack.
  • Perform a technique. In this case, it is the special Bite technique.
  • Miss

We now need to define the technique for our game. Within EnemyClasses.json, we will define Bite in the Goblin entry located within the Unity project.

      "Techniques": [
        {
          "Level": 1,
          "Name":  "Bite"
        }
      ],

We will also need to add it to Techniques.json for the correct class to be loaded through .NET Reflection. The Agent here will contain the class name for which the technique will reference.

[
  {
    "id": "Tech_Omnislash",
    "DisplayName": "Omnislash",
    "MpCost": 5,
    "AttackPower": 30,
    "MagicPower": 0,
    "Agent": "Omnislash, Assembly-CSharp"
  },
  {
    "id": "Tech_Bite",
    "DisplayName": "Bite",
    "MpCost": 10,
    "AttackPower": 20,
    "MagicPower": 0,
    "Agent":  "Bite, Assembly-CSharp"
  }
]

For now, I'm not really strict on the numbers here -- the goal is to just get something working as quickly as possible to test, so I will use the mostly the same data as Omnislash. 😄

Next let's create a script file which will implement the Bite attack. Create Bite.cs script in Unity, and have it inherit the Technique class found in Jrpg.CharacterSystem.

using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.CharacterSystem.StatusEffects;
using UnityEngine;

public class Bite : Technique
{
    public Bite(StatusEffectManager statusEffectManager, TechniqueDefinition definition)
        : base(statusEffectManager, definition)
    {
    }

    public override void Perform(Character source, List<Character> targets)
    {
        if(source.Statistics[StatisticType.MpCurrent].CurrentValue < Definition.MpCost)
        {
            Debug.Log($"{source.Name} does not have enough MP to perform this technique.");

            return;
        }

        // Bite only works on a single character, so assume that to be the first
        // character in targets.
        var target = targets[0];
        var damage = (int)(source.Statistics[StatisticType.Strength].CurrentValue * 0.5);

        target.Statistics[StatisticType.HpCurrent].CurrentValue -= damage;

        source.Statistics[StatisticType.MpCurrent].CurrentValue -= Definition.MpCost;

        Debug.Log($"{source.Name} used {Definition.DisplayName} on {target.Name} and inflicted {damage} HP.");
    }
}

Since Bite inherits Technique, we will need to override the Perform method. Since this type of attack is only applicable to a single target, we can just assume that the target is the first element in our targets list.

The implementation for Bite isn't that important, as long as we are able to change the state of the HP in the target, just having some sort of damage calculation is good enough.

Real-Time Combat

How do we allow our Goblin to attack us? It is a question of whether we want a turn-based combat, or a real-time combat system. To make things interesting, let's make our combat system real-time, similar to how it would be in Diablo.

The Goblin can attack at any moment during the Battle state. However, we must not program it so that it attacks the player too often. We will program the Goblin to attack every 2-4 seconds.

A random number generator will be written to generate the time period needed for the next attack from the Goblin (e.g. 2.39 seconds).

Then we can choose the action which the Goblin will perform based on a weighted random number generator. Of course, the melee attack will have the highest chance to occur.

Something like this sounds good for now:

  • 60% chance melee attack strikes successfully
  • 25% chance the Bite attack strikes successfully
  • 15% chance that the Goblin misses for the turn

The Goblin should be able to attack our character too. So we need to make Cloud be valid target for enemies. I'm still debating whether or not melee attacks should be a framework-based thing, or specific to the game. For now, let's just make it specific to the game, and make Cloud.cs script have the MeleeAttack method, just like the GoblinCharacter class.

Let's also take this opportunity to do some minor refactoring. Since are now needing IsAlive for characters beyond the enemy, we should move the method from Enemy.cs found in Jrpg.BattleSystem to the base character Character.cs class in Jrpg.CharacterSystem.

The MeleeAttack method for Cloud.cs, is more or less the same as the one in GoblinCharacter.cs, but now in the context of the playable character.

    public void MeleeAttack(Character source)
    {
        if(character.IsAlive())
        {
            var sourceAttack = (int)(source.Statistics[StatisticType.Attack].CurrentValue);
            character.Statistics[StatisticType.HpCurrent].CurrentValue -= sourceAttack;
            Debug.Log($"The {source.Name} attacked {character.Name} for {sourceAttack} damage! {character.Name} now has {character.Statistics[StatisticType.HpCurrent].CurrentValue} HP");

            if (character.Statistics[StatisticType.HpCurrent].CurrentValue <= 0)
            {
                Debug.Log($"{character.Name} has fallen.");
            }
        }
    }

Okay how do we know if this works? Well, we can just have to Goblin counter attack our character whenever we attack the Goblin. This is temporary, and will be removed once we develop a system to have the Goblin attack in real-time. We are just interested in seeing our melee attacks working when Goblin attacks Cloud.

Attack

Bite

Within our MonsterCollision::FixedUpdate method, we can hook up this code to "counter-attack" the player whenever the player has used Cloud to attack the Goblin.


                monster.MeleeAttack(character.GetCharacter());
                character.MeleeAttack(monster.GetCharacter());
                Character monsterCharacter = monster.GetCharacter();
                monsterCharacter.UseTechnique(new TechniqueName("Bite"), GameStore.statusEffectManager, new List<Character> {
                    character.GetCharacter()
                });

Now that we have verified that everything is working through Debug.Log, it makes sense to try to make this real-time now. The next step is to come up with a way for the Goblin to attack the player in an unpredictable manner.

First, we will make a random number generator which we can place in our framework. Within Jrpg.System, we create a RandomDouble class that will generate a floating point number within a specified range.

using System;

namespace Jrpg.System
{
    public class RandomDouble
    {
        public static double Get(double min, double max)
        {
            return (new Random((int)DateTime.Now.Ticks).NextDouble() * (max - min)) + min;
        }
    }
}

Here is a quick test class to make sure we do generate numbers in range:

using System;
using Xunit;

namespace Jrpg.System.Tests
{
    public class TestRandomDouble
    {
        [Fact]
        public void TestGetDouble()
        {
            double d = RandomDouble.Get(0, 1);
            Assert.True(d <= 1.0);
            Assert.True(d >= 0.0);

            Console.WriteLine("Random double " + d);

            d = RandomDouble.Get(2.0, 2.6);
            Assert.True(d <= 2.6);
            Assert.True(d >= 2.0);

            Console.WriteLine("Random double " + d);
        }
    }
}

Now, we are ready to use this in our demo. We will first need to generate a time interval for when the Goblin should attack when in the Battle state. Let's make it anywhere between 2 to 4 seconds.

We should have a state variable in MonsterCollision called AttackPeriod which will be a counter of the current time period for when the Goblin should next attack the player. The AttackPeriod is then the random value generated from 2 to 4, in seconds.

For each FixedUpdate call in MonsterCollision, we subtract Time.fixedDeltaTime from AttackPeriod. When AttackPeriod is at, or below 0, we will have the Goblin attack the player. We also generate a new period and assign to it for the next round.

We also reset the AttackPeriod to be the initial value of -1 whenever battle is entered and exited.

Structurally, the real-time system will look like this in MonsterCollision:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Jrpg.System;
using Jrpg.Unity;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Techniques;
using System;

public class MonsterCollision : MonoBehaviour
{
	...
	
    private float AttackPeriod;

    private void Start()
    {
		...
	
        AttackPeriod = -1;
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
		...

        AttackPeriod = -1;

		...
    }

    private void OnCollisionExit2D(Collision2D collision)
    {
		...

        AttackPeriod = -1;

		...
    }

    void FixedUpdate()
    {
        if (GameStore.GetGameState() == Jrpg.GameState.GameStateValue.Battle)
        {
			...
            if (AttackPeriod < 0)
            {
                AttackPeriod = (float)RandomDouble.Get(2.0, 4.0);
				...
				// Attack here
            }

			...

            // Subtract from period
            AttackPeriod -= Time.fixedDeltaTime;
        }
    }
}

Now we will add code to determine whether or not the Goblin will perform a melee attack, Bite, or miss altogether. We will compute a "weighted probability". The total weight will be 100, so we can just generate a number between 1 to 100. The logic is pretty simple, and naïve within MonsterCollision.cs.

private GoblinCharacter.AttackAction getAttackAction()
{
    int r = new System.Random((int)(DateTime.Now.Ticks)).Next(0, 100) + 1;

    if (r <= 15)
        return GoblinCharacter.AttackAction.Miss;
    if (r <= 40)
        return GoblinCharacter.AttackAction.Technique;

    return GoblinCharacter.AttackAction.Melee;
}
  • 60% chance melee attack strikes successfully.
  • 25% chance the Bite attack strikes successfully.
  • 15% chance that the Goblin misses for the turn.

Add an enum to GoblinCharacter to indicate which action which will be taken:

public enum AttackAction
{
    Melee,
    Technique,
    Miss
}

Blinky Cloud

Now that Cloud can take damage, we will also need to provide some feedback that he has taken damage.

We can apply the same blink. We will create another BlinkController object that will specifically be instantiated with the SpriteRenderer from Cloud.

I know it's a little cheap to be doing this, but we'll stuff another instance of BlinkController withinMonsterCollision specifically for Cloud. Course, we'll be calling this cloudBlinker.

We'll introduce the same type of methods we did for the Goblin to make it blink, but now except for Cloud. All that's needed to be done is to call MaybeFlickerCloud at the end of FixedUpdate.

So, what does that look like now?

Blinky Cloud

Pretty good!

Losing

We're almost there for a full battle sequence. We just need to take care of the case where the player is defeated by the Goblin.

When the player has reached 0 HP, we need to trigger some sort of event that indicates that the player has lost the game. A simple game over chime with a screen which fades to black seems pretty good here.

So what's the chime? I really like the Final Fantasy 6 game over soundtrack. Let's use that.

We'll add the MP3 file as an asset. Now we just need to program the event to play the audio.

Now, the next thing is to create a fade effect. We can use a simple GameObject containing only a SpriteRenderer for this. The SpriteRenderer will output a simple white pixel. We will scale this GameObject to fit the entire camera view.

Rectangle

The RectangleFade script will access the SpriteRenderer of the Rectangle object, and alter its opacity whenever a frame update occurs.

using UnityEngine;

public class RectangleFade : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    private bool inFade = false;
    private float opacity = 0.0f;

    void Awake()
    {
        spriteRenderer = GameObject.Find("Rectangle").GetComponent<SpriteRenderer>();
        spriteRenderer.color = new Color(0, 0, 0, opacity);
    }

    // Start is called before the first frame update
    void Start()
    {
    }

    public void StartFade()
    {
        inFade = true;
    }

    public void ResetFade()
    {
        inFade = false;
        opacity = 0.0f;
    }

    // Update is called once per frame
    void Update()
    {
        if(inFade)
        {
            opacity += (float)(1 / 240.0);
            spriteRenderer.color = new Color(0, 0, 0, opacity);
        }
    }
}

The code is written to complete the full fade within 4 seconds.

Fade animated

Let's modify the MonsterCollision.cs game script to check to see if Cloud is still alive. Based on the current HP of the player, we can set a flag to trigger the game over sequence.

  • GameOver - Triggers the game over sequence.
  • GameOverRunning - Is the sequence currently running?

The best place to add the code is within the Update method in the MonsterCollision script. This method is called every frame update, and is a good place to trigger the Fade.

void Update()
{
    var game = GameObject.Find("Game").GetComponent<Game>();
    var cloud = GameObject.Find("Cloud").GetComponent<Cloud>().GetCharacter();

    bool isGameOver = false;
    bool isGameOverRunning = false;

    try
    {
    	isGameOver = (bool)game.Get("GameOver");
    	isGameOverRunning = (bool)game.Get("GameOverRunning");
    } catch { }

    if (cloud.Statistics[StatisticType.HpCurrent].CurrentValue <= 0 && !isGameOver)
    {
    	game.Set("GameOver", true);
    }

    if (isGameOver && !isGameOverRunning)
    {
    	game.Set("GameOverRunning", true);
    	game.SetGameBgm("gameover", 0, 67);

    	var fade = GameObject.Find("Rectangle").GetComponent<RectangleFade>();
    	fade.StartFade();
        
        game.SetGameState(Jrpg.GameState.GameStateValue.Title);
    }
}

Now that we have this event hooked up, we can fade our screen to black when we lose the game.

Conclusion

Whew! After all that, we have a playable demo. Here is what it all looks like after putting everything together:

Okay! Well, that's about it for this session. It was quite productive. Next time we will finish the demo with:

  • Having some UI when in battle.
  • Having the Goblin follow the player.