Create an RPG Leveling System
In this tutorial you will learn how to give enemies health, add a UI to the game, and gain experience from killing monsters.
RPG Programming Architecture Strategy
Understand the core techniques that will be used to create an RPG architecture.
Remember the architecture diagram from earlier in the lesson? You've created the basics of the player, the enemy, and the combat system...but you've got a long way to go towards implementing all of those other systems.
For this core lesson, you're going to implement a more advanced health points and damage system, and a level-up system. But we're going to implement these systems in a way that they will be easy to modify later.
And, most importantly, you're going to implement these systems in a way that makes them easy to update later...and easy to remove if you want to (There's no worse feeling than getting stuck with a system you can't take out of the game simply because it is so heavily linked to the way the game works.)
To make an effective game architecture, you're going to use a special type of object called a Scriptable Object to store data about the player's stats. A Scriptable Object is a script that does NOT have a transform, it can never exist as an object inside of a scene.
Because it does not exist inside of a scene, it stores data independently of what happens inside of scenes. This is a useful property, because it means that you can prevent objects inside the scene from being dependent on each other.
For example, when you make a UI system, in the past you might have needed to make a decision: Do you have the UI system retrieve the player's health? Or do you have the player call a method in the UI system to update it with the correct value?
By using Scriptable Objects, the combat system (where the player loses health when they are hit) and the UI system (which displays how much health the player has left) will never know the other system exists. This is because they will both reference the same Health Scriptable Object they will use to perform their necessary functions.
The fundamental principle is this: If there are any cases where there is data that more than one system needs to understand, then you should use scriptable objects, rather than placing variables directly on the object.
Adding Player Variables
Create variables that will represent the various parts of the RPG game's design.
We're going to create a system that will let us store variables that can be read by any part of the RPG system.
Create a new script called FloatVariable. It will have the following code.
using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu] public class FloatVariable : ScriptableObject { public float Value; }
Now, create a new folder called Variables. Then, because we have the [CreateAssetMenu] decorator on this class, we can create new values of it inside of our project by right-clicking and selecting FloatVariable from the list.
Create the following variables from the FloatVariable Scriptable Object: PlayerHealth, PlayerMaxHealth, PlayerAttack, EnemyMaxHealth, EnemyAttack.
Then, set the values for those variables. For example:
PlayerHealth: 50
PlayerMaxHealth: 50
PlayerAttack: 5
MonsterMaxHealth: 20
MonsterAttack: 2
Feel free to play around with those values if you want to make the game easier or harder.
After you add those variables, we're going to include references to those values inside of MonsterBehavior.
public FloatVariable monsterMaxHealth; public float monsterCurrentHealth; public FloatVariable monsterAttack; public FloatVariable playerHealth; public FloatVariable playerAttack; void Start () { player = GameObject.FindGameObjectWithTag("Player"); monsterCurrentHealth = monsterMaxHealth.Value; }
Next, change the OnCollisionEnter method to use these variables.
void OnCollisionEnter(Collision other) { if (other.gameObject == player) { playerHealth.Value -= monsterAttack.Value; } else if (other.gameObject.tag == "Blade") { monsterCurrentHealth -= playerAttack.Value; if (monsterCurrentHealth <= 0) { Destroy(this.gameObject); } } }
Then, link up those variables and try out the game. Now the player's health will go down if you get hit...but do you notice something peculiar happening?
When you're dealing with Scriptable Objects, it's important to remember that unlike Monobehavior values, the values of Scriptable Objects do NOT reset when you stop the game! Because of this, we need to set up a very basic Player script that will reset the player's health at the start of the game.
Create a Player script with the code below and attach it to the player.
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class Player : MonoBehaviour { public FloatVariable playerMaxHealth; public FloatVariable playerHealth; public void Start() { playerHealth.Value = playerMaxHealth.Value; } public void Update() { if (playerHealth.Value <= 0) { //Reset the Player Health and start the scene over again. playerHealth.Value = playerMaxHealth.Value; SceneManager.LoadScene(SceneManager.GetActiveScene().name); } } }
Now we have a system where the player can fight monsters, defeat them, and be defeated.
With our system, all important variables that multiple objects will use (for right now, just enemies) will be comprised as Scriptable Object FloatVariables.
Adding UI
Use the values in Scriptable Objects to update the game's UI.
Now we're at our first big challenge that Scriptable Objects can help solve: Adding a UI. In the past, you might have just linked your UI directly to your player script. After all, the values in the UI were always coming from player variables.
However, because we are storing the values for our player in an easy-to-access location, our UI won't even know that the player exists. It has one job: Display the stats we want it to display.
Create the below script and call it UIManager
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class UIManager : MonoBehaviour { public Text attackText; public Image healthBar; public FloatVariable playerHealth; public FloatVariable playerMaxHealth; public FloatVariable playerAttack; private void Update() { attackText.text = playerAttack.Value.ToString(); float healthPercent = playerHealth.Value / playerMaxHealth.Value; healthBar.transform.localScale = new Vector3(healthPercent, 1f); } }
Next, drag the UICanvas onto the scene, and hook up the variables to the object
In the past, if you tried to disable your canvas after you created it, or you destroyed your player, the unity game would crash. The reason was that the UI and the Player objects were tightly linked. With this system, you can enable and disable the UI object, and the game will still play perfectly fine (though your players might complain about not being able to see their health!)
Experience Points and Level-Up System
At this point, you might be thinking,
This is a lot of work! There's so many variables I have to link! Why don't I just store all the variables on the Player script, rather than going through this linking process?
If we were to end our RPG right here, with just this level of functionality, that might be the right call. But we're going to be expanding our RPG later, and if we store the variables on the player, the player would need to be referenced if we wanted to:
Add EXP after a quest is completed (Quest System)
Prevent the player from equipping certain items until they reach a specific level (Equipment System)
Spawn monsters that are more powerful based on the player's level (Enemy System)
Unlock player abilities at certain levels (Ability System)
Without our variables in the project, every time we made a new system, we would need to link it up specifically with the player object. With this structure, we can add as many new systems as we want. And if we want to completely change the way the player works, that's fine too.
In order to implement this type of a system, you need to give up a little speed when it comes to coding. But when you're making a complex game, it more than makes up for it.
The next system we're going to implement is a level up system. In an RPG, it is common for the player to gain experience points, and once their experience reaches a certain level, the character grows in power.
For this RPG, you're going to have the player gain experience points when they defeat an enemy, and once they reach a certain amount of experience points they will go to the next level and have more health and attack power.
Create some new FloatVariables with these values:
PlayerStartAttack: 5
PlayerStartMaxHealth: 50
PlayerStartLevel: 1
PlayerStartEXPToNextLevel: 10
PlayerLevel: 1
PlayerEXPToNextLevel: 10
PlayerEXP: 0
MonsterEXP: 10
You'll notice we have two different types of variables. The
Startvariables will allow us to reset our variables when the game restarts
Update the Player script to include logic about EXP.
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class Player : MonoBehaviour { public FloatVariable playerMaxHealth; public FloatVariable playerHealth; public FloatVariable playerAttack; public FloatVariable playerStartAttack; public FloatVariable playerLevel; public FloatVariable playerStartLevel; public FloatVariable playerEXP; public FloatVariable playerStartEXPToNextLevel; public FloatVariable playerEXPToNextLevel; public void Start() { resetVariables(); } public void Update() { if (playerHealth.Value <= 0) { //Reset the Player Health and start the scene over again. resetVariables(); SceneManager.LoadScene(SceneManager.GetActiveScene().name); } if (playerEXP.Value >= playerEXPToNextLevel.Value) { playerEXP.Value = playerEXP.Value - playerEXPToNextLevel.Value; playerLevel.Value++; playerEXPToNextLevel.Value += 5f; playerAttack.Value += 2; playerMaxHealth.Value += 10; playerHealth.Value = playerMaxHealth.Value; } } private void resetVariables() { playerHealth.Value = playerMaxHealth.Value; playerAttack.Value = playerStartAttack.Value; playerLevel.Value = playerStartLevel.Value; playerEXP.Value = 0f; playerEXPToNextLevel.Value = playerStartEXPToNextLevel.Value; } }
Add additional variables to the MonsterBehavior script.
public FloatVariable monsterMaxHealth; public float monsterCurrentHealth; public FloatVariable monsterAttack; public FloatVariable playerHealth; public FloatVariable playerAttack; public FloatVariable monsterEXP; public FloatVariable playerEXP;
Add logic to give the player EXP when they defeat the monster
void OnCollisionEnter(Collision other) { if (other.gameObject == player) { playerHealth.Value -= monsterAttack.Value; } else if (other.gameObject.tag == "Blade") { monsterCurrentHealth -= playerAttack.Value; if (monsterCurrentHealth <= 0) { playerEXP.Value += monsterEXP.Value; Destroy(this.gameObject); } } }
Hook up the variables, and add more monsters out in the world.
To make it clearer to the player they are gaining EXP, change the UIManager to watch the amount of EXP.
public Image expBar; public FloatVariable playerEXP; public FloatVariable playerEXPToNextLevel; private void Update() { attackText.text = playerAttack.Value.ToString(); float healthPercent = playerHealth.Value / playerMaxHealth.Value; healthBar.transform.localScale = new Vector3(healthPercent, 1f); float expPercent = playerEXP.Value / playerEXPToNextLevel.Value; expBar.transform.localScale = new Vector3(expPercent, 1f); }
Hook up the necessary variables, and play the game again to see the UI update
Now our game allows a bit more difficulty, as well as some progression with EXP. We also have a UI to keep track of our player health and EXP. Next we'll add in a way to spawn more monsters and make Bosses.