Dev Diaries - Building a JRPG - Part 5

Today, I will be carrying over the work I had plan to complete in Part 4 into this chapter. 😄

It's a Saturday afternoon for me at 1:00 PM, and I've just had lunch. It was some rice, and vegetarian chop suey.

I'm about to make some coffee soon, with a few biscuits while I get myself in a mood for some game dev.

I have 2 hours (until 3:00 PM) to do this. Let's see how far we get...

In short, this week had been really busy for me because of school work. I'm all caught up now, but wow, if it isn't work keeping me busy, it's school!

Yeah, I'm really going to just dedicate myself to 4 units this coming Summer semester, especially since that's around the time when I will have my newborn.

I guess one news of the week was that... I filed my taxes! Woo!

Taxes

Game Development!

Okay, last week we wired up a bunch of code to get to the point where our sprites on screen actually represent a character, and an enemy.

There's still much to do, but we have most of the code piped, and ready for the data to flow.

I'm going to keep this post short as I want to spend some of my time this weekend thinking about a story for a real JRPG I hope to one day create...

But let's actually make some progress with our current demo, yeah?

What I feel the next step here is to actually simulate a very basic battle. But first...

Music

Our game needs some piz'zaz right now. It starts to get a little boring when all we have are moving graphics on screen. How about some music to create some mood?

Here's what we'll be using for our background music. It is the Shinra theme from Final Fantasy 7:

For our "battle" theme, we'll use the Chrono Trigger battle theme!

Adding the background BGM is easy in Unity. We'll just need to add a new component to our root Game object in our scene.

This will be an Audio Source game component.

Adding background music

Here are the Audio Source component settings:

  • AudioClip

    • (the Shinra audio clip)
  • Play On Awake

    • We want to start the BGM as soon as the game is loaded
  • Loop

    • BGMs always loop!
  • Volume

    • 67 -- we don't want the music too be too loud.

Now, how about when we transition to the battle state? How should we transition? First we'll need to look at where we transition the state to battle. If you recall, we had done this in our OnCollisionEnter2D event handler within the MonsterCollision.cs file. Conveniently, OnCollisionExit2D, the handler to transition back from Battle to World state also lies here.

We'll just need to set the AudioSource game component in the Game object here to be the battle BGM in OnCollisionEnter2D, and then back to the Shinra theme when we exit the battle state on OnCollisionExit2D.

In order to conveniently do all this, let's actually create a helper method within our Game.cs file to programmatically switch between background BGMs.

Game.cs
...
    public void SetGameBgm(string bgmName, float position, float volume)
    {
        var audioSource = GetComponent<AudioSource>();
        audioSource.clip = Resources.Load<AudioClip>(bgmName);
        audioSource.loop = true;
        audioSource.time = position;
        audioSource.volume = volume / 100.0f;
        audioSource.Play();
    }
...

The code is basic here. We just need to get the AudioSource game component, then load the audio resource. We'll also reset the audio clip to the same settings we had.

Our helper method also has position, and volume parameters which will come in handy in making sure we have control over what position of the song to start at, and how loud it will be, as most tracks tend to have varying levels of volume. Normalizing audio, in my opinion, is out of scope for this post. 😄

Alright, we now have this method implemented, and now let's zip back to MonsterCollision.cs. In OnCollisionEnter2D, we will need to find the Game game object, and then within it, find the Game component -- which is our global game state manager.

Once we have that object, we can call SetGameBgm to the audio clip resource found within our assets.

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

        gameStore.SetGameState(Jrpg.GameState.GameStateValue.Battle);
        gameStore.Set("ActiveMonster", gameObject.name);

        var game = GameObject.Find("Game").GetComponent<Game>();
        game.SetGameBgm("ct_battle", 1, 45);

        Debug.Log($"We are in the {gameStore.GetGameState().ToString().ToUpper()} state!");
    }

The above will start playing the Chrono Trigger battle theme whenever we enter in the Battle state.

Upon existing the Battle state, and back into the World state, we will just need to call SetGameBgm again, but now this time to our Shinra Corp. theme.

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

        gameStore.SetGameState(Jrpg.GameState.GameStateValue.World);
        gameStore.Set("ActiveMonster", null);

        var game = GameObject.Find("Game").GetComponent<Game>();
        game.SetGameBgm("shinra", 0, 67);

        Debug.Log($"We are in the {gameStore.GetGameState().ToString().ToUpper()} state.");
    }

Notice that tracks start at different positions, and play at different volumes. The Chrono Trigger battle theme is much louder in comparison, and doesn't begin right away, so I had to skip about 1 second and reduce the volume as opposed to just playing the Shinra Corp. theme as-is.

Melee Attacks

It's time to attach some logic to our Goblin sprite when it is clicked. I think the best place to put this is within the FixedUpdate method in MonsterCollision! This is where we are currently handling the click events from the Raycast2D collisions.

We'll first need to add a new script to our Goblin game object to contain the Character information. This means that we must attach our discrete Goblin enemy class object to the game object.

We'll create a new script game component within the Goblin game object. Let's call it GoblinCharacter. Haha, I'm lame... 😦 I need to start coming up with better names for things!

Pausing for now as it is 3:00 PM, and I need a break. Brb...

Intermission

Okay, now we're back!

The GoblinCharacter class will serve as an adapter from Unity to the Jrpg.BattleSystem.Enemies.Enemy object that will manipulate the internal properties of the Goblin enemy. We'll expose a single method called MeleeAttack that will take in a Character object as a parameter.

This Character object will be the player character who is currently attacking the Goblin. With this character information passed in as a parameter, we can do some useful things to calculate an appropriate amount of damage a player can deal to the Goblin using their character. In this case, it will be Cloud.

Additionally, we can also handle completion of the battle here int his class for now. Let's consider that the battle has "ended" whenever the enemy's current HP reaches 0.

How we can handle this is to simply destroy the game object using Destroy.

GoblinCharacter.cs
using System.Collections;
using System.Collections.Generic;
using Jrpg.BattleSystem.Enemies;
using Jrpg.CharacterSystem;
using UnityEngine;

public class GoblinCharacter : MonoBehaviour
{
    private Enemy character;
    private Game gameStore;

    private void Awake()
    {
        gameStore = GameObject.Find("Game").GetComponent<Game>();
        character = (gameStore.GetEnemy("Goblin", "Goblin"));
    }

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

            if(character.Statistics[StatisticType.HpCurrent].CurrentValue <= 0)
            {
                character.Statistics[StatisticType.HpCurrent].CurrentValue = 0;

                Destroy(GameObject.Find(gameStore.Get("ActiveMonster").ToString()));
                gameStore.SetGameState(Jrpg.GameState.GameStateValue.World);
                gameStore.SetGameBgm("shinra", 0, 67);
            }
        } 
    }

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log($"Enemy: {character.CurrentClassName()}");
    }

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

Also note that when the player has defeated the enemy, we transition back from Battle state to the World state (and changing the BGM along with it).

Now, we will need to hook up logic in order to invoke the MeleeAttack method. As mentioned before, MonsterCollision::FixedUpdate is probably the best player to put it in right now.

    void FixedUpdate()
    {
        if (GameStore.GetGameState() == Jrpg.GameState.GameStateValue.Battle)
        {
            var clicked = Input.GetMouseButton(0);

            if (clicked && MouseUp && ClickedOnMonster())
            {
                // Do some battle stuff here
                var monster = GameObject.Find(GameStore.Get("ActiveMonster").ToString())
                    .GetComponent<GoblinCharacter>();

                var character = GameObject.Find("Cloud")
                    .GetComponent<Cloud>();

                monster.MeleeAttack(character.GetCharacter());

                MouseUp = false;
            }
            else if (!clicked)
                MouseUp = true;
        }
    }

Okay now, let's test it! Here's a little video that demonstrates everything we have done so far:

I'm pretty happy with this, so far! 😎

Stopping Point

I told you it was going to be a shorter one today. I've just about used up all my time for today with respect to working on this demo. Again, all this is for the sake of managing my time better. This is supposed to be fun! Game dev won't be fun anymore if I have to pressure myself to get more done than I really feel like.

Hope you all understand!