Ability System with Scriptable Objects

Checked with version: 5.4

-

Difficulty: Intermediate

In this live training session we will create a flexible player ability system which includes player abilities with cool downs, similar to those seen in MOBA or MMO games. The approach uses scriptable objects and is designed to allow game designers to create multiple variations of an ability and swap between them easily.

Ability System with Scriptable Objects

Intermediate Scripting

Ability

Code snippet

using UnityEngine;
using System.Collections;

public abstract class Ability : ScriptableObject {

    public string aName = "New Ability";
    public Sprite aSprite;
    public AudioClip aSound;
    public float aBaseCoolDown = 1f;

    public abstract void Initialize(GameObject obj);
    public abstract void TriggerAbility();
}

RaycastAbility

Code snippet

using UnityEngine;
using System.Collections;

[CreateAssetMenu (menuName = "Abilities/RaycastAbility")]
public class RaycastAbility : Ability {

    public int gunDamage = 1;
    public float weaponRange = 50f;
    public float hitForce = 100f;
    public Color laserColor = Color.white;

    private RaycastShootTriggerable rcShoot;

    public override void Initialize(GameObject obj)
    {
        rcShoot = obj.GetComponent<RaycastShootTriggerable> ();
        rcShoot.Initialize ();

        rcShoot.gunDamage = gunDamage;
        rcShoot.weaponRange = weaponRange;
        rcShoot.hitForce = hitForce;
        rcShoot.laserLine.material = new Material (Shader.Find ("Unlit/Color"));
        rcShoot.laserLine.material.color = laserColor;

    }

    public override void TriggerAbility()
    {
        rcShoot.Fire ();
    }


}

AbilityCoolDown

Code snippet

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class AbilityCoolDown : MonoBehaviour {

    public string abilityButtonAxisName = "Fire1";
    public Image darkMask;
    public Text coolDownTextDisplay;

    [SerializeField] private Ability ability;
    [SerializeField] private GameObject weaponHolder;
    private Image myButtonImage;
    private AudioSource abilitySource;
    private float coolDownDuration;
    private float nextReadyTime;
    private float coolDownTimeLeft;


    void Start () 
    {
        Initialize (ability, weaponHolder); 
    }

    public void Initialize(Ability selectedAbility, GameObject weaponHolder)
    {
        ability = selectedAbility;
        myButtonImage = GetComponent<Image> ();
        abilitySource = GetComponent<AudioSource> ();
        myButtonImage.sprite = ability.aSprite;
        darkMask.sprite = ability.aSprite;
        coolDownDuration = ability.aBaseCoolDown;
        ability.Initialize (weaponHolder);
        AbilityReady ();
    }
    
    // Update is called once per frame
    void Update () 
    {
        bool coolDownComplete = (Time.time > nextReadyTime);
        if (coolDownComplete) 
        {
            AbilityReady ();
            if (Input.GetButtonDown (abilityButtonAxisName)) 
            {
                ButtonTriggered ();
            }
        } else 
        {
            CoolDown();
        }
    }

    private void AbilityReady()
    {
        coolDownTextDisplay.enabled = false;
        darkMask.enabled = false;
    }

    private void CoolDown()
    {
        coolDownTimeLeft -= Time.deltaTime;
        float roundedCd = Mathf.Round (coolDownTimeLeft);
        coolDownTextDisplay.text = roundedCd.ToString ();
        darkMask.fillAmount = (coolDownTimeLeft / coolDownDuration);
    }

    private void ButtonTriggered()
    {
        nextReadyTime = coolDownDuration + Time.time;
        coolDownTimeLeft = coolDownDuration;
        darkMask.enabled = true;
        coolDownTextDisplay.enabled = true;

        abilitySource.clip = ability.aSound;
        abilitySource.Play ();
        ability.TriggerAbility ();
    }
}

ProjectileAbility

Code snippet

using UnityEngine;
using System.Collections;

[CreateAssetMenu (menuName = "Abilities/ProjectileAbility")]
public class ProjectileAbility : Ability {

    public float projectileForce = 500f;
    public Rigidbody projectile;

    private ProjectileShootTriggerable launcher;

    public override void Initialize(GameObject obj)
    {
        launcher = obj.GetComponent<ProjectileShootTriggerable> ();
        launcher.projectileForce = projectileForce;
        launcher.projectile = projectile;
    }

    public override void TriggerAbility()
    {
        launcher.Launch ();
    }

}

RaycastShootTriggerable

Code snippet

using UnityEngine;
using System.Collections;

public class RaycastShootTriggerable : MonoBehaviour {

    [HideInInspector] public int gunDamage = 1;                         // Set the number of hitpoints that this gun will take away from shot objects with a health script.
    [HideInInspector] public float weaponRange = 50f;                   // Distance in unity units over which the player can fire.
    [HideInInspector] public float hitForce = 100f;                     // Amount of force which will be added to objects with a rigidbody shot by the player.
    public Transform gunEnd;                                            // Holds a reference to the gun end object, marking the muzzle location of the gun.
    [HideInInspector] public LineRenderer laserLine;                    // Reference to the LineRenderer component which will display our laserline.

    private Camera fpsCam;                                              // Holds a reference to the first person camera.
    private WaitForSeconds shotDuration = new WaitForSeconds(.07f);     // WaitForSeconds object used by our ShotEffect coroutine, determines time laser line will remain visible.


    public void Initialize ()
    {
        //Get and store a reference to our LineRenderer component
        laserLine = GetComponent<LineRenderer> ();

        //Get and store a reference to our Camera
        fpsCam = GetComponentInParent<Camera> ();
    }

    public void Fire()
    {

        //Create a vector at the center of our camera's near clip plane.
        Vector3 rayOrigin = fpsCam.ViewportToWorldPoint (new Vector3 (.5f, .5f, 0));
        
        //Draw a debug line which will show where our ray will eventually be
        Debug.DrawRay (rayOrigin, fpsCam.transform.forward * weaponRange, Color.green);
        
        //Declare a raycast hit to store information about what our raycast has hit.
        RaycastHit hit;

        //Start our ShotEffect coroutine to turn our laser line on and off
        StartCoroutine(ShotEffect());
        
        //Set the start position for our visual effect for our laser to the position of gunEnd
        laserLine.SetPosition(0, gunEnd.position);
        
        //Check if our raycast has hit anything
        if (Physics.Raycast(rayOrigin,fpsCam.transform.forward, out hit, weaponRange))
        {
            //Set the end position for our laser line 
            laserLine.SetPosition(1, hit.point);
            
            //Get a reference to a health script attached to the collider we hit
            ShootableBox health = hit.collider.GetComponent<ShootableBox>();
            
            //If there was a health script attached
            if (health != null)
            {
                //Call the damage function of that script, passing in our gunDamage variable
                health.Damage (gunDamage);
            }
            
            //Check if the object we hit has a rigidbody attached
            if (hit.rigidbody != null)
            {
                //Add force to the rigidbody we hit, in the direction it was hit from
                hit.rigidbody.AddForce (-hit.normal * hitForce);
            }
        }
        else
        {
            //if we did not hit anything, set the end of the line to a position directly away from
            laserLine.SetPosition(1, fpsCam.transform.forward * weaponRange);
        }
    }

    private IEnumerator ShotEffect()
    {

        //Turn on our line renderer
        laserLine.enabled = true;
        //Wait for .07 seconds
        yield return shotDuration;

        //Deactivate our line renderer after waiting
        laserLine.enabled = false;
    }
}

ProjectileShootTriggerable

Code snippet

using UnityEngine;
using System.Collections;

public class ProjectileShootTriggerable : MonoBehaviour {
    
    [HideInInspector] public Rigidbody projectile;                          // Rigidbody variable to hold a reference to our projectile prefab
    public Transform bulletSpawn;                           // Transform variable to hold the location where we will spawn our projectile
    [HideInInspector] public float projectileForce = 250f;                  // Float variable to hold the amount of force which we will apply to launch our projectiles

    public void Launch()
    {
        //Instantiate a copy of our projectile and store it in a new rigidbody variable called clonedBullet
        Rigidbody clonedBullet = Instantiate(projectile, bulletSpawn.position, transform.rotation) as Rigidbody;
        
        //Add force to the instantiated bullet, pushing it forward away from the bulletSpawn location, using projectile force for how hard to push it away
        clonedBullet.AddForce(bulletSpawn.transform.forward * projectileForce);
    }
}

Scripting

  1. Scripts as Behaviour Components
  2. Variables and Functions
  3. Conventions and Syntax
  4. C# vs JS syntax
  5. IF Statements
  6. Loops
  7. Scope and Access Modifiers
  8. Awake and Start
  9. Update and FixedUpdate
  10. Vector Maths
  11. Enabling and Disabling Components
  12. Activating GameObjects
  13. Translate and Rotate
  14. Look At
  15. Linear Interpolation
  16. Destroy
  17. GetButton and GetKey
  18. GetAxis
  19. OnMouseDown
  20. GetComponent
  21. Delta Time
  22. Data Types
  23. Classes
  24. Instantiate
  25. Arrays
  26. Invoke
  27. Enumerations
  28. Switch Statements
  1. Properties
  2. Ternary Operator
  3. Statics
  4. Method Overloading
  5. Generics
  6. Inheritance
  7. Polymorphism
  8. Member Hiding
  9. Overriding
  10. Interfaces
  11. Extension Methods
  12. Namespaces
  13. Lists and Dictionaries
  14. Coroutines
  15. Quaternions
  16. Delegates
  17. Attributes
  18. Events
  1. Building a Custom Inspector
  2. The DrawDefaultInspector Function
  3. Adding Buttons to a Custom Inspector
  1. MonoDevelop's Debugger
  2. Good Coding Practices in Unity
  3. Unity Editor Extensions – Menu Items
  4. Creating Meshes
  1. AssetBundles and the AssetBundle Manager
  2. Mastering Unity Project Folder Structure - Version Control Systems
  1. Installing Tools for Unity Development
  2. Building your first Unity Game with Visual Studio
  3. Editing Unity games in Visual Studio
  4. Debugging Unity games in Visual Studio
  5. Graphics debugging Unity games in Visual Studio
  6. Taking Unity games to Universal Windows Platform
  7. Testing Unity games on Android in Visual Studio
  1. Scripting Primer and Q&A
  2. Scripting Primer and Q&A - Continued
  3. Scripting Primer and Q&A - Continued (Again)
  4. Persistence - Saving and Loading Data
  5. Object Pooling
  6. Introduction to Scriptable Objects
  7. How to communicate between Scripts and GameObjects
  8. Coding in Unity for the Absolute Beginner
  9. Sound Effects & Scripting
  10. Editor Scripting Intro
  11. Writing Plugins
  12. Property Drawers & Custom Inspectors
  13. Events: Creating a simple messaging system
  14. Ability System with Scriptable Objects
  15. Character Select System with Scriptable Objects
  1. Intro and Setup
  2. Data Classes
  3. Menu Screen
  4. Game UI
  5. Answer Button
  6. Displaying Questions
  7. Click To Answer
  8. Ending The Game and Q&A
  1. Intro To Part Two
  2. High Score with PlayerPrefs
  3. Serialization and Game Data
  4. Loading Game Data via JSON
  5. Loading and Saving via Editor Script
  6. Game Data Editor GUI
  7. Question and Answer
  1. Overview and Goals
  2. Localization Data
  3. Dictionary, JSON and Streaming Assets
  4. Localization Manager
  5. Startup Manager
  6. Localized Text Component
  7. Localized Text Editor Script
  8. Localization Q&A
  1. Introduction and Session Goals
  2. Particle Launcher
  3. Particle Collisions
  4. ParticleLauncher Script
  5. Particle Collisions and Scripting
  6. Random Particle Colors
  7. Drawing Decals with Particles
  8. Collecting Particle Information For Display
  9. Displaying Particles Via Script
  10. Droplet Decals
  11. Questions and Answers