Dev Diaries - Building a JRPG - Part 4

WARNING! Super long post ahead. This chapter was written over the course of 2 weekends starting from February 1, 2020. I refrained from publishing the first weekend because I wanted to make sure that I actually had some sort of deliverable for you wonderful readers out there. πŸš€

The content is written in such a way in that it is more like a play-by-play development log of the latest game demo I had been working on to demonstrate the JRPG framework in my spare time. Time isn't really "spare" nowadays, but there can be if managed correctly.

I have recently only blocked off 2-3 hours a weekend to work on this game dev stuff with school, work, and personal life needing to be at a higher priority. Because of that, I have had awkward periods where I just have to "stop" working on it to handle the other responsibilities.

Overall, my belief was that it was only worth it if I delayed publishing until I had a lot of good material to show. I think after a couple of weekends, this is definitely enough. February is short, and we're approaching mid-month by the time you read this. I'm getting super anxious with the realization that I might not have a post this month!

First, Journaling About My Life

February 1, 2020

School really takes a lot out of my time. Go figure. I thought I could handle 8 units while keeping a full time job. Whew, it is really hard. I am managing well as far as grades are concerned.

The Computer Graphics course kicks my butt. It's definitely been a 75/25 split in terms of trying to understand the math as opposed to actually learning about computer graphics. It's an intentional decision to approach it this way for myself, though.

My belief is that understanding the fundamental mathematics behind graphics will help with actually understanding more abstract/theoretical concepts down the line. Besides, it's easy to learn a graphics API, but to make it do something? Well, that takes a lot of conceptual knowledge. So, I will take the time to build up as much domain knowledge on computer graphics as much as possible.

To kick things off here, I've started converting some of my notes to little online tutorials. The first one is matrix multiplication. It's pretty simple, but I started with this one to learn how to write in TeX. Is that what it is called? I'm too lazy to really look it up right now, but everyone who has gone to college, or has read academic papers will know what I am trying to describe. Yeah, I never got to learning the syntax until now. How disappointing for someone who came out of Engineering School, right?

My Cloud Computing course is going well. I am actually a little ahead here. Again, my real world experience has really helped a lot in this class. πŸ˜„

Work has been manageable again, for now. I expect it to pick up again in the middle of the month. For now, I'll just enjoy the breather that I have. As I'm writing this on a Friday night, I was debating whether or not to try and get ahead with school, work, or just relax a little and do some game dev.

Of course, you know what I chose!

I've also had some ideas on how I can take this series in a direction where the projects I demonstrate can be a little more coherent. I think I should start to seriously think about shaping these discussions around a game in which I am planning to one day distribute/publish.

I have some ideas brewing, and I happen to think they are quite original, but at the same time, I want to use those for the game which I'm working on with Scott. Ah... dilemma.

Feburary 8, 2020

I'd like to announce that my wife is 28 weeks pregnant! Woo! We're going onto 7 months. Time is flying, and we've just barely started buying some of the things we need.

The most important thing that's semi-stressing me out right now is just making sure I stay on top of pediatric stuff once my daughter is born. My wife and I spoke to a pediatrician this past week, and he gave us a pretty good overview on what we'd need to do after our child's birth. I feel a little more comfortable, but for some reason, not totally. I guess that's normal, with nerves, and all.

Anyway, yes! 2 months to go!

I also feel like this week was a bit difficult for my emotional health. I think I had leaked out a little bit of it at work. My co-worker approached me and stopped me at the hallway, and asked if I was doing okay. Puzzled, I just stared at him, surprised. Surprised at the fact that he took time to stop our movements at that point, and show compassion.

"Yes, I'm okay. What's up?", I replied.

"Oh no, it's just that the other day, you seemed a bit down walking outside when we had crossed paths. I just wanted to make sure you're holding up."

Surprised, but not really, as I had been a bit emotionally stressed lately. Due to myself being taken aback, I immediately thought of something to say.. "Ah, yeah, I'm okay. Well, at least doing better today. I think I haven't just been sleeping well again. :) But thanks man for caring like that!"

"Like I said, I enjoy working with you. Just let me know whenever you want to talk, or need anything. I'm here for you."

What a guy.

It's time for... Game Dev!

I think we left off last week where we actually have our objects on screen, with the ability to move around. (Yes, I cheated and went back to the blog post to check.)

My Unity window has been open for two weeks now, and I'm surprised it hasn't crashed on my Mac. Let's see how long the uptime can be with 2019.3.0f3.

We have a few goals today. We're going to try, and apply some event handling for when Cloud walks into the Goblin. We're going to actually transition to a new state of the game where battle can take place.

Therefore, we'll need:

  • State management - We'll need to give some sort of feedback that will allow logic be performed specific to battling the Goblin. Making use of basic state management here will be fun. We'll have 2 states.

    • World State - The default state when the player is not battling.
    • Battle State - The state which the player is in when in proximity of an enemy.

We'll transition in, and out of the Battle State whenever Cloud collides with the Goblin. The World State in this case, is the default state.

  • Input handling in battle - We'll hook up the left mouse button to "attack" the monster.
  • Battle BGM - Hey, we do need to know we're fighting, right? How about some of this? Diablo 1 - Dungeon
  • Basic Menu UI - This is a basic overlay to indicate we're in battle.

State Management

As mentioned, we'll have 2 states: World, and Battle. The reason why this would be useful is that:

  1. We can bind different actions to the input keys depending on the state of the game. For example, in the World state, the left mouse button won't do anything, but it would initiate attacks against the enemy if in the Battle state.
  2. We can display different types of UI on the screen when we're in battle.
  3. We can allow different actions to be taken in the battle state such as performing spells which are only allowable within battle.
  4. Many more reasons... I hope I don't have to conveince you any further. πŸ˜„

Luckily, jrpg-system already has some code we can leverage to implement game states! Let's include the library into our project. Simply drag the necessary binaries found in the GitHub repository into the Assets folder.

Within the Jrpg.GameState package, we have the following states made available:

using System;
namespace Jrpg.GameState
{
    public enum GameStateValue
    {
        Title,
        World,
        Map,
        Battle,
        Menu
    }
}

Perfect, we don't have to do anything here. All we'll need to do is just find a way to transition between states as needed. Looking at Jrpg.System.GameStore, we have the API needed to do just that: SetGameState, and CurrentGameState should be enough for our use case.

Let's create a GameStore singleton instance which will live within the global scope of our game. How would we achieve this exactly? Well, for now, the easiest thing to do is is to create an empty GameObject, and have all our GameObjects be children of it. It will serve as the root GameObject.

New GameObject hierarchy

Within this root, GameObject, we can create a script that will serve as a wrapper around the GameStore. We'll call it Game.cs.

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

public class Game : MonoBehaviour
{
    private GameStore store;

    private void Awake()
    {
        store = GameStore.GetInstance();
    }

    public void SetGameState(GameStateValue state)
    {
        store.SetGameState(state);
    }

    public GameStateValue GetGameState()
    {
        return store.CurrentGameState();
    }
}

I chose to initialize the GameStore within the object's Awake method to ensure that it is the first thing that is done when the object is created.

Now, let's guarantee that the Game script is the first script that gets run in the entire game. We can do this through the Project Settings within Unity.

Project Settings > Script Execution Order

Script execution order

Okay, now let's handle some state change! Looking at the Unity API docs, we can see that the Box Collider 2D game component has a couple of events which we can use to handle collision.

  • OnCollisionEnter2D
  • OnCollisionExit2D

https://docs.unity3d.com/ScriptReference/Collider.html

If we implement these handlers on the Goblin GameObject, we will be able to set code that can handle the transition to Battle state, and back to World state.

Let's create a new script against the Goblin, and call it MonsterCollision.cs, and implement those methods.

OnCollisionEnter2D will transition the player into the Battle state, while OnCollisionExit2D should transition to the World state. In order to confirm that we are in these states, we will also log the current state to the console.

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

public class MonsterCollision : MonoBehaviour
{
    private Game GetGameStore()
    {
        var game = GameObject.Find("Game");
        if (game == null)
            throw new System.Exception("Game is not initialized, for some reason.");

        var gameStore = game.GetComponent<Game>();

        return gameStore;
    }

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

        gameStore.SetGameState(Jrpg.GameState.GameStateValue.Battle);

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

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

        gameStore.SetGameState(Jrpg.GameState.GameStateValue.World);

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

Game state

Input Handling in Battle

Let's hook up the mouse click to "attack" the Goblin whenever we're in the battle state. Let's revisit the MonsterCollision.cs, and hook up the handling of the left button mouse click.

First, let's have a boolean variable to keep track of the mouse button -- whether it is being clicked, or not. In the most original manner, the variable name will be MouseUp.

private bool MouseUp;

void Start() {
	...
	MouseUp = true;
}

We will assume that the mouse button will be initially "up", so therefore it will be true.

Now, In our FixedUpdate method, we'll add in some code to detect the mouse button click event when we are in battle.

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

		if (clicked && MouseUp)
		{
			Debug.Log($"HITTING {(string)GameStore.Get("ActiveMonster")}");
			MouseUp = false;
		}
		else if (!clicked)
			MouseUp = true;
	}
}

Here, the logic is to first only execute when we're in the Battle state. If we are in the Battle state, then the code will check to see if the left mouse button is currently being held down.

A battle action will then be performed if the mouse button is being clicked, and only if the button has been released before. If the conditions are met, then we will simulate a "battle turn".

Otherwise, we will reset the MouseUp variable.

We'll need to modify the Game object to also contain Set and Get methods to hold some data within the store.

...
    public void Set(string key, object value)
    {
        store.Put(key, value);
    }

    public object Get(string key)
    {
        return store.Get<object>(key);
    }
...

Next, we'll modify MonsterCollision.cs to hold an ActiveMonster variable, the name of the monster, whenever we enter battle. The ActiveMonster variable will also be reset when we exit battle.

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

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

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

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

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

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

Debugging the battle

Testing this, you'll notice that the click will be registered in the Battle state no matter where the mouse is. Let's actually fix this and restrict the click event to only be registered when the mouse pointer is only situated on the Goblin.

I found this article to be extremely helpful in writing the logic necessary to determine if the mouse cursor is on the collided object during a click.

https://kylewbanks.com/blog/unity-2d-detecting-gameobject-clicks-using-raycasts

A Raycast essentially β€œdraws” a line between two points in the game world, and detects any physics bodies that are hit along the way. You can then use this information to determine what was hit by the Raycast and act accordingly. For another useful example of Raycasts, check out my post on Checking if a Character or Object is on the Ground using Raycasts.

The basic gist of it is that when a click has been registered, we will get the current mousePosition in screen coordinates. Since that position is the already rendered frame, we must find the corresponding position in the scene -- which is the world coordinates.

These must be first converted to world coordinates (the coordinate system before the frame is rendered to the screen), and use that position to create a ray to be casted upon the object where the mouse position is.

This simulates a point of light coming from the camera, and creates a collision. We can then check to see what GameObject the particular ray had collided with.

Here is the code implemented:

...
		private bool ClickedOnMonster()
    {
        Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector2 mousePosition2D = new Vector2(mousePosition.x, mousePosition.y);

        RaycastHit2D hit = Physics2D.Raycast(mousePosition2D, Vector2.zero);
        if (hit.collider == null)
            return false;

        if (!hit.collider.name.Equals(gameObject.name))
            return false;

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

            if (clicked && MouseUp && ClickedOnMonster())
            {
                Debug.Log($"HITTING {(string)GameStore.Get("ActiveMonster")}");
                MouseUp = false;
            }
            else if (!clicked)
                MouseUp = true;
        }
    }
...

Testing it, you'll find that HITTING Goblin only now gets logged when the mouse is on the Goblin GameObject itself when in Battle state.

Attaching Character Objects

It's time to give Cloud, and Goblin some life by assigning the GameObjects to contain the Character instances. First thing's first, we'll create new scripts to contain the Character instances in the GameObjects.

We'll start with Cloud.

  • Select the game object relating to Cloud, and add a new Script game component to the object.
  • Then create a Character instance variable, and initialize it in the Awake method of the script. For now, we can just make Cloud a Freelancer, so we don't need to initialize a job class. We'll do that later.
  • Add some debugging to the Start method. I think outputting the name, and the stats of the character will be enough to give assurance that everything is working well. Let's just show the Level, HP, and MP statistics for now.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Jrpg.CharacterSystem;
using System.Text;

public class Cloud : MonoBehaviour
{
    private Character character;
    void Awake()
    {
        character = new Character(gameObject.name);
    }

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log($"Initialized the character with {character.Name})");

        var currentLevel = character.Statistics[StatisticType.Level].CurrentValue;
        var currentHp = character.Statistics[StatisticType.HpCurrent].CurrentValue;
        var maxHp = character.Statistics[StatisticType.HpMax].CurrentValue;
        var currentMp = character.Statistics[StatisticType.MpCurrent].CurrentValue;
        var maxMp = character.Statistics[StatisticType.MpMax].CurrentValue;

        Debug.Log($"-- Character Stats: Lvl {currentLevel}, {currentHp}/{maxHp} HP, {currentMp}/{maxMp} MP");
    }

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

Testing this out, we will see that it is working as expected.

Debug output for character instance

Okay, now that we have our sanity confirmed, let's make Cloud more than just a Freelancer. Let's make him a class that is more in line with what we sort of was in the original game. How about a Soldier? Create a directory under the Assets view in Unity called Resources.

Let's add a CharacterClasses.json file in this folder so that we can register these classes through the jrpg-system engine.

The starting statistics I will be using for the Soldier class models off of the Fighter class from Final Fantasy I.

https://guides.gamercorner.net/ff/classes/fighter

{
    "Name": "Soldier",
    "Agent": "",
    "Techniques": [
        {
            "Level": 1,
            "Name": "Omnislash"
        }
    ],
    "StartingStatistics": [
        {
            "Name": "Level",
            "Value": 1
        },
        {
            "Name": "HP Current",
            "Value": 35
        },
        {
            "Name": "HP Max",
            "Value": 35
        },
        {
            "Name": "MP Current",
            "Value": 5
        },
        {

            "Name": "MP Max",
            "Value": 5
        },
        {
            "Name": "Experience",
            "Value": 0
        },
        {
            "Name": "Strength",
            "Value": 20
        },
        {
            "Name": "Speed",
            "Value": 5
        },
        {
            "Name": "Stamina",
            "Value": 10
        },
        {
            "Name": "Magic",
            "Value": 5
        },
        {
            "Name": "Attack",
            "Value": 20
        },
        {
            "Name": "Defense",
            "Value": 10
        },
        {
            "Name": "Evasion",
            "Value": 10
        },
        {
            "Name": "Magic Defense",
            "Value": 15
        },
        {
            "Name": "Magic Evasion",
            "Value": 10
        }
    ]
}

By default, Cloud will know how to use Omnislash. I'm not sure if we'll get to it in this post, but it's something worth trying to implement should there be enough time. πŸ˜„

You'll notice that we still need to define an Agent for the Soldier class. As a reminder, the Agent is a class which implements the Jrpg.CharacterSystem.Classes.BaseCharacterClass object.

We'll also need additional things to be able to instantiate an agent properly.

  • The starting statistics of the character class (we have defined it in the JSON file), which will be used to instantiate the scalers for the statistics.
  • The list of technique definitions
  • The list of technique definition mappings for the character (at what level have they learned a specific technique)

Technique: Omnislash

We'll need to create a Techniques.json file within the Resources folder which will define the technique's meta information, and agent which will invoke the effect.

Here's what we can immediately define:

[
    {
        "id": "Tech_Omnislash",
        "DisplayName": "Omnislash",
        "MpCost": 5,
        "AttackPower": 30,
        "MagicPower": 0,
        "Agent": ""
    }
]

Now, our next step is to create the agent for this technique. What we will need to do is create an Omnislash class which extends the Jrpg.CharacterSystem.Techniques.Technique class. I've decided to just place it under the root Unity Assets folder for now.

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

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

    public override void Perform(Character source, List<Character> targets)
    {
        Debug.Log("Performing OMNISLASH");
    }
}

The class right now is just a stub, and we'll be adding more logic later, but now we can add this to our Agent property in the Techniques.json file.

{
	...
	"Agent": "Omnislash, Assembly-CSharp"
}

Statistics: Scalers

The next requirement we will need to have for our Soldier class is to provide a set of statistic scalers. These are individual classes which operate on a specific statistic whenever a character belonging to the class, levels up.

There are a total of 11 Scaler classes which will be implemented:

  1. AttackScaler
  2. DefenseScaler
  3. EvasionScaler
  4. HpScaler
  5. MagicDefenseScaler
  6. MagicEvasionScaler
  7. MagicScaler
  8. MpScaler
  9. SpeedScaler
  10. StaminaScaler
  11. StrengthScaler

I'll only show how to do the AttackScaler class here, since the rest will just be variants of this. I'll leave it up to everyone else to determine how they want to scale the statistics for the class. For this article, the exact implementation isn't important -- as long as it does something.

All scalars will inherit Jrpg.CharacterSystem.Scalers.BaseScaler.Right now, we'll just keep it simple and just leave the implementation to the default in which the base class provides. The only thing we'll have to do is just set the StatisticType associated with the scaler.

using System;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Scalers;

namespace JobClasses.Scalers
{
    public class AttackScaler : BaseScaler
    {
        public AttackScaler()
        {
            type = StatisticType.Attack;
        }
    }
}

Just repeat for the other 10 scalers. We'll definitely modify the scaling of each statistic later one. The goal right now is just to pipe all the data which we need. 😎

Now, we have enough to create our Soldier class, so let's create a class that will inherit BaseCharacterClass. Just like our scalers, we keep it minimal:

using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Classes;
using Jrpg.CharacterSystem.Scalers;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.CharacterSystem.Classes.Definitions;
using JobClasses.Scalers;

public class Soldier : BaseCharacterClass
{
    private IStatisticScaler hpScaler;
    private IStatisticScaler mpScaler;
    private IStatisticScaler strengthScaler;
    private IStatisticScaler speedScaler;
    private IStatisticScaler staminaScaler;
    private IStatisticScaler magicScaler;
    private IStatisticScaler attackScaler;
    private IStatisticScaler defenseScaler;
    private IStatisticScaler evasionScaler;
    private IStatisticScaler magicDefenseScaler;
    private IStatisticScaler magicEvasionScaler;

    public Soldier(Dictionary<StatisticType, Statistic> statistics,
        List<TechniqueDefinition> techniqueDefinitions,
        List<ClassTechniqueDefinition> techniqueDefinitionMapping)
        : base(statistics, techniqueDefinitions, techniqueDefinitionMapping)
    {
        hpScaler = new HpScaler();
        mpScaler = new MpScaler();
        strengthScaler = new StrengthScaler();
        speedScaler = new SpeedScaler();
        staminaScaler = new StaminaScaler();
        magicScaler = new MagicScaler();
        attackScaler = new AttackScaler();
        defenseScaler = new DefenseScaler();
        evasionScaler = new EvasionScaler();
        magicDefenseScaler = new MagicDefenseScaler();
        magicEvasionScaler = new MagicEvasionScaler();
    }

    public override Statistic NextHpMax()
    {
        return hpScaler.NewStatistic(Statistics);
    }

    public override Statistic NextMpMax()
    {
        return mpScaler.NewStatistic(Statistics);
    }

    public override Statistic NextStrength()
    {
        return strengthScaler.NewStatistic(Statistics);
    }

    public override Statistic NextSpeed()
    {
        return speedScaler.NewStatistic(Statistics);
    }

    public override Statistic NextStamina()
    {
        return staminaScaler.NewStatistic(Statistics);
    }

    public override Statistic NextMagic()
    {
        return magicScaler.NewStatistic(Statistics);
    }

    public override Statistic NextAttack()
    {
        return attackScaler.NewStatistic(Statistics);
    }

    public override Statistic NextDefense()
    {
        return defenseScaler.NewStatistic(Statistics);
    }

    public override Statistic NextEvasion()
    {
        return evasionScaler.NewStatistic(Statistics);
    }

    public override Statistic NextMagicDefense()
    {
        return magicDefenseScaler.NewStatistic(Statistics);
    }

    public override Statistic NextMagicEvasion()
    {
        return magicEvasionScaler.NewStatistic(Statistics);
    }

    public override string ClassName()
    {
        return "Soldier";
    }
}

We'll also need to update the CharacterClasses.json file to make a reference to the Solider class as the agent.

{
    "Name": "Soldier",
    "Agent": "Soldier, Assembly-CSharp",
    ...
 }

Okay, now that we have a very basic version of Cloud set up, let's move onto the Goblin!

The Enemy

I've purposely left creation of the Goblin as a section of its own because it is a bit more complex than the typical Character object.

Let's create a new JSON file called EnemyClasses.json under Assets/Resources in Unity. Enemy definitions within this JSON file are ultimately deserialized into an EnemyDefinition. Taking a look at the source code of the JRPG framework, the EnemyDefinition is specified as follows:

using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.Classes.Definitions;
using Jrpg.InventorySystem.PgItems;

namespace Jrpg.BattleSystem.Enemies.Definitions
{
    public class EnemyDefinition : ClassDefinition
    {
        public string Id { get; set; }
        public string Elemental { get; set; }
        public List<ItemClassEdge> ItemClass { get; set; }
        public int Gold { get; set; }
        public int Experience { get; set; }
    }
}

The EnemyDefinition inherits ClassDefinition, which means that the following properties are also included:

using System;
using System.Collections.Generic;
namespace Jrpg.CharacterSystem.Classes.Definitions
{
    public class ClassDefinition
    {
        public string Name { get; set; }
        public string Agent { get; set; }
        public List<ClassTechniqueDefinition> Techniques = new List<ClassTechniqueDefinition>();
        public List<ClassStatistic> StartingStatistics;
    }
}

We can then create a starting definition with some basic information:

{
    "Id": "Goblin", 
    "Name": "Goblin",
    "Agent": "Goblin, Assembly-CSharp",
    "Elemental": "None",
    "ItemClass": "",
    "Gold": 100,
    "Experience": 50,
    "Techniques": [],
    "StartingStatistics": [
        {
            "Name": "Level",
            "Value": 1
        },
        {
            "Name": "HP Current",
            "Value": 20
        },
        {
            "Name": "HP Max",
            "Value": 20
        },
        {
            "Name": "MP Current",
            "Value": 10
        },
        {

            "Name": "MP Max",
            "Value": 10
        },
        {
            "Name": "Experience",
            "Value": 0
        },
        {
            "Name": "Strength",
            "Value": 15
        },
        {
            "Name": "Speed",
            "Value": 7
        },
        {
            "Name": "Stamina",
            "Value": 8
        },
        {
            "Name": "Magic",
            "Value": 1
        },
        {
            "Name": "Attack",
            "Value": 14
        },
        {
            "Name": "Defense",
            "Value": 9
        },
        {
            "Name": "Evasion",
            "Value": 8
        },
        {
            "Name": "Magic Defense",
            "Value": 4
        },
        {
            "Name": "Magic Evasion",
            "Value": 4
        }
    ]
}

Notice that I have decided to make the Goblin slightly weaker than a character who is a part of the Soldier class. We can tune this later to adjust the difficulty to what we want.

There are other properties about the Goblin which differ from a normal character:

  • The Goblin will reward 100 gold to the player after battle.
  • The Goblin will reward 50 experience points to the player after battle.
  • The Goblin will not have any "special" techniques apart from the normal melee attacks.
  • The Goblin has no elemental affinity.

Also note, that the Goblin is supposed to drop some sort of item. Right now, we have the ItemClass field as an empty string. Additionally, we'll need to create the Goblin class itself. Let's first tackle the ItemClass attached to the Goblin.

The Item

So what would be a good item for the Goblin to drop? I think a good item which makes sense would be a Potion. In order to do that let's first create all the data for the Potion item.

Let's create another JSON definition file called Items.json. The basic item definitions contained in this file will be NoDrop, to represent that the monster will not drop an item in this battle, and Potion, which is pretty self-explanatory. πŸ˜„

[
    {
        "Name": "NoDrop",
        "ItemClass": [],
        "Properties": [],
        "Value": 0,
        "BodyPart": "Default"
    },
    {
        "Name": "Potion",
        "ItemClass": [],
        "Properties": [
            {
                "Name": "HP Current",
                "Value": 10
            }
        ],
        "Value": 50,
        "BodyPart": "Default"
    }
]

Remember that items are essentially graphs, and a particular item in the Items.json file represents a vertex within the graph. ItemClass in each item then points to the neighboring items. If you want a review on how this is implemented, check out the article which I had designed this.

To keep things consistent here with what we are used to, Prefixes.json, and Suffixes.json will also be created for the procedurally generated item. Of course, for Potion this is not needed right now, so our affix files will only contain the NoPrefix, and NoSuffix entries respectively.

Example:

[
  {
    "Name": "NoPrefix",
    "ParentItemClass": [],
    "Properties": [],
    "Weight": 15,
    "Value": {}
  }
]

Okay, let's go back to EnemyClasses.json, and fill in this item information.

{
		...
    "ItemClass": [
        {
            "Name": "NoDrop",
            "Weight": 6
        },
        {
            "Name": "Potion",
            "Weight":  9
        }
    ],
    ...
}

I've designated for Potion to have a higher chance of being dropped than nothing at all.

Goblin Class

Shifting the gears back to actually creating the Goblin class itself, we'll find that we must extend the existing Jrpg.BattleSystem.Enemies.EnemyClass class itself. Keeping things super basic, we'll just override the ClassName method in that class.

This also means that we will also use the default scalers provided by the EnemyClass.

using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Classes.Definitions;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.InventorySystem.PgItems;
using Jrpg.BattleSystem.Enemies;

public class Goblin : EnemyClass
{
    public Goblin(
        Dictionary<StatisticType, Statistic> statistics,
        List<TechniqueDefinition> techniqueDefinitions,
        List<ClassTechniqueDefinition> techniqueDefinitionMapping,
        List<ItemClassEdge> itemClasses,
        int gold,
        int experience
    ) : base(statistics, techniqueDefinitions, techniqueDefinitionMapping, itemClasses, gold, experience)
    {
    }

    public override string ClassName()
    {
        return "Goblin";
    }
}

Initializing Everything So Far...

Oh man, we've done a lot so far, but now we actually need to start wiring everything up together so that we can actually test our game with some of this data.

Let's do that now before we go any further.

A good place to start here is to start building out more initialization logic within our Game.cs class. A few managers are needed to manage all this data floating around.

The primary instance we need is an instanceStatusEffectManager. We need this in order to:

  • An instance of this is needed in order to read in the technique definitions, and in turns instantiate the ClassManager. (To manage playable character related classes)
  • The EnemyManager also needs technique definitions which means it also needs the StatusEffectManager. (To manage enemy related classes)

We will need to:

  1. Initialize a StatusEffectManager instance.
  2. Load the technique definitions from the JSON file found in the Resources folder. We can do this using Resources.Load. See: https://docs.unity3d.com/ScriptReference/Resources.Load.html for reference. We can use a basic helper method:
private string loadStringFromResource(string path)
{
	return Resources.Load<TextAsset>(path).text;
}
  1. Create an instance of ClassManager with the loaded technique definitions as a parameter. Then load the character classes definitions from the Resources folder, and register the classes.
  2. Load all items from the Items.json, Prefixes.json, and Suffixese.json file, and create an InventoryManager, and ItemGenerator instance.
  3. Create an instance of EnemyManager with the technique definitions, and item generator. We will also need to load the enemy definitions -- right now, it is only just the Goblin.

Phew, that's a lot of manual work, and I think I'm going to actually improve the framework code a bit to make it easier to do all this initial setup. Ultimately, I think the draw back in making things "easier" to use is that I will have a more opinionated framework. Is that necessarily a bad thing?

It depends. On one hand, an opinionated framework posseses tighter control on the usage of its API, which results in a cleaner set of calls to get things done. A less opinionated framework grants greater freedom to the API consumer to leverage the raw resources in the way they intend to use it.

We see this all the time in the web in various frameworks. Take Redux for example, simple things like having your Actions, and Store functions in a single file isn't illegal, and Redux won't stop you from doing something like that. However, it probably wouldn't fit against the norm in most codebases. This could result in some unpredictability. While, on the other hand, something like your typical Angular application is a bit more opinionated, and you would be able to navigate through an unfamiliar codebase because of that.

Anyway, I am rambling. πŸ˜„ Here's our main Game code so far to bootstrap everrrrything.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Jrpg.System;
using Jrpg.GameState;
using Jrpg.CharacterSystem.Classes;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.CharacterSystem.StatusEffects;
using Jrpg.InventorySystem;
using Jrpg.InventorySystem.PgItems;
using Jrpg.BattleSystem.Enemies;

public class Game : MonoBehaviour
{
    private GameStore store;
    private StatusEffectManager statusEffectManager;
    private ClassManager classManager;
    private InventoryManager inventoryManager;
    private ItemGenerator itemGenerator;
    private EnemyManager enemyManager;

    private void Awake()
    {
        store = GameStore.GetInstance();
        statusEffectManager = new StatusEffectManager();

        var techniqueDefinitions = new TechniqueFactory(statusEffectManager).FromJsonDefinition(loadStringFromResource("Techniques"));


        classManager = new ClassManager(techniqueDefinitions);

        var classDefinitions = classManager.FromJsonDefinition(loadStringFromResource("CharacterClasses"));
        foreach (var className in classDefinitions.Keys)
            classManager.Register(className, classDefinitions[className]);

        var itemsList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Item>>(loadStringFromResource("Items"));
        var prefixesList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Affix>>(loadStringFromResource("Prefixes"));
        var suffixesList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Affix>>(loadStringFromResource("Suffixes"));

        inventoryManager = new InventoryManager();
        itemGenerator = new ItemGenerator(itemsList, prefixesList, suffixesList);

        enemyManager = new EnemyManager(techniqueDefinitions, itemGenerator);
        enemyManager.FromJsonDefinition(loadStringFromResource("EnemyClasses"));
    }

    private string loadStringFromResource(string path)
    {
        return Resources.Load<TextAsset>(path).text;
    }

    public void SetGameState(GameStateValue state)
    {
        store.SetGameState(state);
    }

    public GameStateValue GetGameState()
    {
        return store.CurrentGameState();
    }

    public void Set(string key, object value)
    {
        store.Put(key, value);
    }

    public object Get(string key)
    {
        return store.Get<object>(key);
    }
}

Now, all that being said... Does it compile?

Indeed it does!

It's working!

Rest Stop!

Okay, I think this is a good stopping point now. The post will get too long if I try to cram everything I want to get done into here. I think the biggest accomplishment here is that data is finally flowing around the game, and that we've integrated our "backend" into Unity.

We'll continue next time, and hopefully you'll hear from me again before March. πŸ˜„

What year is it?