Creating a Rogue-like (like Vampire Survivors) - Part 4

Creating a Rogue-like Shoot ‘Em Up (like Vampire Survivors) — Part 4: Weapon and Enemy Stats

Ever wanted to create a rogue-like shoot ’em up game like Vampire Survivors? In Part 4 of our guide, we will go through how to create stats for our weapons and enemies, including how to damage enemies with weapons. You can also find Part 3 of our guide here, where we went through how to create our first weapons and basic enemy AI.

A link to a package containing the project files up to Part 4 of this tutorial series can also be found at the end of this article.

Video authored, edited and subtitled by Xavier Lee
  1. Introduction
  2. Creating weapon stats
    1. Weapon scriptable objects
    2. Linking stats to weapons
  3. Creating enemy stats
    1. Enemy scriptable objects
    2. Linking stats to enemies
  4. Creating properties of stats
  5. Damaging enemies with weapons
    1. A way to damage enemies
    2. Applying damage to enemies for projectile weapons
    3. Miscellaneous stats for projectile weapons
    4. Applying damage to enemies for melee weapons
  6. Conclusion

1. Introduction

Statistics makes up a huge part of Vampire Survivors just like any other game. They define many different things such as speed and attack or defense and health.

If you recall back to the previous part, we have already created some stats for our weapon and enemies while working on their respective scripts. However, these aren’t really expandable and they are kind of a nuisance since we always have to reference back to the script whenever we want a stat of a weapon and enemy per say.

Additionally, testing will be a lot harder as we need to create 2 of the same prefabs to compare a of batch values. You might think this is a minor issue, but as you get more into creating new weapons and enemies, you will soon realize that it is better to have a more convenient way to test different values.

So in today’s part, we are going to be taking a look at how we can create scalable weapon and enemy stats and also touch on how we can make use of weapon stats to damage enemies.

2. Creating weapon stats

Let’s start with creating stats for our weapons we made in the previous part.

a. Weapon scriptable objects

Before we begin creating any form of stats, we first have to understand what we are trying to achieve. We basically want something that is:

  1. Easy to add on to.
  2. A data container.
  3. Makes testing easier.

Luckily for us Unity already has something built in that fits all our needs, and that is ScriptableObjects. If this is your first time hearing or working with ScriptableObjects, I recommend you check out Brackeys’ or samyam’s video on them, they are extremely informative and teach you the ins and outs about how to use ScriptableObjects. However for the sake of the tutorial I’ll still give you the basic rundown as to what exactly they are.

ScriptableObjects are pretty much data containers used to contain data we need for the game to function. They act as templates for objects and are most notably used to store stats like the ones mentioned previously.

The biggest difference between storing stats in a script i.e. Monobehaviour and a ScriptableObject is that ScriptableObjects are an asset. This means that they are stored within your assets folder as a Project-level asset. This makes them more convenient because we can easily duplicate them and test for different stats.

ScriptableObjects are also independent from anything else within the game. And since they are independent, any game object within any scene can access the ScriptableObject which makes it convenient for referencing data, there won’t be any need to reference a certain script or game object. What that means is that if you were to play the game and change a value of a ScriptableObject, the value will be saved. This is different from a prefab as a prefab only saves values after stopping play mode and not during play mode.

I could on and on about ScriptableObjects and their various benefits, but for the sake of simplicity let’s dive right in.

First up, create a new C# script called WeaponScriptableObject inside of the Weapon subfolder in Scripts and open it.

By default all scripts in Unity derive from Monobehaviour as it allows the script to be a component of an object. However, we want a ScriptableObject that doesn’t sit on the game object. So if you recall back to the previous part about inheritance, you would know that all we have to do is just derive from ScriptableObject instead.

Make sure to also delete away the Start() and Update() functions as we have no need for them.

Now if you take a look at your WeaponController script, you should see the stats we created for the weapon previously. Let’s copy over all of the variables that won’t change during runtime, this is because ScriptableObjects save the value during runtime and thus the variable will not be reset even when the game is restarted.

WeaponScriptableObject.cs

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

public class WeaponScriptableObject : ScriptableObject
{
    public GameObject prefab;
    //Base stats for the weapon
    public float damage;
    public float speed;
    public float cooldownDuration;
    public int pierce;
}

Now, all that’s needed is to define a way to create a scriptable object from this template in the assets folder. And to do that we need to add an attribute to the class called [CreateAssetMenu].

WeaponScriptableObject.cs

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

[CreateAssetMenu]
public class WeaponScriptableObject : ScriptableObject
{
    public GameObject prefab;
    //Base stats for the weapon
    public float damage;
    public float speed;
    public float cooldownDuration;
    public int pierce;
}

Article continues after the advertisement:


If you head back into the Editor like this, Right-click on the Project window > Create, it should show up as your first option.

vampire survivors weaponso

Although this works fine, for the sake of organization let’s add a few more properties.

Let’s set the default file name when creating a new scriptable object to be WeaponScriptableObject and the menu name to be something like ScriptableObjects. However, since we are going to be creating more scriptable objects for enemies and the player later down the line, it would be better to create a submenu by going ScriptableObjects/Weapon.

WeaponScriptableObject.cs

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

[CreateAssetMenu(fileName = "WeaponScriptableObject", menuName = "ScriptableObjects/Weapon")]
public class WeaponScriptableObject : ScriptableObject
{
    public GameObject prefab;
    //Base stats for the weapon
    public float damage;
    public float speed;
    public float cooldownDuration;
    public int pierce;
}

We can now save this and head back into the Editor. Create a new folder called Scriptable Objects and inside there if you Right-click > Create, you should now see that the scriptable object has been sorted.

vampire survivors weaponso2

Create a 2 new weapon scriptable objects and rename them to Knife Weapon and Garlic Weapon respectively. If you look towards the inspector while selecting one the scriptable objects, you should see the stats we created within the script.

b. Linking stats to weapons

Now it’s time for us to link the stats to the relevant weapon scripts.

Open up the WeaponController script and inside here we have to:

  1. Create a new public variable of type WeaponScriptableObject and name it weaponData. This variable will be assigned through the inspector and will serve as the reference to all our stats.
  2. Remove all the variables we copied over to the WeaponScriptableObject.

Note: I recommend you head back into your Editor and save a screenshot of your values in the KnifeController and GarlicController scripts beforehand, because we’ll have to reassign them later.

WeaponController.cs

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

/// <summary>
/// Base script for all weapon controllers
/// </summary>
public class WeaponController : MonoBehaviour
{
    [Header("Weapon Stats")]
    public WeaponScriptableObject weaponData;
    float currentCooldown;

    protected PlayerMovement pm;


    protected virtual void Start()
    {
        pm = FindObjectOfType<PlayerMovement>();
        currentCooldown = cooldownDuration; //At the start set the current cooldown to be cooldown duration
    }

    protected virtual void Update()
    {
        currentCooldown -= Time.deltaTime;
        if (currentCooldown <= 0f)   //Once the cooldown becomes 0, attack
        {
            Attack();
        }
    }

    protected virtual void Attack()
    {
        currentCooldown = cooldownDuration;
    }
}

There should now be a ton of errors showing up in your console from the WeaponController, KnifeController, GarlicController and KnifeBehaviour scripts, so let’s open them up and fix them.

WeaponController.cs

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

/// <summary>
/// Base script for all weapon controllers
/// </summary>
public class WeaponController : MonoBehaviour
{
    [Header("Weapon Stats")]
    public WeaponScriptableObject weaponData;
    float currentCooldown;

    protected PlayerMovement pm;


    protected virtual void Start()
    {
        pm = FindObjectOfType<PlayerMovement>();
        currentCooldown = weaponData.cooldownDuration; //At the start set the current cooldown to be cooldown duration
    }

    protected virtual void Update()
    {
        currentCooldown -= Time.deltaTime;
        if (currentCooldown <= 0f)   //Once the cooldown becomes 0, attack
        {
            Attack();
        }
    }

    protected virtual void Attack()
    {
        currentCooldown = weaponData.cooldownDuration;
    }
}

KnifeController.cs

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

public class KnifeController : WeaponController
{
    protected override void Start()
    {
        base.Start();
    }

    protected override void Attack()
    {
        base.Attack();
        GameObject spawnedKnife = Instantiate(weaponData.prefab);
        spawnedKnife.transform.position = transform.position; //Assign the position to be the same as this object which is parented to the player
        spawnedKnife.GetComponent<KnifeBehaviour>().DirectionChecker(pm.lastMovedVector);   //Reference and set the direction
    }
}

GarlicController.cs

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

public class GarlicController : WeaponController
{
    protected override void Start()
    {
        base.Start();
    }

    protected override void Attack()
    {
        base.Attack();
        GameObject spawnedGarlic = Instantiate(weaponData.prefab);
        spawnedGarlic.transform.position = transform.position; //Assign the position to be the same as this object which is parented to the player
        spawnedGarlic.transform.parent = transform;
    }
}

Article continues after the advertisement:


For the KnifeBehaviour script, we can actually remove the reference to the KnifeController since we no longer need it. Instead, let’s head over to our ProjectileWeaponBehaviour script and add in the same public variable called weaponData.

We can also do the same for the MeleeWeaponBehaviour script for future use.

ProjectileWeaponBehaviour.cs

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

/// <summary>
/// Base script of all projectile behaviours [To be placed on a prefab of a weapon that is a projectile]
/// </summary>
public class ProjectileWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    protected Vector3 direction;

    public float destroyAfterSeconds;

    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

    public void DirectionChecker(Vector3 dir)
    {
        direction = dir;

        float dirx = direction.x;
        float diry = direction.y;

        Vector3 scale = transform.localScale;
        Vector3 rotation = transform.rotation.eulerAngles;

        if (dirx < 0 && diry == 0) //left
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry < 0) //down
        {
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry > 0) //up
        {
            scale.x = scale.x * -1;
        }
        else if (dirx > 0 && diry > 0) //right up
        {
            rotation.z = 0f;
        }
        else if (dirx > 0 && diry < 0) //right down
        {
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry > 0) //left up
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry < 0) //left down
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = 0f;
        }

        transform.localScale = scale;
        transform.rotation = Quaternion.Euler(rotation);    //Can't simply set the vector because cannot convert
    }
}

MeleeWeaponBehaviour.cs

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

/// <summary>
/// Base script of all melee behaviours [To be placed on a prefab of a weapon that is melee]
/// </summary>
public class MeleeWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    public float destroyAfterSeconds;

    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

}

Now in the KnifeBehaviour script, simply reference the newly created weaponData.

KnifeBehaviour.cs

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

public class KnifeBehaviour : ProjectileWeaponBehaviour
{

    protected override void Start()
    {
        base.Start();
    }

    void Update()
    {
        transform.position += direction * weaponData.speed * Time.deltaTime;    //Set the movement of the knife
    }
}

If you head back into your Editor now and look towards the Knife Controller or Garlic Controller objects, you should be able to see that a slot has appeared. The same thing should also happen to the behaviour scripts on the prefabs. Simply drag and drop their respective scriptable object inside.

Now let’s assign the values of the parameters in the scriptable objects. Below are my previous values that I have assigned for the Knife Weapon and Garlic Weapon.

vampire survivors knife values
Knife Weapon
vampire survivors garlic values
Garlic Weapon

Hit Play and test to make sure everything is working as intended.

As you might have been able to tell already, if we want to test a whole new set of values, we can now just simply duplicate the scriptable object and change the values before assigning it. This will make testing a whole lot easier.

Great!

3. Creating enemy stats

Now that we’re done with the weapon stats, it’s time for us to move on to creating enemy stats.

a. Enemy scriptable objects

Similar to how we did so for the weapons, let’s also make enemy scriptable objects so that everything in our game is consistent. We don’t want to be using scriptable objects to store stats for one thing and then go back to using Monobehaviours to store stats for another thing.

Create a script called EnemyScriptableObject and:

  1. Derive it from ScriptableObject.
  2. Create some stats for the enemies.
  3. Create an asset menu for it.

EnemyScriptableObject.cs

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


[CreateAssetMenu(fileName = "EnemyScriptableObject", menuName = "ScriptableObjects/Enemy")]
public class EnemyScriptableObject : ScriptableObject
{
    //Base stats for the enemy
    public float moveSpeed;
    public float maxHealth;
    public float damage;
}

Article continues after the advertisement:


Head into your Scriptable Objects folder, and now create an enemy scriptable object and name it Bat Enemy. Next, create 2 folders and name them Weapons and Enemies and drag the respective scriptable objects into the folders.

b. Linking stats to enemies

Open the EnemyMovement script and then:

  1. Create a new public variable with type EnemyScriptableObject called enemyData.
  2. Remove the moveSpeed variable.
  3. Reference the enemyData‘s stats.

EnemyMovement.cs

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

public class EnemyMovement : MonoBehaviour
{
    public EnemyScriptableObject enemyData;
    Transform player;


    void Start()
    {
        player = FindObjectOfType<PlayerMovement>().transform;
    }

    void Update()
    {
        transform.position = Vector2.MoveTowards(transform.position, player.transform.position, enemyData.moveSpeed * Time.deltaTime);    //Constantly move the enemy towards the player
    }
}

Head back into the Editor and assign the Bat enemy scriptable object to the Bat prefab. Afterwards, just assign the previous moveSpeed value you had for the Bat inside the Bat enemy scriptable object. As mine was 2, I’ll set it as that.

As per usual hit Play and make sure everything is working good before moving on.

4. Creating properties of stats

However before we move on, there is a very important thing we must do, and that is creating properties for our stats.

Properties also known as getters and setters encapsulate member variables such that we can make them read or write only. Currently all our variables in our scriptable object scripts are marked as public which allows us to access them in other scripts. This is great and all, but there is a fatal flaw in this and that is because it is very easy to accidentally write to a variable of a scriptable object.

You might think this is a small issue, but you have to remember that ScriptableObjects save values even during runtime and do not reset afterwards. This is very dangerous as values can be modified without your knowledge.

Properties will act as a fail-safe so that such things don’t happen. They give us a wider range of control of when and how a variable is accessed so we don’t unnecessarily access data. For more information on properties you can check out Unity’s official tutorial on how to use them.

Within our WeaponScriptableObject script let’s start creating a property of every single field here. We can begin with prefab.

  1. Remove the public keyword.
  2. Add the SerializeField attribute above the variable.
  3. Create a new public GameObject called Prefab. It is always good to use the same name for properties but with a capital letter to differentiate them.
  4. Create a public getter and private setter using the => (Lamda) operator. This will allow us to shorten our syntax.

I recommend you head back into your Editor and save a screenshot of your values in the Scriptable Objects. But it shouldn’t be an issue since Unity should save the index values.

WeaponScriptableObject.cs

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

[CreateAssetMenu(fileName = "WeaponScriptableObject", menuName = "ScriptableObjects/Weapon")]
public class WeaponScriptableObject : ScriptableObject
{
    [SerializeField]
    GameObject prefab;
    public GameObject Prefab { get => prefab; private set => prefab = value; }
    //Base stats for the weapon
    public float damage;
    public float speed;
    public float cooldownDuration;
    public int pierce;
}

Now that you have the gist of it, let’s do the same for the other variables as well. And by the end you should have something like this.

WeaponScriptableObject.cs

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

[CreateAssetMenu(fileName = "WeaponScriptableObject", menuName = "ScriptableObjects/Weapon")]
public class WeaponScriptableObject : ScriptableObject
{
    [SerializeField]
    GameObject prefab;
    public GameObject Prefab { get => prefab; private set => prefab = value; }

    //Base stats for the weapon
    [SerializeField]
    float damage;
    public float Damage { get => damage; private set => damage = value; }

    [SerializeField]
    float speed;
    public float Speed { get => speed; private set => speed = value; }

    [SerializeField]
    float cooldownDuration;
    public float CooldownDuration { get => cooldownDuration; private set => cooldownDuration = value; }

    [SerializeField]
    int pierce;
    public int Pierce { get => pierce; private set => pierce = value; }
}

Now when you try to write to these variables you will receive an error.

Awesome!

Now let’s fix all the errors that arose because of this.

WeaponController.cs

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

/// <summary>
/// Base script for all weapon controllers
/// </summary>
public class WeaponController : MonoBehaviour
{
    [Header("Weapon Stats")]
    public WeaponScriptableObject weaponData;
    float currentCooldown;

    protected PlayerMovement pm;


    protected virtual void Start()
    {
        pm = FindObjectOfType<PlayerMovement>();
        currentCooldown = weaponData.CooldownDuration; //At the start set the current cooldown to be cooldown duration
    }

    protected virtual void Update()
    {
        currentCooldown -= Time.deltaTime;
        if (currentCooldown <= 0f)   //Once the cooldown becomes 0, attack
        {
            Attack();
        }
    }

    protected virtual void Attack()
    {
        currentCooldown = weaponData.CooldownDuration;
    }
}

KnifeController.cs

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

public class KnifeController : WeaponController
{
    protected override void Start()
    {
        base.Start();
    }

    protected override void Attack()
    {
        base.Attack();
        GameObject spawnedKnife = Instantiate(weaponData.Prefab);
        spawnedKnife.transform.position = transform.position; //Assign the position to be the same as this object which is parented to the player
        spawnedKnife.GetComponent<KnifeBehaviour>().DirectionChecker(pm.lastMovedVector);   //Reference and set the direction
    }
}

Article continues after the advertisement:


GarlicController.cs

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

public class GarlicController : WeaponController
{
    protected override void Start()
    {
        base.Start();
    }

    protected override void Attack()
    {
        base.Attack();
        GameObject spawnedGarlic = Instantiate(weaponData.Prefab);
        spawnedGarlic.transform.position = transform.position; //Assign the position to be the same as this object which is parented to the player
        spawnedGarlic.transform.parent = transform;
    }
}

KnifeBehaviour.cs

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

public class KnifeBehaviour : ProjectileWeaponBehaviour
{

    protected override void Start()
    {
        base.Start();
    }

    void Update()
    {
        transform.position += direction * weaponData.Speed * Time.deltaTime;    //Set the movement of the knife
    }
}

Let’s also do the same for the variables in the EnemyScriptableObject script.

EnemyScriptableObject.cs

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


[CreateAssetMenu(fileName = "EnemyScriptableObject", menuName = "ScriptableObjects/Enemy")]
public class EnemyScriptableObject : ScriptableObject
{
    //Base stats for the enemy
    [SerializeField]
    float moveSpeed;
    public float MoveSpeed { get => moveSpeed; private set => moveSpeed = value; }

    [SerializeField]
    float maxHealth;
    public float MaxHealth { get => maxHealth; private set => maxHealth = value; }

    [SerializeField]
    float damage;
    public float Damage { get => damage; private set => damage = value; }
}

Then let’s fix the error that arose because of this.

EnemyMovement.cs

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

public class EnemyMovement : MonoBehaviour
{
    public EnemyScriptableObject enemyData;
    Transform player;


    void Start()
    {
        player = FindObjectOfType<PlayerMovement>().transform;
    }

    void Update()
    {
        transform.position = Vector2.MoveTowards(transform.position, player.transform.position, enemyData.MoveSpeed * Time.deltaTime);    //Constantly move the enemy towards the player
    }
}

5. Damaging enemies with weapons

Now it’s time for us to finally create a way to damage enemies with weapons. We already have the stats in place, so all that’s left is for us to create a system that makes enemies lose health when hit by weapons.

a. A way to damage enemies

Let’s create a new script called EnemyStats that will be used to handle all the enemy stats. Inside here we want to:

  1. Create a reference to the EnemyScriptableObject.
  2. Create variables for every single variable in the EnemyScriptableObject script with the prefix of current except for maxHealth, we can give it the name currentHealth. These will be used to track our “current” values which might change midway through the game due to some effect and since we can’t write to the variables of scriptable objects, these variables are a necessity. For instance, the player might receive a buff that slows all enemies down and thus the currentMoveSpeed will need to change.
  3. Remove the Start() and Update() functions.
  4. Create an Awake() function and we then have to assign these “current” stats to be the values of the ones we set in the scriptable objects in the Awake() function. We are using Awake() because it calls before Start() and thus, is more reliable.
  5. A public function called TakeDamage() used to make the enemy lose health.

EnemyStats.cs

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

public class EnemyStats : MonoBehaviour
{
    public EnemyScriptableObject enemyData;

    //Current stats
    float currentMoveSpeed;
    float currentHealth;
    float currentDamage;

    void Awake()
    {
        //Assign the vaiables
        currentMoveSpeed = enemyData.MoveSpeed;
        currentHealth = enemyData.MaxHealth;
        currentDamage = enemyData.Damage;
    }

    public void TakeDamage()
    {

    }
}

Inside TakeDamage() we simply want to create an argument of type float called dmg and reduce the currentHealth by dmg. Afterwards, check if the currentHealth is <= 0 and if it is, kill the enemy.

Create a new public function called Kill() which simply destroys the enemy when called.

EnemyStats.cs

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

public class EnemyStats : MonoBehaviour
{
    public EnemyScriptableObject enemyData;

    //Current stats
    float currentMoveSpeed;
    float currentHealth;
    float currentDamage;

    void Awake()
    {
        //Assign the vaiables
        currentMoveSpeed = enemyData.MoveSpeed;
        currentHealth = enemyData.MaxHealth;
        currentDamage = enemyData.Damage;
    }

    public void TakeDamage(float dmg)
    {
        currentHealth -= dmg;

        if (currentHealth <= 0)
        {
            Kill();
        }
    }

    public void Kill()
    {
        Destroy(gameObject);
    }
}

Alright great!

This will basically allow us to input the amount of damage to be dealt to the enemy and check if the enemy should be dead every time damage is inflicted. We can leave this be for now and expand on it later.


Article continues after the advertisement:


b. Applying damage to enemies for projectile weapons

Now it’s time to detect enemies and determine how they take damage. We’ll begin with the projectile weapon (knife weapon) before moving onto the melee weapon (garlic weapon).

The easiest and simplest way to do this would be to check if the collider on the weapon is touching the collider on the enemy and apply damage afterwards.

Let’s add a PolygonCollider2D to the knife weapon prefab and drag it into the scene, make sure to set it to be a trigger as well. The reason why we are using this collider is because we want to be as accurate as possible, if we used a BoxCollider2D you would see that there is no way we can be accurate due to the sprite orientation of our knife.

We are also editing this in the scene view because editing the polygon collider in the prefab causes it to autosave which makes the process a lot slower. So it’s just much better to edit in the scene and make it a new prefab later on.

In the scene view, edit the collider such that is takes the shape of the knife. Click the Edit Collider button within the inspector and drag the points to match the shape. You can also add more points by clicking and dragging anywhere between 2 lines.

vampire survivors polygon collider knife
End result

Add a Rigidbody2D and set the Freeze Rotation on the Z axis to be true and drag the Knife Weapon in the scene to the prefab folder and make it an original prefab. Now you can delete away the old prefab and rename the new prefab appropriately. Once that’s done, make sure to set the new prefab to the prefab slot on the Knife Weapon scriptable object. You can also remove the knife weapon in the scene.

Let’s start applying damage for the projectiles first. Open up your ProjectileWeaponBehaviour script and in here we want to do something similar for we did in the EnemyStats:

  1. Create variables for every single variable in the WeaponScriptableObject script with the prefix of current except for prefab. Also make them protected so that they can be accessed by child classes.
  2. Create an Awake() function and we then have to assign these “current” stats to be the values of the ones we set in the scriptable objects in the Awake() function.

ProjectileWeaponBehaviour.cs

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

/// <summary>
/// Base script of all projectile behaviours [To be placed on a prefab of a weapon that is a projectile]
/// </summary>
public class ProjectileWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    protected Vector3 direction;

    public float destroyAfterSeconds;

    //Current stats
    protected float currentDamage;
    protected float currentSpeed;
    protected float currentCooldownDuration;
    protected int currentPierce;

    void Awake()
    {
        currentDamage = weaponData.Damage;
        currentSpeed = weaponData.Speed;
        currentCooldownDuration = weaponData.CooldownDuration;
        currentPierce = weaponData.Pierce;
    }


    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

    public void DirectionChecker(Vector3 dir)
    {
        direction = dir;

        float dirx = direction.x;
        float diry = direction.y;

        Vector3 scale = transform.localScale;
        Vector3 rotation = transform.rotation.eulerAngles;

        if (dirx < 0 && diry == 0) //left
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry < 0) //down
        {
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry > 0) //up
        {
            scale.x = scale.x * -1;
        }
        else if (dirx > 0 && diry > 0) //right up
        {
            rotation.z = 0f;
        }
        else if (dirx > 0 && diry < 0) //right down
        {
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry > 0) //left up
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry < 0) //left down
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = 0f;
        }

        transform.localScale = scale;
        transform.rotation = Quaternion.Euler(rotation);    //Can't simply set the vector because cannot convert
    }
}

Now let’s create an OnTriggerEnter2D() marked with the keywords protected and virtual so that we can override them if needed. Inside the function check if the collider that the projectile collided with has the tag of Enemy and afterwards reference the EnemyStats from the collided collider and apply the damage to the enemy.

ProjectileWeaponBehaviour.cs

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

/// <summary>
/// Base script of all projectile behaviours [To be placed on a prefab of a weapon that is a projectile]
/// </summary>
public class ProjectileWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    protected Vector3 direction;

    public float destroyAfterSeconds;

    //Current stats
    protected float currentDamage;
    protected float currentSpeed;
    protected float currentCooldownDuration;
    protected int currentPierce;

    void Awake()
    {
        currentDamage = weaponData.Damage;
        currentSpeed = weaponData.Speed;
        currentCooldownDuration = weaponData.CooldownDuration;
        currentPierce = weaponData.Pierce;
    }


    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

    public void DirectionChecker(Vector3 dir)
    {
        direction = dir;

        float dirx = direction.x;
        float diry = direction.y;

        Vector3 scale = transform.localScale;
        Vector3 rotation = transform.rotation.eulerAngles;

        if (dirx < 0 && diry == 0) //left
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry < 0) //down
        {
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry > 0) //up
        {
            scale.x = scale.x * -1;
        }
        else if (dirx > 0 && diry > 0) //right up
        {
            rotation.z = 0f;
        }
        else if (dirx > 0 && diry < 0) //right down
        {
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry > 0) //left up
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry < 0) //left down
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = 0f;
        }

        transform.localScale = scale;
        transform.rotation = Quaternion.Euler(rotation);    //Can't simply set the vector because cannot convert
    }

    protected virtual void OnTriggerEnter2D(Collider2D col)
    {
        //Reference the script from the collided collider and deal damage using TakeDamage()
        if(col.CompareTag("Enemy"))
        {
            EnemyStats enemy = col.GetComponent<EnemyStats>();
            enemy.TakeDamage(currentDamage);     //Make sure to use currentDamage instead of weaponData.Damage in case any damage multipliers in the future
        }
    }
}

Make sure to use currentDamage instead of weaponData.Damage in case of any damage multipliers that are going to be applied in the future.

Head back into the Editor to set the Damage and MaxHealth of the Knife Weapon and Bat Enemy scriptable objects. I’ll set mine to be 5 and 10 respectively. Before you press Play, make sure to create a new tag called Enemy and apply it onto the Bat prefab along with the EnemyStats script. (drag the relevant scriptable object for the parameter as well)

Now when you play the game and shoot at the enemy you will see that it disappears after being hit twice since its health is depleted!

vampire survivors kill enemy knife

Awesome!


Article continues after the advertisement:


c. Miscellaneous stats for projectile weapons

But we are not quite yet done for the projectile behaviour, because as you can see in the scene, the projectile doesn’t get destroyed when hitting the enemy, but only when the destroyAfterSeconds time is up.

That’s where Pierce comes in. As explained in the previous part, pierce is the maximum amount of times a weapon can hit before it gets destroyed. We can use this stat to determine the amount of times a projectile can pass through an enemy before it is removed.

  1. Create a function called ReducePierce() and inside it decrement the currentPierce. Call ReducePierce at the end of the OnTriggerEnter2D() as well.
  2. Afterwards check if the currentPierce has reached 0 and if it has, destroy it.

ProjectileWeaponBehaviour.cs

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

/// <summary>
/// Base script of all projectile behaviours [To be placed on a prefab of a weapon that is a projectile]
/// </summary>
public class ProjectileWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    protected Vector3 direction;

    public float destroyAfterSeconds;

    //Current stats
    protected float currentDamage;
    protected float currentSpeed;
    protected float currentCooldownDuration;
    protected int currentPierce;

    void Awake()
    {
        currentDamage = weaponData.Damage;
        currentSpeed = weaponData.Speed;
        currentCooldownDuration = weaponData.CooldownDuration;
        currentPierce = weaponData.Pierce;
    }


    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

    public void DirectionChecker(Vector3 dir)
    {
        direction = dir;

        float dirx = direction.x;
        float diry = direction.y;

        Vector3 scale = transform.localScale;
        Vector3 rotation = transform.rotation.eulerAngles;

        if (dirx < 0 && diry == 0) //left
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry < 0) //down
        {
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry > 0) //up
        {
            scale.x = scale.x * -1;
        }
        else if (dirx > 0 && diry > 0) //right up
        {
            rotation.z = 0f;
        }
        else if (dirx > 0 && diry < 0) //right down
        {
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry > 0) //left up
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry < 0) //left down
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = 0f;
        }

        transform.localScale = scale;
        transform.rotation = Quaternion.Euler(rotation);    //Can't simply set the vector because cannot convert
    }

    protected virtual void OnTriggerEnter2D(Collider2D col)
    {
        //Reference the script from the collided collider and deal damage using TakeDamage()
        if(col.CompareTag("Enemy"))
        {
            EnemyStats enemy = col.GetComponent<EnemyStats>();
            enemy.TakeDamage(currentDamage);    //Make sure to use currentDamage instead of weaponData.Damage in case any damage multipliers in the future
            ReducePierce();
        }
    }

    void ReducePierce() //Destroy once the pierce reaches 0
    {
        currentPierce--;
        if (currentPierce <= 0)
        {
            Destroy(gameObject);
        }
    }
}

Back in the Editor, set the Pierce of the Knife Weapon to be 1 and hit Play. You should now see that the knife projectile disappears after hitting the enemy!

d. Applying damage to enemies for melee weapons

Now for the Garlic Weapon we can simply add a CircleCollider2D because of the shape and it should auto-scale to our sprite, make sure to set it to be a trigger too.

Next up is to open the MeleeWeaponBehaviour script and do the same for what we did previously in the ProjectileWeaponBehaviour script.

MeleeWeaponBehaviour.cs

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

/// <summary>
/// Base script of all melee behaviours [To be placed on a prefab of a weapon that is melee]
/// </summary>
public class MeleeWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    public float destroyAfterSeconds;

    //Current stats
    protected float currentDamage;
    protected float currentSpeed;
    protected float currentCooldownDuration;
    protected int currentPierce;

    void Awake()
    {
        currentDamage = weaponData.Damage;
        currentSpeed = weaponData.Speed;
        currentCooldownDuration = weaponData.CooldownDuration;
        currentPierce = weaponData.Pierce;
    }

    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

    protected virtual void OnTriggerEnter2D(Collider2D col)
    {
        if (col.CompareTag("Enemy"))
        {
            EnemyStats enemy = col.GetComponent<EnemyStats>();
            enemy.TakeDamage(currentDamage);
        }
    }
}

Head back into the Editor and set the Damage of the garlic scriptable object to be something like 5. Afterwards, you can disable the Knife Controller game object and EnemyMovement script on the Bat to make testing easier.

Hit Play and move towards the enemy. We now have a way to damage enemies using the garlic!

Although this is great you might have already realized there is one minor issue with this, and that is if you move away from the enemy and back into the enemy, you can immediately deal damage to it over and over again.

However in Vampire Survivors, the garlic does the following:

Damage will be dealt instantly to an enemy if they have never been in the circle before, after which they cannot be hit by it again until the Garlic’s Cooldown is over, even if they temporarily leave its area.

Garlic — Vampire Survivors Wiki

In order to make the behaviour of our garlic similar to the one in Vampire Survivors we must do a few things:

  1. Inside the GarlicBehaviour script override the OnTriggerEnter2D() function. We don’t have to call the base as we want access to the local variable enemy, which is not possible if you simply call the base, so we have to copy over the full function.
  2. Create a List<GameObject> called markedEnemies and initialize it in the Start() function.
  3. Add a new condition in the OnTriggerEnter2D‘s if statement checking the the markedEnemies list contains the detected object. If it does not, we shall allow it to run.
  4. At the end of the function, after damaging the enemy, add the enemy to the markedEnemies list.

GarlicBehaviour.cs

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

public class GarlicBehaviour : MeleeWeaponBehaviour
{
    List<GameObject> markedEnemies;

    protected override void Start()
    {
        base.Start();
        markedEnemies = new List<GameObject>();
    }

    protected override void OnTriggerEnter2D(Collider2D col)
    {
        if (col.CompareTag("Enemy") && !markedEnemies.Contains(col.gameObject))
        {
            EnemyStats enemy = col.GetComponent<EnemyStats>();
            enemy.TakeDamage(currentDamage);

            markedEnemies.Add(col.gameObject);  //Mark the enemy
        }
    }
}

What this entire system allows us to do is to mark enemies that the current spawned garlic has hit and make it so that they won’t be damaged by the same garlic again. This more or less matches up with what Vampire Survivors has done.

With that said, hit Play and test it out. Everything should work just fine!

vampire survivors kill enemy garlic

Article continues after the advertisement:


6. Conclusion

That’s all for this part! In this part we went through how to create stats for our weapons and enemies, including damaging enemies with weapons. If you spot any errors or typos in this article, please highlight them in the comments below.

You can also download the project files for what we have done so far. To use the files, you will have to unzip the file (7-Zip can help you do that), and open the folder with Assets and ProjectSettings as a project using Unity.

If you are unsure on how to open downloaded Unity projects, check out our article and video here where we explain how to do so.

These are the final end results of all scripts we have worked with today:

WeaponScriptableObject.cs

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

[CreateAssetMenu(fileName = "WeaponScriptableObject", menuName = "ScriptableObjects/Weapon")]
public class WeaponScriptableObject : ScriptableObject
{
    [SerializeField]
    GameObject prefab;
    public GameObject Prefab { get => prefab; private set => prefab = value; }

    //Base stats for the weapon
    [SerializeField]
    float damage;
    public float Damage { get => damage; private set => damage = value; }

    [SerializeField]
    float speed;
    public float Speed { get => speed; private set => speed = value; }

    [SerializeField]
    float cooldownDuration;
    public float CooldownDuration { get => cooldownDuration; private set => cooldownDuration = value; }

    [SerializeField]
    int pierce;
    public int Pierce { get => pierce; private set => pierce = value; }
}

WeaponController.cs

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

/// <summary>
/// Base script for all weapon controllers
/// </summary>
public class WeaponController : MonoBehaviour
{
    [Header("Weapon Stats")]
    public WeaponScriptableObject weaponData;
    float currentCooldown;

    protected PlayerMovement pm;


    protected virtual void Start()
    {
        pm = FindObjectOfType<PlayerMovement>();
        currentCooldown = weaponData.CooldownDuration; //At the start set the current cooldown to be cooldown duration
    }

    protected virtual void Update()
    {
        currentCooldown -= Time.deltaTime;
        if (currentCooldown <= 0f)   //Once the cooldown becomes 0, attack
        {
            Attack();
        }
    }

    protected virtual void Attack()
    {
        currentCooldown = weaponData.CooldownDuration;
    }
}

KnifeController.cs

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

public class KnifeController : WeaponController
{
    protected override void Start()
    {
        base.Start();
    }

    protected override void Attack()
    {
        base.Attack();
        GameObject spawnedKnife = Instantiate(weaponData.Prefab);
        spawnedKnife.transform.position = transform.position; //Assign the position to be the same as this object which is parented to the player
        spawnedKnife.GetComponent<KnifeBehaviour>().DirectionChecker(pm.lastMovedVector);   //Reference and set the direction
    }
}

GarlicController.cs

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

public class GarlicController : WeaponController
{
    protected override void Start()
    {
        base.Start();
    }

    protected override void Attack()
    {
        base.Attack();
        GameObject spawnedGarlic = Instantiate(weaponData.Prefab);
        spawnedGarlic.transform.position = transform.position; //Assign the position to be the same as this object which is parented to the player
        spawnedGarlic.transform.parent = transform;
    }
}

ProjectileWeaponBehaviour.cs

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

/// <summary>
/// Base script of all projectile behaviours [To be placed on a prefab of a weapon that is a projectile]
/// </summary>
public class ProjectileWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    protected Vector3 direction;

    public float destroyAfterSeconds;

    //Current stats
    protected float currentDamage;
    protected float currentSpeed;
    protected float currentCooldownDuration;
    protected int currentPierce;

    void Awake()
    {
        currentDamage = weaponData.Damage;
        currentSpeed = weaponData.Speed;
        currentCooldownDuration = weaponData.CooldownDuration;
        currentPierce = weaponData.Pierce;
    }


    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

    public void DirectionChecker(Vector3 dir)
    {
        direction = dir;

        float dirx = direction.x;
        float diry = direction.y;

        Vector3 scale = transform.localScale;
        Vector3 rotation = transform.rotation.eulerAngles;

        if (dirx < 0 && diry == 0) //left
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry < 0) //down
        {
            scale.y = scale.y * -1;
        }
        else if (dirx == 0 && diry > 0) //up
        {
            scale.x = scale.x * -1;
        }
        else if (dirx > 0 && diry > 0) //right up
        {
            rotation.z = 0f;
        }
        else if (dirx > 0 && diry < 0) //right down
        {
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry > 0) //left up
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = -90f;
        }
        else if (dirx < 0 && diry < 0) //left down
        {
            scale.x = scale.x * -1;
            scale.y = scale.y * -1;
            rotation.z = 0f;
        }

        transform.localScale = scale;
        transform.rotation = Quaternion.Euler(rotation);    //Can't simply set the vector because cannot convert
    }

    protected virtual void OnTriggerEnter2D(Collider2D col)
    {
        //Reference the script from the collided collider and deal damage using TakeDamage()
        if(col.CompareTag("Enemy"))
        {
            EnemyStats enemy = col.GetComponent<EnemyStats>();
            enemy.TakeDamage(currentDamage);    //Make sure to use currentDamage instead of weaponData.Damage in case any damage multipliers in the future
            ReducePierce();
        }
    }

    void ReducePierce() //Destroy once the pierce reaches 0
    {
        currentPierce--;
        if (currentPierce <= 0)
        {
            Destroy(gameObject);
        }
    }
}

MeleeWeaponBehaviour.cs

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

/// <summary>
/// Base script of all melee behaviours [To be placed on a prefab of a weapon that is melee]
/// </summary>
public class MeleeWeaponBehaviour : MonoBehaviour
{
    public WeaponScriptableObject weaponData;

    public float destroyAfterSeconds;

    //Current stats
    protected float currentDamage;
    protected float currentSpeed;
    protected float currentCooldownDuration;
    protected int currentPierce;

    void Awake()
    {
        currentDamage = weaponData.Damage;
        currentSpeed = weaponData.Speed;
        currentCooldownDuration = weaponData.CooldownDuration;
        currentPierce = weaponData.Pierce;
    }

    protected virtual void Start()
    {
        Destroy(gameObject, destroyAfterSeconds);
    }

    protected virtual void OnTriggerEnter2D(Collider2D col)
    {
        if (col.CompareTag("Enemy"))
        {
            EnemyStats enemy = col.GetComponent<EnemyStats>();
            enemy.TakeDamage(currentDamage);
        }
    }
}

KnifeBehaviour.cs

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

public class KnifeBehaviour : ProjectileWeaponBehaviour
{

    protected override void Start()
    {
        base.Start();
    }

    void Update()
    {
        transform.position += direction * weaponData.Speed * Time.deltaTime;    //Set the movement of the knife
    }
}

EnemyScriptableObject.cs

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


[CreateAssetMenu(fileName = "EnemyScriptableObject", menuName = "ScriptableObjects/Enemy")]
public class EnemyScriptableObject : ScriptableObject
{
    //Base stats for the enemy
    [SerializeField]
    float moveSpeed;
    public float MoveSpeed { get => moveSpeed; private set => moveSpeed = value; }

    [SerializeField]
    float maxHealth;
    public float MaxHealth { get => maxHealth; private set => maxHealth = value; }

    [SerializeField]
    float damage;
    public float Damage { get => damage; private set => damage = value; }
}

EnemyMovement.cs

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

public class EnemyMovement : MonoBehaviour
{
    public EnemyScriptableObject enemyData;
    Transform player;


    void Start()
    {
        player = FindObjectOfType<PlayerMovement>().transform;
    }

    void Update()
    {
        transform.position = Vector2.MoveTowards(transform.position, player.transform.position, enemyData.MoveSpeed * Time.deltaTime);    //Constantly move the enemy towards the player
    }
}

EnemyStats.cs

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

public class EnemyStats : MonoBehaviour
{
    public EnemyScriptableObject enemyData;

    //Current stats
    float currentMoveSpeed;
    float currentHealth;
    float currentDamage;

    void Awake()
    {
        //Assign the vaiables
        currentMoveSpeed = enemyData.MoveSpeed;
        currentHealth = enemyData.MaxHealth;
        currentDamage = enemyData.Damage;
    }

    public void TakeDamage(float dmg)
    {
        currentHealth -= dmg;

        if (currentHealth <= 0)
        {
            Kill();
        }
    }

    public void Kill()
    {
        Destroy(gameObject);
    }
}

GarlicBehaviour.cs

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

public class GarlicBehaviour : MeleeWeaponBehaviour
{
    List<GameObject> markedEnemies;

    protected override void Start()
    {
        base.Start();
        markedEnemies = new List<GameObject>();
    }

    protected override void OnTriggerEnter2D(Collider2D col)
    {
        if (col.CompareTag("Enemy") && !markedEnemies.Contains(col.gameObject))
        {
            EnemyStats enemy = col.GetComponent<EnemyStats>();
            enemy.TakeDamage(currentDamage);

            markedEnemies.Add(col.gameObject);  //Mark the enemy
        }
    }
}

Article continues after the advertisement:


There are 4 comments:

Leave a Reply

Your email address will not be published. Required fields are marked *

For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

I agree to these terms.