Hello everyone! In this post, we are going to add the previously created skill tree to our game. In this case, I’m going to use the Survival Shooter from Unity tutorials. So stick to this post if you want to achieve something as freaking cool as this.

Introduction

Let’s get started by downloading and importing the project from the Asset Store (or just select your own project if you want to add it directly).

Once we have our project loaded we need to think about the things we want to add as skills. In my case, I’m going to improve the character’s health and movement speed, also I’m going to improve the weapon’s cadence.

Of course, you can add your own improvement to the character or even add new mechanics linked to our skill tree system. As I am focused on the skill tree, I will not create new mechanics for this game. But hopefully, you will be able to get the idea about how to work with the skill tree and add your own behaviors linked to it.

Alright, we have our project, let’s import the unity package with the skill tree system.

From now on, I will be working on the scene _Complete-Game from the Survival Shooter Tutorial (Unity). By the way, in order to have a better feedback on the skill improvements, I added a text over the life slider printing the current and the maximum life of the player.

Creating the Skill Tree

Let’s start by creating the skill tree. For this, you only need to open the Node Based Editor from the previous tutorial. This is my final skill tree:

I’m going to use the IDs 1 and 4 for the health improvement, the IDs 2 and 5 for the speed movement upgrades and the IDs 3 and 6 for the weapon’s cadence improvements. But it’s totally up to you if you want to change it or add even more skills, just be careful and use the correct ID for each improvement.

Checking and applying the enhancements

The first thing we need to do is to check the skill tree of the player and provide the correct improvement. In my case, I need to check for the player’s health, speed and weapon’s cadence.

We need to modify the PlayerHealth script and set a different maximum life if the player has some of the skills unlocked. In this game, we can’t recover health so we can check it on the Awake method and forget about it if the player unlocks the skill, it will be applied in the next match.

Warning: Be careful, because there are duplicates scripts in the asset. Every time I refer to a script from the asset I’m talking about the one located in the _Complete-Game/Scripts folder.

PlayerHealth.cs using UnityEngine; using UnityEngine.UI; using System.Collections; using UnityEngine.SceneManagement; namespace CompleteProject { public class PlayerHealth : MonoBehaviour { public int startingHealth = 100; // The amount of health the player starts the game with. public int currentHealth; // The current health the player has. public Slider healthSlider; // Reference to the UI's health bar. public Text currentHealthText; public Text maxHealthText; public Image damageImage; // Reference to an image to flash on the screen on being hurt. public AudioClip deathClip; // The audio clip to play when the player dies. public float flashSpeed = 5f; // The speed the damageImage will fade at. public Color flashColour = new Color(1f, 0f, 0f, 0.1f); // The colour the damageImage is set to, to flash. Animator anim; // Reference to the Animator component. AudioSource playerAudio; // Reference to the AudioSource component. PlayerMovement playerMovement; // Reference to the player's movement. PlayerShooting playerShooting; // Reference to the PlayerShooting script. bool isDead; // Whether the player is dead. bool damaged; // True when the player gets damaged. void Awake () { // Setting up the references. anim = GetComponent <Animator> (); playerAudio = GetComponent <AudioSource> (); playerMovement = GetComponent <PlayerMovement> (); playerShooting = GetComponentInChildren <PlayerShooting> (); if (SkillTreeReader.Instance.IsSkillUnlocked(4)) { startingHealth *= 3; } else if (SkillTreeReader.Instance.IsSkillUnlocked(1)) { startingHealth *= 2; } // Set the initial health of the player. currentHealth = startingHealth; currentHealthText.text = currentHealth.ToString(); maxHealthText.text = startingHealth.ToString(); } void Update () { // If the player has just been damaged... if(damaged) { // ... set the colour of the damageImage to the flash colour. damageImage.color = flashColour; } // Otherwise... else { // ... transition the colour back to clear. damageImage.color = Color.Lerp (damageImage.color, Color.clear, flashSpeed * Time.deltaTime); } // Reset the damaged flag. damaged = false; } public void TakeDamage (int amount) { // Set the damaged flag so the screen will flash. damaged = true; // Reduce the current health by the damage amount. currentHealth -= amount; // Set the health bar's value to the current health. healthSlider.value = currentHealth; currentHealthText.text = currentHealth.ToString(); // Play the hurt sound effect. playerAudio.Play (); // If the player has lost all it's health and the death flag hasn't been set yet... if(currentHealth <= 0 && !isDead) { // ... it should die. Death (); } } void Death () { // Set the death flag so this function won't be called again. isDead = true; // Turn off any remaining shooting effects. playerShooting.DisableEffects (); // Tell the animator that the player is dead. anim.SetTrigger ("Die"); // Set the audiosource to play the death clip and play it (this will stop the hurt sound from playing). playerAudio.clip = deathClip; playerAudio.Play (); // Turn off the movement and shooting scripts. playerMovement.enabled = false; playerShooting.enabled = false; } public void RestartLevel () { // Reload the level that is currently loaded. SceneManager.LoadScene (0); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 using UnityEngine ; using UnityEngine . UI ; using System . Collections ; using UnityEngine . SceneManagement ; namespace CompleteProject { public class PlayerHealth : MonoBehaviour { public int startingHealth = 100 ; // The amount of health the player starts the game with. public int currentHealth ; // The current health the player has. public Slider healthSlider ; // Reference to the UI's health bar. public Text currentHealthText ; public Text maxHealthText ; public Image damageImage ; // Reference to an image to flash on the screen on being hurt. public AudioClip deathClip ; // The audio clip to play when the player dies. public float flashSpeed = 5f ; // The speed the damageImage will fade at. public Color flashColour = new Color ( 1f , 0f , 0f , 0.1f ) ; // The colour the damageImage is set to, to flash. Animator anim ; // Reference to the Animator component. AudioSource playerAudio ; // Reference to the AudioSource component. PlayerMovement playerMovement ; // Reference to the player's movement. PlayerShooting playerShooting ; // Reference to the PlayerShooting script. bool isDead ; // Whether the player is dead. bool damaged ; // True when the player gets damaged. void Awake ( ) { // Setting up the references. anim = GetComponent < Animator > ( ) ; playerAudio = GetComponent < AudioSource > ( ) ; playerMovement = GetComponent < PlayerMovement > ( ) ; playerShooting = GetComponentInChildren < PlayerShooting > ( ) ; if ( SkillTreeReader . Instance . IsSkillUnlocked ( 4 ) ) { startingHealth * = 3 ; } else if ( SkillTreeReader . Instance . IsSkillUnlocked ( 1 ) ) { startingHealth * = 2 ; } // Set the initial health of the player. currentHealth = startingHealth ; currentHealthText . text = currentHealth . ToString ( ) ; maxHealthText . text = startingHealth . ToString ( ) ; } void Update ( ) { // If the player has just been damaged... if ( damaged ) { // ... set the colour of the damageImage to the flash colour. damageImage . color = flashColour ; } // Otherwise... else { // ... transition the colour back to clear. damageImage . color = Color . Lerp ( damageImage . color , Color . clear , flashSpeed * Time . deltaTime ) ; } // Reset the damaged flag. damaged = false ; } public void TakeDamage ( int amount ) { // Set the damaged flag so the screen will flash. damaged = true ; // Reduce the current health by the damage amount. currentHealth -= amount ; // Set the health bar's value to the current health. healthSlider . value = currentHealth ; currentHealthText . text = currentHealth . ToString ( ) ; // Play the hurt sound effect. playerAudio . Play ( ) ; // If the player has lost all it's health and the death flag hasn't been set yet... if ( currentHealth <= 0 && ! isDead ) { // ... it should die. Death ( ) ; } } void Death ( ) { // Set the death flag so this function won't be called again. isDead = true ; // Turn off any remaining shooting effects. playerShooting . DisableEffects ( ) ; // Tell the animator that the player is dead. anim . SetTrigger ( "Die" ) ; // Set the audiosource to play the death clip and play it (this will stop the hurt sound from playing). playerAudio . clip = deathClip ; playerAudio . Play ( ) ; // Turn off the movement and shooting scripts. playerMovement . enabled = false ; playerShooting . enabled = false ; } public void RestartLevel ( ) { // Reload the level that is currently loaded. SceneManager . LoadScene ( 0 ) ; } } }

In order to apply the skills for movement speed, we need to edit the PlayerMovement script. As you can see in the code below I am checking every frame if the player has unlocked the skill and applying the changes. But you can make the check once and store the result in order to not checking it every frame. Just keep in mind that you need to check it when the game resumes from pause menu in case the player bought any new skill.

PlayerMovement.cs using UnityEngine; using UnitySampleAssets.CrossPlatformInput; namespace CompleteProject { public class PlayerMovement : MonoBehaviour { public float speed = 6f; // The speed that the player will move at. private float baseSpeed; Vector3 movement; // The vector to store the direction of the player's movement. Animator anim; // Reference to the animator component. Rigidbody playerRigidbody; // Reference to the player's rigidbody. #if !MOBILE_INPUT int floorMask; // A layer mask so that a ray can be cast just at gameobjects on the floor layer. float camRayLength = 100f; // The length of the ray from the camera into the scene. #endif void Awake () { #if !MOBILE_INPUT // Create a layer mask for the floor layer. floorMask = LayerMask.GetMask ("Floor"); #endif // Set up references. anim = GetComponent <Animator> (); playerRigidbody = GetComponent <Rigidbody> (); baseSpeed = speed; } void FixedUpdate () { // Store the input axes. float h = CrossPlatformInputManager.GetAxisRaw("Horizontal"); float v = CrossPlatformInputManager.GetAxisRaw("Vertical"); // Move the player around the scene. Move (h, v); // Turn the player to face the mouse cursor. Turning (); // Animate the player. Animating (h, v); } void Move (float h, float v) { // Set the movement vector based on the axis input. movement.Set (h, 0f, v); if (SkillTreeReader.Instance.IsSkillUnlocked(5)) { speed = baseSpeed * 2; } else if (SkillTreeReader.Instance.IsSkillUnlocked(2)) { speed = baseSpeed * 1.3f; } // Normalise the movement vector and make it proportional to the speed per second. movement = movement.normalized * speed * Time.deltaTime; // Move the player to it's current position plus the movement. playerRigidbody.MovePosition (transform.position + movement); } void Turning () { #if !MOBILE_INPUT // Create a ray from the mouse cursor on screen in the direction of the camera. Ray camRay = Camera.main.ScreenPointToRay (Input.mousePosition); // Create a RaycastHit variable to store information about what was hit by the ray. RaycastHit floorHit; // Perform the raycast and if it hits something on the floor layer... if(Physics.Raycast (camRay, out floorHit, camRayLength, floorMask)) { // Create a vector from the player to the point on the floor the raycast from the mouse hit. Vector3 playerToMouse = floorHit.point - transform.position; // Ensure the vector is entirely along the floor plane. playerToMouse.y = 0f; // Create a quaternion (rotation) based on looking down the vector from the player to the mouse. Quaternion newRotatation = Quaternion.LookRotation (playerToMouse); // Set the player's rotation to this new rotation. playerRigidbody.MoveRotation (newRotatation); } #else Vector3 turnDir = new Vector3(CrossPlatformInputManager.GetAxisRaw("Mouse X") , 0f , CrossPlatformInputManager.GetAxisRaw("Mouse Y")); if (turnDir != Vector3.zero) { // Create a vector from the player to the point on the floor the raycast from the mouse hit. Vector3 playerToMouse = (transform.position + turnDir) - transform.position; // Ensure the vector is entirely along the floor plane. playerToMouse.y = 0f; // Create a quaternion (rotation) based on looking down the vector from the player to the mouse. Quaternion newRotatation = Quaternion.LookRotation(playerToMouse); // Set the player's rotation to this new rotation. playerRigidbody.MoveRotation(newRotatation); } #endif } void Animating (float h, float v) { // Create a boolean that is true if either of the input axes is non-zero. bool walking = h != 0f || v != 0f; // Tell the animator whether or not the player is walking. anim.SetBool ("IsWalking", walking); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 using UnityEngine ; using UnitySampleAssets . CrossPlatformInput ; namespace CompleteProject { public class PlayerMovement : MonoBehaviour { public float speed = 6f ; // The speed that the player will move at. private float baseSpeed ; Vector3 movement ; // The vector to store the direction of the player's movement. Animator anim ; // Reference to the animator component. Rigidbody playerRigidbody ; // Reference to the player's rigidbody. #if !MOBILE_INPUT int floorMask ; // A layer mask so that a ray can be cast just at gameobjects on the floor layer. float camRayLength = 100f ; // The length of the ray from the camera into the scene. #endif void Awake ( ) { #if !MOBILE_INPUT // Create a layer mask for the floor layer. floorMask = LayerMask . GetMask ( "Floor" ) ; #endif // Set up references. anim = GetComponent < Animator > ( ) ; playerRigidbody = GetComponent < Rigidbody > ( ) ; baseSpeed = speed ; } void FixedUpdate ( ) { // Store the input axes. float h = CrossPlatformInputManager . GetAxisRaw ( "Horizontal" ) ; float v = CrossPlatformInputManager . GetAxisRaw ( "Vertical" ) ; // Move the player around the scene. Move ( h , v ) ; // Turn the player to face the mouse cursor. Turning ( ) ; // Animate the player. Animating ( h , v ) ; } void Move ( float h , float v ) { // Set the movement vector based on the axis input. movement . Set ( h , 0f , v ) ; if ( SkillTreeReader . Instance . IsSkillUnlocked ( 5 ) ) { speed = baseSpeed * 2 ; } else if ( SkillTreeReader . Instance . IsSkillUnlocked ( 2 ) ) { speed = baseSpeed * 1.3f ; } // Normalise the movement vector and make it proportional to the speed per second. movement = movement . normalized * speed * Time . deltaTime ; // Move the player to it's current position plus the movement. playerRigidbody . MovePosition ( transform . position + movement ) ; } void Turning ( ) { #if !MOBILE_INPUT // Create a ray from the mouse cursor on screen in the direction of the camera. Ray camRay = Camera . main . ScreenPointToRay ( Input . mousePosition ) ; // Create a RaycastHit variable to store information about what was hit by the ray. RaycastHit floorHit ; // Perform the raycast and if it hits something on the floor layer... if ( Physics . Raycast ( camRay , out floorHit , camRayLength , floorMask ) ) { // Create a vector from the player to the point on the floor the raycast from the mouse hit. Vector3 playerToMouse = floorHit . point - transform . position ; // Ensure the vector is entirely along the floor plane. playerToMouse . y = 0f ; // Create a quaternion (rotation) based on looking down the vector from the player to the mouse. Quaternion newRotatation = Quaternion . LookRotation ( playerToMouse ) ; // Set the player's rotation to this new rotation. playerRigidbody . MoveRotation ( newRotatation ) ; } #else Vector3 turnDir = new Vector3 ( CrossPlatformInputManager . GetAxisRaw ( "Mouse X" ) , 0f , CrossPlatformInputManager . GetAxisRaw ( "Mouse Y" ) ) ; if ( turnDir != Vector3 . zero ) { // Create a vector from the player to the point on the floor the raycast from the mouse hit. Vector3 playerToMouse = ( transform . position + turnDir ) - transform . position ; // Ensure the vector is entirely along the floor plane. playerToMouse . y = 0f ; // Create a quaternion (rotation) based on looking down the vector from the player to the mouse. Quaternion newRotatation = Quaternion . LookRotation ( playerToMouse ) ; // Set the player's rotation to this new rotation. playerRigidbody . MoveRotation ( newRotatation ) ; } #endif } void Animating ( float h , float v ) { // Create a boolean that is true if either of the input axes is non-zero. bool walking = h != 0f || v != 0f ; // Tell the animator whether or not the player is walking. anim . SetBool ( "IsWalking" , walking ) ; } } }

And finally, we must check the skills to improve our weapon. For that, we need to edit the PlayerShooting script.

PlayerShooting using UnityEngine; using UnitySampleAssets.CrossPlatformInput; namespace CompleteProject { public class PlayerShooting : MonoBehaviour { public int damagePerShot = 20; // The damage inflicted by each bullet. public float timeBetweenBullets = 0.15f; // The time between each shot. public float range = 100f; // The distance the gun can fire. float timer; // A timer to determine when to fire. Ray shootRay = new Ray(); // A ray from the gun end forwards. RaycastHit shootHit; // A raycast hit to get information about what was hit. int shootableMask; // A layer mask so the raycast only hits things on the shootable layer. ParticleSystem gunParticles; // Reference to the particle system. LineRenderer gunLine; // Reference to the line renderer. AudioSource gunAudio; // Reference to the audio source. Light gunLight; // Reference to the light component. public Light faceLight; // Duh float effectsDisplayTime = 0.2f; // The proportion of the timeBetweenBullets that the effects will display for. private float timeBetweenBulletsBase; void Awake () { // Create a layer mask for the Shootable layer. shootableMask = LayerMask.GetMask ("Shootable"); // Set up the references. gunParticles = GetComponent<ParticleSystem> (); gunLine = GetComponent <LineRenderer> (); gunAudio = GetComponent<AudioSource> (); gunLight = GetComponent<Light> (); //faceLight = GetComponentInChildren<Light> (); timeBetweenBulletsBase = timeBetweenBullets; } void Update () { // Add the time since Update was last called to the timer. timer += Time.deltaTime; if (SkillTreeReader.Instance.IsSkillUnlocked(6)) { timeBetweenBullets = timeBetweenBulletsBase / 2; } else if (SkillTreeReader.Instance.IsSkillUnlocked(3)) { timeBetweenBullets = timeBetweenBulletsBase / 1.3f; } #if !MOBILE_INPUT // If the Fire1 button is being press and it's time to fire... if (Input.GetButton ("Fire1") && timer >= timeBetweenBullets && Time.timeScale != 0) { // ... shoot the gun. Shoot (); } #else // If there is input on the shoot direction stick and it's time to fire... if ((CrossPlatformInputManager.GetAxisRaw("Mouse X") != 0 || CrossPlatformInputManager.GetAxisRaw("Mouse Y") != 0) && timer >= timeBetweenBullets) { // ... shoot the gun Shoot(); } #endif // If the timer has exceeded the proportion of timeBetweenBullets that the effects should be displayed for... if(timer >= timeBetweenBullets * effectsDisplayTime) { // ... disable the effects. DisableEffects (); } } public void DisableEffects () { // Disable the line renderer and the light. gunLine.enabled = false; faceLight.enabled = false; gunLight.enabled = false; } void Shoot () { // Reset the timer. timer = 0f; // Play the gun shot audioclip. gunAudio.Play (); // Enable the lights. gunLight.enabled = true; faceLight.enabled = true; // Stop the particles from playing if they were, then start the particles. gunParticles.Stop (); gunParticles.Play (); // Enable the line renderer and set it's first position to be the end of the gun. gunLine.enabled = true; gunLine.SetPosition (0, transform.position); // Set the shootRay so that it starts at the end of the gun and points forward from the barrel. shootRay.origin = transform.position; shootRay.direction = transform.forward; // Perform the raycast against gameobjects on the shootable layer and if it hits something... if(Physics.Raycast (shootRay, out shootHit, range, shootableMask)) { // Try and find an EnemyHealth script on the gameobject hit. EnemyHealth enemyHealth = shootHit.collider.GetComponent <EnemyHealth> (); // If the EnemyHealth component exist... if(enemyHealth != null) { // ... the enemy should take damage. enemyHealth.TakeDamage (damagePerShot, shootHit.point); } // Set the second position of the line renderer to the point the raycast hit. gunLine.SetPosition (1, shootHit.point); } // If the raycast didn't hit anything on the shootable layer... else { // ... set the second position of the line renderer to the fullest extent of the gun's range. gunLine.SetPosition (1, shootRay.origin + shootRay.direction * range); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 using UnityEngine ; using UnitySampleAssets . CrossPlatformInput ; namespace CompleteProject { public class PlayerShooting : MonoBehaviour { public int damagePerShot = 20 ; // The damage inflicted by each bullet. public float timeBetweenBullets = 0.15f ; // The time between each shot. public float range = 100f ; // The distance the gun can fire. float timer ; // A timer to determine when to fire. Ray shootRay = new Ray ( ) ; // A ray from the gun end forwards. RaycastHit shootHit ; // A raycast hit to get information about what was hit. int shootableMask ; // A layer mask so the raycast only hits things on the shootable layer. ParticleSystem gunParticles ; // Reference to the particle system. LineRenderer gunLine ; // Reference to the line renderer. AudioSource gunAudio ; // Reference to the audio source. Light gunLight ; // Reference to the light component. public Light faceLight ; // Duh float effectsDisplayTime = 0.2f ; // The proportion of the timeBetweenBullets that the effects will display for. private float timeBetweenBulletsBase ; void Awake ( ) { // Create a layer mask for the Shootable layer. shootableMask = LayerMask . GetMask ( "Shootable" ) ; // Set up the references. gunParticles = GetComponent < ParticleSystem > ( ) ; gunLine = GetComponent < LineRenderer > ( ) ; gunAudio = GetComponent < AudioSource > ( ) ; gunLight = GetComponent < Light > ( ) ; //faceLight = GetComponentInChildren<Light> (); timeBetweenBulletsBase = timeBetweenBullets ; } void Update ( ) { // Add the time since Update was last called to the timer. timer += Time . deltaTime ; if ( SkillTreeReader . Instance . IsSkillUnlocked ( 6 ) ) { timeBetweenBullets = timeBetweenBulletsBase / 2 ; } else if ( SkillTreeReader . Instance . IsSkillUnlocked ( 3 ) ) { timeBetweenBullets = timeBetweenBulletsBase / 1.3f ; } #if !MOBILE_INPUT // If the Fire1 button is being press and it's time to fire... if ( Input . GetButton ( "Fire1" ) && timer >= timeBetweenBullets && Time . timeScale != 0 ) { // ... shoot the gun. Shoot ( ) ; } #else // If there is input on the shoot direction stick and it's time to fire... if ( ( CrossPlatformInputManager . GetAxisRaw ( "Mouse X" ) != 0 || CrossPlatformInputManager . GetAxisRaw ( "Mouse Y" ) != 0 ) && timer >= timeBetweenBullets ) { // ... shoot the gun Shoot ( ) ; } #endif // If the timer has exceeded the proportion of timeBetweenBullets that the effects should be displayed for... if ( timer >= timeBetweenBullets * effectsDisplayTime ) { // ... disable the effects. DisableEffects ( ) ; } } public void DisableEffects ( ) { // Disable the line renderer and the light. gunLine . enabled = false ; faceLight . enabled = false ; gunLight . enabled = false ; } void Shoot ( ) { // Reset the timer. timer = 0f ; // Play the gun shot audioclip. gunAudio . Play ( ) ; // Enable the lights. gunLight . enabled = true ; faceLight . enabled = true ; // Stop the particles from playing if they were, then start the particles. gunParticles . Stop ( ) ; gunParticles . Play ( ) ; // Enable the line renderer and set it's first position to be the end of the gun. gunLine . enabled = true ; gunLine . SetPosition ( 0 , transform . position ) ; // Set the shootRay so that it starts at the end of the gun and points forward from the barrel. shootRay . origin = transform . position ; shootRay . direction = transform . forward ; // Perform the raycast against gameobjects on the shootable layer and if it hits something... if ( Physics . Raycast ( shootRay , out shootHit , range , shootableMask ) ) { // Try and find an EnemyHealth script on the gameobject hit. EnemyHealth enemyHealth = shootHit . collider . GetComponent < EnemyHealth > ( ) ; // If the EnemyHealth component exist... if ( enemyHealth != null ) { // ... the enemy should take damage. enemyHealth . TakeDamage ( damagePerShot , shootHit . point ) ; } // Set the second position of the line renderer to the point the raycast hit. gunLine . SetPosition ( 1 , shootHit . point ) ; } // If the raycast didn't hit anything on the shootable layer... else { // ... set the second position of the line renderer to the fullest extent of the gun's range. gunLine . SetPosition ( 1 , shootRay . origin + shootRay . direction * range ) ; } } } }

So that’s all, the game already changes when we set a different skill tree config, but right now the only way of changing it is within the editor (unlocking with the node based editor) and the player wouldn’t be capable of unlocking skills. Then, the next step is creating a basic menu for unlocking skills.

Creating the skill tree menu

We are not going to make a whole new menu for this. I just made the old pause menu a little bit smaller and added some buttons for each skill (6 in my game). This would be the result:

I also created a new component called SkillButton. This component takes care of the state of the skill (if it’s unlocked, if it can be unlocked or not) in order to give feedback to the players of their current skill tree state.

This component should be set with an ID of the skill that it represents, a color to make the button look different when the skill is unlocked and a reference to a SkillHub. This is a new component that should be called when a new skill is bought in order to refresh all of the skill buttons, just in case a new skill can be unlocked now.

SkillButton.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class SkillButton : MonoBehaviour { public int skillId; public Color unlockedColor; public SkillHub skillHub; private Image _image; private Button _button; void Start () { _image = GetComponent<Image>(); _button = GetComponent<Button>(); RefreshState(); } public void RefreshState() { if (SkillTreeReader.Instance.IsSkillUnlocked(skillId)) { _image.color = unlockedColor; } else if (!SkillTreeReader.Instance.CanSkillBeUnlocked(skillId)) { _button.interactable = false; } else { _image.color = Color.white; _button.interactable = true; } } public void BuySkill() { if (SkillTreeReader.Instance.UnlockSkill(skillId)) { skillHub.RefreshButtons(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; using UnityEngine . UI ; public class SkillButton : MonoBehaviour { public int skillId ; public Color unlockedColor ; public SkillHub skillHub ; private Image _image ; private Button _button ; void Start ( ) { _image = GetComponent < Image > ( ) ; _button = GetComponent < Button > ( ) ; RefreshState ( ) ; } public void RefreshState ( ) { if ( SkillTreeReader . Instance . IsSkillUnlocked ( skillId ) ) { _image . color = unlockedColor ; } else if ( ! SkillTreeReader . Instance . CanSkillBeUnlocked ( skillId ) ) { _button . interactable = false ; } else { _image . color = Color . white ; _button . interactable = true ; } } public void BuySkill ( ) { if ( SkillTreeReader . Instance . UnlockSkill ( skillId ) ) { skillHub . RefreshButtons ( ) ; } } }

SkillHub.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class SkillHub : MonoBehaviour { public SkillButton[] skillsButton; public void RefreshButtons() { for(int i = 0; i < skillsButton.Length; ++i) { skillsButton[i].RefreshState(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; public class SkillHub : MonoBehaviour { public SkillButton [ ] skillsButton ; public void RefreshButtons ( ) { for ( int i = 0 ; i < skillsButton . Length ; ++ i ) { skillsButton [ i ] . RefreshState ( ) ; } } }

With all this, the player should be able to buy new skills. But we didn’t give any points to the player in order to buy this! Let’s fix this.

Skill points management

Every time we finish a match we are going to add the score of the game to the skill points. For this, we are going to edit the ScoreManager script.

ScoreManager.cs using UnityEngine; using UnityEngine.UI; using System.Collections; namespace CompleteProject { public class ScoreManager : MonoBehaviour { public static int score; // The player's score. Text text; // Reference to the Text component. void Awake () { // Set up the reference. text = GetComponent <Text> (); // Reset the score. score = 0; SkillTreeReader.Instance.availablePoints = PlayerPrefs.GetInt("Score", 0); } void Update () { // Set the displayed text to be the word "Score" followed by the score value. text.text = "Score: " + score; } void OnDestroy() { PlayerPrefs.SetInt("Score", PlayerPrefs.GetInt("Score", 0) + score); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 using UnityEngine ; using UnityEngine . UI ; using System . Collections ; namespace CompleteProject { public class ScoreManager : MonoBehaviour { public static int score ; // The player's score. Text text ; // Reference to the Text component. void Awake ( ) { // Set up the reference. text = GetComponent < Text > ( ) ; // Reset the score. score = 0 ; SkillTreeReader . Instance . availablePoints = PlayerPrefs . GetInt ( "Score" , 0 ) ; } void Update ( ) { // Set the displayed text to be the word "Score" followed by the score value. text . text = "Score: " + score ; } void OnDestroy ( ) { PlayerPrefs . SetInt ( "Score" , PlayerPrefs . GetInt ( "Score" , 0 ) + score ) ; } } }

With this, every time the game starts the player loads the available skill points. And every time the game finishes we save the current score as skill points. With this, the player already should be able to buy new skills. But we aren’t correctly setting the current skill points after buying! The players could buy everything without wasting their points. Let’s fix this undesired behavior.

SkillButton.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class SkillButton : MonoBehaviour { public int skillId; public Color unlockedColor; public SkillHub skillHub; private Image _image; private Button _button; void Start () { _image = GetComponent<Image>(); _button = GetComponent<Button>(); RefreshState(); } public void RefreshState() { if (SkillTreeReader.Instance.IsSkillUnlocked(skillId)) { _image.color = unlockedColor; } else if (!SkillTreeReader.Instance.CanSkillBeUnlocked(skillId)) { _button.interactable = false; } else { _image.color = Color.white; _button.interactable = true; } } public void BuySkill() { if (SkillTreeReader.Instance.UnlockSkill(skillId)) { PlayerPrefs.SetInt("Score", SkillTreeReader.Instance.availablePoints); skillHub.RefreshButtons(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; using UnityEngine . UI ; public class SkillButton : MonoBehaviour { public int skillId ; public Color unlockedColor ; public SkillHub skillHub ; private Image _image ; private Button _button ; void Start ( ) { _image = GetComponent < Image > ( ) ; _button = GetComponent < Button > ( ) ; RefreshState ( ) ; } public void RefreshState ( ) { if ( SkillTreeReader . Instance . IsSkillUnlocked ( skillId ) ) { _image . color = unlockedColor ; } else if ( ! SkillTreeReader . Instance . CanSkillBeUnlocked ( skillId ) ) { _button . interactable = false ; } else { _image . color = Color . white ; _button . interactable = true ; } } public void BuySkill ( ) { if ( SkillTreeReader . Instance . UnlockSkill ( skillId ) ) { PlayerPrefs . SetInt ( "Score" , SkillTreeReader . Instance . availablePoints ) ; skillHub . RefreshButtons ( ) ; } } }

Now, every time the player buys a new skill we set the correct skill points available to our game.

I also added a new component just for printing the currently available points in our menu. Something quick and simple like this should work:

SkillPoints.cs using UnityEngine; using UnityEngine.UI; public class SkillPoints : MonoBehaviour { private Text skillText; void Start () { skillText = GetComponent<Text>(); } void Update() { skillText.text = PlayerPrefs.GetInt("Score", 0).ToString(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using UnityEngine ; using UnityEngine . UI ; public class SkillPoints : MonoBehaviour { private Text skillText ; void Start ( ) { skillText = GetComponent < Text > ( ) ; } void Update ( ) { skillText . text = PlayerPrefs . GetInt ( "Score" , 0 ) . ToString ( ) ; } }

Right now, the player can play, earn points and unlock new skills, that’s perfect. But… what if the player stops playing and relaunches the game? We aren’t saving the skill tree state so the player loses everything that he or she earned by playing our game. And we don’t want to piss our players, am I right?

Saving the Skill Tree state

For this last part of the tutorial, we are going to use the Unity PlayerPrefs. We will store the skill tree JSON string on it, and if it doesn’t exist we are going to load the original SkillTree config. So we are going to edit the SkillTreeReader script adding a few methods for loading the player state of the Skill Tree and for saving it.

SkillTreeReader.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using System.IO; public class SkillTreeReader : MonoBehaviour { private static SkillTreeReader _instance; public static SkillTreeReader Instance { get { return _instance; } set { } } // Array with all the skills in our skilltree private Skill[] _skillTree; // Dictionary with the skills in our skilltree private Dictionary<int, Skill> _skills; // Variable for caching the currently being inspected skill private Skill _skillInspected; public int availablePoints = 100; void Awake() { if(_instance == null) { _instance = this; DontDestroyOnLoad(this.gameObject); SetUpSkillTree(); } else { Destroy(this.gameObject); } } // Use this for initialization of the skill tree void SetUpSkillTree () { _skills = new Dictionary<int, Skill>(); if(PlayerPrefs.GetString("SkillTree", "").CompareTo("") == 0) { LoadSkillTree(); } else { LoadPlayerSkillTree(); } } public void LoadSkillTree() { string path = "Assets/SkillTree/Data/skilltree.json"; string dataAsJson; if (File.Exists(path)) { // Read the json from the file into a string dataAsJson = File.ReadAllText(path); // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility.FromJson<SkillTree>(dataAsJson); // Store the SkillTree as an array of Skill _skillTree = new Skill[loadedData.skilltree.Length]; _skillTree = loadedData.skilltree; // Populate a dictionary with the skill id and the skill data itself for (int i = 0; i < _skillTree.Length; ++i) { _skills.Add(_skillTree[i].id_Skill, _skillTree[i]); } } else { Debug.LogError("Cannot load game data!"); } } public void LoadPlayerSkillTree() { string dataAsJson = PlayerPrefs.GetString("SkillTree"); // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility.FromJson<SkillTree>(dataAsJson); // Store the SkillTree as an array of Skill _skillTree = new Skill[loadedData.skilltree.Length]; _skillTree = loadedData.skilltree; // Populate a dictionary with the skill id and the skill data itself for (int i = 0; i < _skillTree.Length; ++i) { _skills.Add(_skillTree[i].id_Skill, _skillTree[i]); } } public void SaveSkillTree() { // We fill with as many skills as nodes we have SkillTree skillTree = new SkillTree(); skillTree.skilltree = new Skill[_skillTree.Length]; for (int i = 0; i < _skillTree.Length; ++i) { _skills.TryGetValue(_skillTree[i].id_Skill, out _skillInspected); if(_skillInspected != null) { skillTree.skilltree[i] = _skillInspected; } } string json = JsonUtility.ToJson(skillTree); PlayerPrefs.SetString("SkillTree", json); } public bool IsSkillUnlocked(int id_skill) { if (_skills.TryGetValue(id_skill, out _skillInspected)) { return _skillInspected.unlocked; } else { return false; } } public bool CanSkillBeUnlocked(int id_skill) { bool canUnlock = true; if(_skills.TryGetValue(id_skill, out _skillInspected)) // The skill exists { if(_skillInspected.cost <= availablePoints) // Enough points available { int[] dependencies = _skillInspected.skill_Dependencies; for (int i = 0; i < dependencies.Length; ++i) { if (_skills.TryGetValue(dependencies[i], out _skillInspected)) { if (!_skillInspected.unlocked) { canUnlock = false; break; } } else // If one of the dependencies doesn't exist, the skill can't be unlocked. { return false; } } } else // If the player doesn't have enough skill points, can't unlock the new skill { return false; } } else // If the skill id doesn't exist, the skill can't be unlocked { return false; } return canUnlock; } public bool UnlockSkill(int id_Skill) { if(_skills.TryGetValue(id_Skill, out _skillInspected)) { if (_skillInspected.cost <= availablePoints) { availablePoints -= _skillInspected.cost; _skillInspected.unlocked = true; // We replace the entry on the dictionary with the new one (already unlocked) _skills.Remove(id_Skill); _skills.Add(id_Skill, _skillInspected); return true; } else { return false; // The skill can't be unlocked. Not enough points } } else { return false; // The skill doesn't exist } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; using System . IO ; public class SkillTreeReader : MonoBehaviour { private static SkillTreeReader _instance ; public static SkillTreeReader Instance { get { return _instance ; } set { } } // Array with all the skills in our skilltree private Skill [ ] _skillTree ; // Dictionary with the skills in our skilltree private Dictionary < int , Skill > _skills ; // Variable for caching the currently being inspected skill private Skill _skillInspected ; public int availablePoints = 100 ; void Awake ( ) { if ( _instance == null ) { _instance = this ; DontDestroyOnLoad ( this . gameObject ) ; SetUpSkillTree ( ) ; } else { Destroy ( this . gameObject ) ; } } // Use this for initialization of the skill tree void SetUpSkillTree ( ) { _skills = new Dictionary < int , Skill > ( ) ; if ( PlayerPrefs . GetString ( "SkillTree" , "" ) . CompareTo ( "" ) == 0 ) { LoadSkillTree ( ) ; } else { LoadPlayerSkillTree ( ) ; } } public void LoadSkillTree ( ) { string path = "Assets/SkillTree/Data/skilltree.json" ; string dataAsJson ; if ( File . Exists ( path ) ) { // Read the json from the file into a string dataAsJson = File . ReadAllText ( path ) ; // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility . FromJson < SkillTree > ( dataAsJson ) ; // Store the SkillTree as an array of Skill _skillTree = new Skill [ loadedData . skilltree . Length ] ; _skillTree = loadedData . skilltree ; // Populate a dictionary with the skill id and the skill data itself for ( int i = 0 ; i < _skillTree . Length ; ++ i ) { _skills . Add ( _skillTree [ i ] . id_Skill , _skillTree [ i ] ) ; } } else { Debug . LogError ( "Cannot load game data!" ) ; } } public void LoadPlayerSkillTree ( ) { string dataAsJson = PlayerPrefs . GetString ( "SkillTree" ) ; // Pass the json to JsonUtility, and tell it to create a SkillTree object from it SkillTree loadedData = JsonUtility . FromJson < SkillTree > ( dataAsJson ) ; // Store the SkillTree as an array of Skill _skillTree = new Skill [ loadedData . skilltree . Length ] ; _skillTree = loadedData . skilltree ; // Populate a dictionary with the skill id and the skill data itself for ( int i = 0 ; i < _skillTree . Length ; ++ i ) { _skills . Add ( _skillTree [ i ] . id_Skill , _skillTree [ i ] ) ; } } public void SaveSkillTree ( ) { // We fill with as many skills as nodes we have SkillTree skillTree = new SkillTree ( ) ; skillTree . skilltree = new Skill [ _skillTree . Length ] ; for ( int i = 0 ; i < _skillTree . Length ; ++ i ) { _skills . TryGetValue ( _skillTree [ i ] . id_Skill , out _skillInspected ) ; if ( _skillInspected != null ) { skillTree . skilltree [ i ] = _skillInspected ; } } string json = JsonUtility . ToJson ( skillTree ) ; PlayerPrefs . SetString ( "SkillTree" , json ) ; } public bool IsSkillUnlocked ( int id_skill ) { if ( _skills . TryGetValue ( id_skill , out _skillInspected ) ) { return _skillInspected . unlocked ; } else { return false ; } } public bool CanSkillBeUnlocked ( int id_skill ) { bool canUnlock = true ; if ( _skills . TryGetValue ( id_skill , out _skillInspected ) ) // The skill exists { if ( _skillInspected . cost <= availablePoints ) // Enough points available { int [ ] dependencies = _skillInspected . skill_Dependencies ; for ( int i = 0 ; i < dependencies . Length ; ++ i ) { if ( _skills . TryGetValue ( dependencies [ i ] , out _skillInspected ) ) { if ( ! _skillInspected . unlocked ) { canUnlock = false ; break ; } } else // If one of the dependencies doesn't exist, the skill can't be unlocked. { return false ; } } } else // If the player doesn't have enough skill points, can't unlock the new skill { return false ; } } else // If the skill id doesn't exist, the skill can't be unlocked { return false ; } return canUnlock ; } public bool UnlockSkill ( int id_Skill ) { if ( _skills . TryGetValue ( id_Skill , out _skillInspected ) ) { if ( _skillInspected . cost <= availablePoints ) { availablePoints -= _skillInspected . cost ; _skillInspected . unlocked = true ; // We replace the entry on the dictionary with the new one (already unlocked) _skills . Remove ( id_Skill ) ; _skills . Add ( id_Skill , _skillInspected ) ; return true ; } else { return false ; // The skill can't be unlocked. Not enough points } } else { return false ; // The skill doesn't exist } } }

After those changes, we need to give the order to save the player’s current state of the Skill Tree. For that, we are going to edit the SkillButton script in order to save after every time the player buys a new skill.

SkillButton.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class SkillButton : MonoBehaviour { public int skillId; public Color unlockedColor; public SkillHub skillHub; private Image _image; private Button _button; void Start () { _image = GetComponent<Image>(); _button = GetComponent<Button>(); RefreshState(); } public void RefreshState() { if (SkillTreeReader.Instance.IsSkillUnlocked(skillId)) { _image.color = unlockedColor; } else if (!SkillTreeReader.Instance.CanSkillBeUnlocked(skillId)) { _button.interactable = false; } else { _image.color = Color.white; _button.interactable = true; } } public void BuySkill() { if (SkillTreeReader.Instance.UnlockSkill(skillId)) { PlayerPrefs.SetInt("Score", SkillTreeReader.Instance.availablePoints); skillHub.RefreshButtons(); SkillTreeReader.Instance.SaveSkillTree(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; using UnityEngine . UI ; public class SkillButton : MonoBehaviour { public int skillId ; public Color unlockedColor ; public SkillHub skillHub ; private Image _image ; private Button _button ; void Start ( ) { _image = GetComponent < Image > ( ) ; _button = GetComponent < Button > ( ) ; RefreshState ( ) ; } public void RefreshState ( ) { if ( SkillTreeReader . Instance . IsSkillUnlocked ( skillId ) ) { _image . color = unlockedColor ; } else if ( ! SkillTreeReader . Instance . CanSkillBeUnlocked ( skillId ) ) { _button . interactable = false ; } else { _image . color = Color . white ; _button . interactable = true ; } } public void BuySkill ( ) { if ( SkillTreeReader . Instance . UnlockSkill ( skillId ) ) { PlayerPrefs . SetInt ( "Score" , SkillTreeReader . Instance . availablePoints ) ; skillHub . RefreshButtons ( ) ; SkillTreeReader . Instance . SaveSkillTree ( ) ; } } }

Conclusion

So that’s all, this tutorial series is finished, unless you ask for something to be added to it 🙂 I hope you all liked the whole series and that these tutorials had been helpful to you guys.

See you in the next tutorial!