Website Logo

Part 36

Death and Damage Particles

2D Platformer Player Controller

Introduction

Now that we are able to damage our entities it is time to start giving some visual indication when an entity is damaged. We can also go ahead and make it so that our entities are able to die. To do this we are going to make use of two new CoreComponents: the ParticleManager core component, and Death core component. The ParticleManager will hold functions that will be responsible for instantiating particles. Death will be responsible (for now only) for disabling an entity when it’s health runs out. Eventually, more core components will inherit from Death that will each be responsible for what happens when a certain entity type dies. For example, when the player dies we might want to show a game over screen, reload the last save, or simply go back to the last checkpoint. When an enemy dies we might want to add the enemy back into a pool, let the enemy manager know, and open a room door. This is why we made the changes to the Core in the previous part of the series. To facilitate having the different types of core components on different entities.

Bug Fix

I’m not sure how this one slipped through the cracks. I feel like I fixed it before but maybe it was just a dream. So our bug for the day is: When the character crouches through a low area and the user tries to jump, the character will get stuck in the walls.

If you are experiencing this I challenge you to try and solve it yourself first. 

The solution is rather simple. We forgot to do a ceiling check before transitioning to the PlayerJumpState from the PlayerGroundedState. Let’s consider the PlayerGroundedState.

PlayerGroundedState.cs
				
					using UnityEngine;

public class PlayerGroundedState : PlayerState
{
    protected int xInput;
    protected int yInput;

    protected bool isTouchingCeiling;

    protected Movement Movement { get => movement ?? core.GetCoreComponent(ref movement); }
    protected Movement movement;

    private bool JumpInput;
    private bool grabInput;
    private bool isGrounded;
    private bool isTouchingWall;
    private bool isTouchingLedge;
    private bool dashInput;

    private CollisionSenses CollisionSenses { get => collisionSenses ?? core.GetCoreComponent(ref collisionSenses); }
    private CollisionSenses collisionSenses;

    public PlayerGroundedState(Player player, PlayerStateMachine stateMachine, PlayerData playerData, string animBoolName) : base(player, stateMachine, playerData, animBoolName)
    {
    }

    public override void DoChecks()
    {
        base.DoChecks();

        if (CollisionSenses)
        {
            isGrounded = CollisionSenses.Ground;
            isTouchingWall = CollisionSenses.WallFront;
            isTouchingLedge = CollisionSenses.LedgeHorizontal;
            isTouchingCeiling = CollisionSenses.Ceiling;
        }        
    }

    public override void Enter()
    {
        base.Enter();

        player.JumpState.ResetAmountOfJumpsLeft();
        player.DashState.ResetCanDash();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void LogicUpdate()
    {
        base.LogicUpdate();

        xInput = player.InputHandler.NormInputX;
        yInput = player.InputHandler.NormInputY;
        JumpInput = player.InputHandler.JumpInput;
        grabInput = player.InputHandler.GrabInput;
        dashInput = player.InputHandler.DashInput;

        if (player.InputHandler.AttackInputs[(int)CombatInputs.primary] && !isTouchingCeiling)
        {
            stateMachine.ChangeState(player.PrimaryAttackState);
        }
        else if (player.InputHandler.AttackInputs[(int)CombatInputs.secondary] && !isTouchingCeiling)
        {
            stateMachine.ChangeState(player.SecondaryAttackState);
        }
        else if (JumpInput && player.JumpState.CanJump())
        {
            stateMachine.ChangeState(player.JumpState);
        }else if (!isGrounded)
        {
            player.InAirState.StartCoyoteTime();
            stateMachine.ChangeState(player.InAirState);
        }else if(isTouchingWall && grabInput && isTouchingLedge)
        {
            stateMachine.ChangeState(player.WallGrabState);
        }
        else if (dashInput && player.DashState.CheckIfCanDash() && !isTouchingCeiling)
        {
            stateMachine.ChangeState(player.DashState);
        }
    }

    public override void PhysicsUpdate()
    {
        base.PhysicsUpdate();
    }
}

				
			

In the script above notice lines 63 and 67. In the if statements we are checking to see if !isTouchingCeiling before allowing the transition to our primary or secondary attack states. We simply need to do the same in line 71. So:

				
					else if (JumpInput && player.JumpState.CanJump())
				
			

Becomes:

				
					else if (JumpInput && player.JumpState.CanJump() && !isTouchingCeiling)
				
			

And that’s it. Bug squashed. Let’s move on.

Creating the Particle Manager

Let’s go ahead and start with the ParticleManager core component. In the Assets > Scripts > Core > CoreComponents folder, create a new C# script and call it ParticleManager.

Open this script and delete the pre-generated code. We can also get rid of MonoBehaviour and make it inherit from CoreComponent instead. We can also remove all the imported libraries at the top except for UnityEngine.

ParticleManager.cs
				
					using UnityEngine;

public class ParticleManager : CoreComponent
{

}
				
			

Now, let’s discuss how the ParticleManager is going to work. The particle manager is essentially a collection of functions that allow us to spawn particles on the entity with various other parameters such as rotation and position. If we wanted we could even define specific points where particles could be spawned without having to pass in a position. The particle manager itself will not hold any of the particles. It merely facilitates putting spawning the particles.

When we spawn particles we cannot set the entity as a parent as we want the particles to stay in word space and not track on the entity. So let us go ahead and create a new GameObject called Particles that will be used as a container for the instantiated particles so that we can keep our hierarchy organized.

In the Hierarchy, right click and select Create Empty. Name the new game object Particles.

Don’t forget to reset the transform of this game object by clicking on the tree dots at the top right of the component and selecting Reset.

So the first thing the ParticleManager needs is a reference to this new game object. Let’s give the game object a tag so that we can find the game object by tag at the start of the game. Click on the dropdown next to tag and select Add Tag… 

In the Tags & Layers inspector we can then hit the + and add the new tag called ParticleContainer.

After that we need to remember to set the tag of the Particles game object.

We can now reference this game object in the ParticleManager.

				
					using UnityEngine;

public class ParticleManager : CoreComponent
{
  // Transform that will be the parent of spawned particles
  private Transform particleContainer;

  protected override void Awake()
  {
    base.Awake();

    // Setting the reference
    particleContainer = GameObject.FindGameObjectWithTag("ParticleContainer").transform;
  }
}
				
			

We start off by creating a private variable of the Transform type and we call it particleContainer. Then we can override the base Awake function. Don’t forget to call base.Awake(); to make sure the base core component awake function is still called. Inside the awake function we can then set the reference to the container game object using:

GameObject.FindGameObjectWithTag() – This function takes in string which is the tag we are looking for. Now unfortunately there is no tag list to choose from so we need to ensure that our spelling here is the same as when we created the tag.

.transform – is then simply used to get the transform of the returned game object.

Now that we have this reference we can instantiate our particles and set them as children of this game object. So let’s go ahead and create the function that will be responsible for this. After Awake:

				
					public GameObject StartParticles(GameObject particlesPrefab, Vector2 position, Quaternion rotation)
  {
    return Instantiate(particlesPrefab, position, rotation, particleContainer);
  }
				
			

Let’s break down this function, starting with the function declaration:

				
					public GameObject StartParticles(GameObject particlesPrefab, Vector2 position, Quaternion rotation)
  
				
			

public -We use the public access modifier as we want to call this function from other scripts

GameObject – The return type as we will return the instantiated particle in case the script that calls this function has use for it.

We then have three input parameters:

  1. GameObject particlesPrefab – This is the particles the caller wishes to instantiate for the entity.
  2. Vector2 position – The position where the caller wishes to instantiate the particles
  3. Quaternion rotation – The rotation the caller wishes the instantiated particles to have

 

The inside of the function is simply one line:

				
					return Instantiate(particlesPrefab, position, rotation, particleContainer);
				
			

This makes use of the last version of the Instantiate function that. It takes in all the parameters passed to the StartParticles function with the addition of a parent parameter. Calling this function will instantiate the particlesPrefab at position with rotation and also set the parent to particleContainer. Finally it returns the particles GameObject that can then be further returned by our StartParticles function.

What about if we don’t want to specify a position and rotation? Or want to have a random rotation? Let’s go ahead and create two more functions that take care of this for us. Let’s start with a function that will spawn the particles at the origin of the core component and no rotation:

 

				
					public GameObject StartParticles(GameObject particlesPrefab)
  {
    return StartParticles(particlesPrefab, transform.position, Quaternion.identity);
  }
				
			

So in this function take in one parameter: the prefab we want to spawn. It then makes use of the original StartParticles() function and passes the prefab through along with the position of the core component’s transform and Quaternion.identity

Next, let’s create the function that will spawn the particle with a random rotation at the core component’s transform.

				
					public GameObject StartParticlesWithRandomRotation(GameObject particlesPrefab)
  {
    // Generate a random rotation along the z-axis
    var randomRotation = Quaternion.Euler(0f, 0f, Random.Range(0f, 360f));
    // Spawn the particle and return
    return StartParticles(particlesPrefab, transform.position, randomRotation);
  }
				
			

This function works like the rest but it has a different name as we cannot have two functions with the same name that also has the same parameter types. We start by creating a Quaternion with a random rotation on the z-axis. Then we call the StartParticles function using our transform.position  and the generated rotation.

Our ParticleManager script should now look like this:

ParticleManager.cs
				
					using UnityEngine;

public class ParticleManager : CoreComponent
{
  private Transform particleContainer;

  protected override void Awake()
  {
    base.Awake();

    particleContainer = GameObject.FindGameObjectWithTag("ParticleContainer").transform;
  }

  public GameObject StartParticlesWithRandomRotation(GameObject particlesPrefab)
  {
    var randomRotation = Quaternion.Euler(0f, 0f, Random.Range(0f, 360f));
    return StartParticles(particlesPrefab, transform.position, randomRotation);
  }

  public GameObject StartParticles(GameObject particlesPrefab)
  {
    return StartParticles(particlesPrefab, transform.position, Quaternion.identity);
  }

  public GameObject StartParticles(GameObject particlesPrefab, Vector2 position, Quaternion rotation)
  {
    return Instantiate(particlesPrefab, position, rotation, particleContainer);
  }
}

				
			

Damage Particles

Let’s get some particles spawning when the player or enemies are damaged. We will put the reference to our damage particle prefab on our Combat core component so that when the Damage() function is called, we can start the particle from there.

So let’s consider the Combat.cs script.

Combat.cs
				
					using UnityEngine;

public class Combat : CoreComponent, IDamageable, IKnockbackable
{
    [SerializeField] private float maxKnockbackTime = 0.2f;

    private bool isKnockbackActive;
    private float knockbackStartTime;

    private Stats Stats { get => stats ?? core.GetCoreComponent(ref stats); }
    private CollisionSenses CollisionSenses { get => collisionSenses ?? core.GetCoreComponent(ref collisionSenses); }
    private Movement Movement { get => movement ?? core.GetCoreComponent(ref movement); }
   
    private Movement movement;
    private Stats stats;
    private CollisionSenses collisionSenses;
    

    public override void LogicUpdate()
    {
        CheckKnockback();
    }

    public void Damage(float amount)
    {
        Debug.Log(core.transform.parent.name + " Damaged!");
        Stats?.DecreaseHealth(amount);
    }

    public void Knockback(Vector2 angle, float strength, int direction)
    {
        Movement?.SetVelocity(strength, angle, direction);
        Movement.CanSetVelocity = false;
        isKnockbackActive = true;
        knockbackStartTime = Time.time;
    }

    private void CheckKnockback()
    {
        if(isKnockbackActive && Movement.CurrentVelocity.y <= 0.01f && (CollisionSenses.Ground || Time.time >= knockbackStartTime + maxKnockbackTime))
        {
            isKnockbackActive = false;
            Movement.CanSetVelocity = true;
        }
    }
}

				
			

By our variable declerations, let’s go ahead and add:

				
					[SerializeField] private GameObject damageParticles;
				
			

With a reference to the damage particles, we now need a reference to the ParticleManager so that we can tell it to spawn this particle once the entity is damaged. So let’s go ahead and add:

				
					//... Other Core Components
  private ParticleManager ParticleManager { get => particleManager ?? core.GetCoreComponent(ref particleManager); }

  //... Other Core Components
  private ParticleManager particleManager;
				
			

With this reference set, let’s go ahead and spawn some particles when the damage function is called.

				
					public void Damage(float amount)
  {
    Debug.Log(core.transform.parent.name + " Damaged!");
    Stats?.DecreaseHealth(amount);
    ParticleManager?.StartParticlesWithRandomRotation(damageParticles);    
  }

				
			

In the Damage() function we can access the ParticleManager reference and call the StartParticlesWithRandomRotation() function, passing in the damageParticles variable.

Now before we can test we need to do some setup in Unity. We need to add the ParticleManager game object to our entities and set the reference to the damage particles we want to use. Let’s start with the Player:

Let’s go ahead and add a new empty game object under the Player’s Core game object. 

Go ahead and rename it to ParticleManager.

Then go ahead and add the ParticleManager script.

We can then come to the Combat core component, and drag in our Enemy1HitParticle prefab. Feel free to rename it to something more sensible.

Go ahead and repeat this on the two enemies and then we should be ready to test. You should now be able to see particles being instantiated when the player or enemies are damaged.

Death

Let us now deal with the Death core component. In Assets > Scripts > Core > CoreComponents create a new C# script and call it Death. Like before, go ahead and get rid of the pre-generated code and make it inherit from CoreComponent

Death.cs
				
					using UnityEngine;

public class Death : CoreComponent
{
    
}
				
			

Now as I mentioned before, the Death core component will not have too much functionality yet. For now all it will be responsible for is listening for when the entity’s health reaches zero, playing some death particles, and disabling the entity.

Notice that I said we are going to be listening for when the entity’s health reaches zero. This is because we are going to make use of events and delegates. Now this is not going to be an in depth tutorial on that topic so if those two words are completely foreign to you, I suggest you check out these two tutorials by Code Monkey:

Events

Delegates

I will at least try and cover the basics here though. If you are interested in me doing a more in depth tutorial, let me know 😀

Events:

Events in C# are used by classes or objects to notify other classes or objects when something of interest has happened. The class that raises the event is called the publisher and the classes that handle the event are called subscribers. So basically, a class can use an event to notify the world when something happens. And classes that care about knowing when that thing has happened can subscribe to that event. This means that the class that is raising the event does not need to know about all the other classes that might care about that information. So using events allow us to decrease the dependency between classes. However, the classes that do care about that information need to know about the class that raises the event in order to subscribe to it. We will look at how to implement an event soon in our Stats core component.

Delegates

A delegate is a type. It represents references to methods that have a particular parameter list and return type, otherwise known as the method signature. When you create a function it has a function signature. This signature is made up of input parameters and return type of the function. You can store your functions in the delegate so that when you invoke the delegate, all those functions are called. This essentially allows us to pass functions around as if they were variables. 

Action

Action is a default delegate that ships with C#. It represents a function that has between 0 and 16 input parameters and that returns void. This is a very useful delegate as it covers a wide variety of use cases, including ours, meaning we don’t need to create our own delegates just yet.

Okay so let’s consider our Death core component again. Let’s start by creating the function that will disable the entity:

				
					public void Die()
  {
    core.transform.parent.gameObject.SetActive(false);
  }
				
			

So this function is public because it will be called from outside the Death core component. It does not return anything so the return type is void, and we will just call it Die(). This function also does not have any input parameters. Inside the function we say:

				
					core.transform.parent.gameObject.SetActive(false);
				
			

We start by referencing the Core this core component belongs to. From there we say .transform to reference the GameObject where the Core script is attached. Then we hve .parent which will return the GameObject one level higher than our Core. The way we have it set up our Core will always be a direct child of the top most parents GameObject of an entity. So if you do not have it set up like this you will have to adjust this line. We then reference the parent’s GameObject with .gameObject, and finally we call the .SetActive() function and pass in False. This will disable the whole entity.

Let’s also go ahead and spawn some particles when the entity dies. First we need to create references for the particles we want to use.

				
					[SerializeField] private GameObject[] deathParticles;
				
			

Because we have two death particles, the blood particles and the chunk particles, instead of declaring those as two separate variables we’ll make an array. Then we can have any number of particle systems to build the perfect death for an entity and all we need to do is loop through the array and spawn them all. So with that reference we now need a reference to the ParticleManager core component.

				
					private ParticleManager ParticleManager { get => particleManager ?? core.GetCoreComponent(ref particleManager); }
  private ParticleManager particleManager;
				
			

With that done, we can then come back to our Die() function and call the StartParticles function.

				
					public void Die()
  {
    foreach (var particle in deathParticles)
    {
      ParticleManager.StartParticles(particle);
    }

    core.transform.parent.gameObject.SetActive(false);
  }
				
			

So all we add is a foreach loop that will loop through all the particles in deathParticles and then call the StartParticles method from the ParticleManager with the particle as the parameter.

So now we are ready to call this function when the entity dies. Let’s consider our Stats core component.

Stats.cs
				
					using UnityEngine;

public class Stats : CoreComponent
{
    [SerializeField] private float maxHealth;
    private float currentHealth;

    protected override void Awake()
    {
        base.Awake();

        currentHealth = maxHealth;
    }

    public void DecreaseHealth(float amount)
    {
        currentHealth -= amount;

        if(currentHealth <= 0)
        {
            currentHealth = 0;
            Debug.Log("Health is zero!!");
        }
    }

    public void IncreaseHealth(float amount)
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
    }
}

				
			

We are going to make use of a C# event to notify other classes when our health reaches zero. But why do this instead of just referencing our Death core component and calling the Die function? Well that would also be an okay approach. But there might be many other systems interested in knowing when an entity dies. Maybe not every entity dies when our health reaches zero. And it’s just a good time to start getting exposure to events.

So let’s start by declaring our event by our varibales:

 

				
					// Import System namespace to get Action delegate
using System;
using UnityEngine;

public class Stats : CoreComponent
{
    [SerializeField] private float maxHealth;
    private float currentHealth;

    // Our event
    public event Action HealthZero; 
    //... Rest of the class
				
			

Because we are making use of the Action delegate, we need to include the System name space as is done in line 1. We can then declare our event like in line 11. Let’s walk through it word for word.

public – like any other variable we would declare public because we want to access it from other classes. In this case we want to subscribe to this event so it needs to be public.

event – The event keyword is used to declare that this is an event in a publisher class.

Action – The action delegate is our “type”. It specifies what the method signature needs to be if it wants to subscribe to this event. Action is generic. We can define the input type like this for example: Action – A function that takes in a single int input and returns void. Or: Action<float, GameObject> – A function that takes in a float and GameObject as parameters and returns void. In our case because there are no <> brackets, we are saying it is a function that takes in no parameters and returns void.

HealthZero – This is just the event name.

With this we can then come to our DecreaseHealth function and invoke the event when our health reaches zero.

				
					public void DecreaseHealth(float amount)
    {
        currentHealth -= amount;

        if(currentHealth <= 0)
        {
            currentHealth = 0;
            // Invoke the event. ?. needed to avoid errors if there are no subscribers.
            HealthZero?.Invoke();
            Debug.Log("Health is zero!!");
        }
    }
				
			

So when our currentHealth <= 0, we set it back to 0 and then invoke our event. Notice that we are using the null-conditional operator as if we try to invoke the event when there are no subscribers will throw an error. Also note that if we were to use a version of Action that did have input parameters, we would pass these parameters to the Invoke() function. And that is it. Stats should now look like this:

Stats.cs
				
					using System;
using UnityEngine;

public class Stats : CoreComponent
{
    [SerializeField] private float maxHealth;
    private float currentHealth;

    public event Action HealthZero; 

    protected override void Awake()
    {
        base.Awake();

        currentHealth = maxHealth;
    }

    public void DecreaseHealth(float amount)
    {
        currentHealth -= amount;

        if(currentHealth <= 0)
        {
            currentHealth = 0;
            HealthZero?.Invoke();
            Debug.Log("Health is zero!!");
        }
    }

    public void IncreaseHealth(float amount)
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
    }
}

				
			

 That is all we need to do to create the event. Now we need to subscribe to it.

Subscribing to the Event

Please don’t hate me but I made a mistake. In part 35 we were presented with two option how how to get the CoreComponent references set in the Core. We chose the method where each component would add themselves to the list on the core instead of having the core detect all children core components. Now why is this an issue? Well our Death core component needs to subscribe to the HealthZero event on the Stats core component. Usually you subscribe to events in the Start or Awake functions at the start of the game. However, we cannot do this in the CoreComponent‘s awake function as we can  again potentially run into some sequencing issues the Death awake is called before Stats has added itself to the list. And another problem we have is, Death does not make use of any functions or properties on Stats, so our method for setting the reference the first time it is used will not work.

So how do we fix this? We are going to switch over to the other approach where the Core detects all CoreComponent children. Then, after the core has added them all to the list, it will go through the list and call a new Init function where we can set those references. We can still use the functions we created before as a safety measure, but we need this to subscribe to the events.

Let’s consider the Core again.

Core.cs
				
					using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class Core : MonoBehaviour
{
    public readonly List<CoreComponent> CoreComponents = new List<CoreComponent>();

    public void LogicUpdate()
    {
        foreach (CoreComponent component in CoreComponents)
        {
            component.LogicUpdate();
        }
    }

    public T GetCoreComponent<T>() where T:CoreComponent
    {
        var comp = CoreComponents
            .OfType<T>()
            .FirstOrDefault();

        if (comp == null)
        {
            Debug.LogWarning($"{typeof(T)} not found on {transform.parent.name}");
        }

        return comp;
    }

    public T GetCoreComponent<T>(ref T value) where T:CoreComponent
    {
        value = GetCoreComponent<T>();
        return value;
    }

    public void AddComponent(CoreComponent component)
    { 
        if (!CoreComponents.Contains(component))
        {
            CoreComponents.Add(component);
        }
    }
}

				
			

So we need to add the Awake function and then find all the children CoreComponents.

				
					 private void Awake()
  {
    // Find all core component children
    var comps = GetComponentsInChildren<CoreComponent>();
    
    // Add componets found to list. Use old function to avoid duplicates.
    foreach (var component in comps)
    {
      AddComponent(component);
    }
    
    // Call Init on each 
    foreach (var component in CoreComponents){
      component.Init(this);
    }
  }

				
			

In the Awake function we start by calling the GetComponentsInChildren function that will return a collection off all objects found that are of the passed type. We then loop through this collection and add the component to our CoreComponents list using the AddComponent function. After that we loop through the CoreComponents list and call the Init() function (That does not exist yet) and pass this is the parameter. We pass through the core so that the core components can just set the reference to the core from that instead of having to look for it as well.

Now let’s consider our base CoreComponent script.

CoreComponent.cs
				
					using UnityEngine;

public class CoreComponent : MonoBehaviour, ILogicUpdate
{
    protected Core core;

    protected virtual void Awake()
    {
        core = transform.parent.GetComponent<Core>();

        if (core == null) { Debug.LogError("There is no Core on the parent"); }
        core.AddComponent(this);
    }

    public virtual void LogicUpdate() { }
}

				
			

We can get rid of everything in or Awake function as our Init function will now be responsible for setting the reference to the Core. Also note that the Init function is going to be a virtual function so that any core component can expand it. So we end up with:

CoreComponent.cs
				
					using UnityEngine;

public class CoreComponent : MonoBehaviour, ILogicUpdate
{
  protected Core core;

  public virtual void Init(Core core){
    this.core = core;
  }

  protected virtual void Awake() { }

  public virtual void LogicUpdate() { }
}

				
			

So now with that handled, we can come back to our Death core component and subscribe to the event. But first we need to create the variable for our Stats reference.

				
					private Stats Stats { get => stats ?? core.GetCoreComponent(ref stats); }
  private Stats stats;
				
			

Then we can create our override function for the Init function, inside of which we will subscribe to our event.

				
					public override void Init(Core core)
  {
    base.Init(core);

    Stats.HealthZero += Die;
  }
				
			

Don’t forget to call the base Init function. So to subscribe to an event you simply say += Die. Notice that we are saying Die  and not Die(). So we are only using the name of the function. Now we also need to unsubscribe (-=) from the event when the script is disabled to avoid getting errors. We can also subscribe to the event again when the script is enabled just to be thorough.

				
					private void OnEnable() {
    Stats.HealthZero += Die;
  }

  private void OnDisable() {
    Stats.HealthZero -= Die;
  }
				
			

Finally, our Death core component script should look like this:

Death.cs
				
					using UnityEngine;

public class Death : CoreComponent
{
  [SerializeField] private GameObject[] deathParticles;

  private Stats Stats { get => stats ?? core.GetCoreComponent(ref stats); }
  private Stats stats;

  private ParticleManager ParticleManager { get => particleManager ?? core.GetCoreComponent(ref particleManager); }
  private ParticleManager particleManager;

  public override void Init(Core core)
  {
    base.Init(core);

    Stats.HealthZero += Die;
  }

  public void Die()
  {
    foreach (var particle in deathParticles)
    {
      ParticleManager.StartParticles(particle);
    }

    core.transform.parent.gameObject.SetActive(false);
  }

  private void OnEnable() {
    Stats.HealthZero += Die;
  }

  private void OnDisable() {
    Stats.HealthZero -= Die;
  }
}

				
			

We can now go ahead and add the Death core component to all of our entities, drag in our particle prefabs, and test the game.

Conclusion

There we go. Now when an entity is damaged or dies we at least get some sort of visual indication. Out ParticleManager script is not done yet though. As we move on to the rest of the weapons you will see how we are going to add more functions that allow us to conveniently spawn particles in certain ways. Our Death script is also not complete. This is just a small demonstration of how the different systems will interact. The Death script, or various scripts that inherit form the Death script will be responsible for all sorts of logic.

So that will do it for this article. If you guys have any comments or questions then please reach out to me on discord or in the comments down below. 

Once again, thank you so much to all of my patrons who make it possible for me to do this. I really appreciate you. 

Thank you to my patrons for making this possible:
ABSOLUTE MAD LADS - Cody Lee - Peter Smith - Ryuha - Madger Sins - SM - Kareem Butler - pyro says - jeremiah miranda - Tlean Vasner - BinaryChef - Alex from OKda - Borgia MK Ultra - Gregory McManus - Levente Tóth - Anthony - Neska Fé - Hari Dimitriou - Pa Dwipa - Mason Crowe - WONDERFUL PEOPLE - ravel - Oskar Antretter - Emilio Vacca - Clay Bailes - Xander Epelman - Zarralax - Hardcore Virgin - Erendrood - jErkan - nathan leblanc-limoges - JC System - SUPPORTERS - Joxev - Triak - Kevin Malaka - Ana Grace - Don Nguyen - Peter Siri - JOHN-ROBERT STEWART - Afflicted Mind - Pitou - Thomas Bing - Daniel Sibaja - kalmiya - Mayke Rodrigues - Juan Collin - Matte - Patrick P - Domantas Gudonis - MMUS - Matthew Courtnell - Onur - RO LI - Crazy Potato - Pooty Lim - Todd - Furkan Kursav -
ABSOLUTE MAD LADS - Cody Lee - Peter Smith - Ryuha - Madger Sins - SM - Kareem Butler - pyro says - jeremiah miranda - Tlean Vasner - BinaryChef - Alex from OKda - Borgia MK Ultra - Gregory McManus - Levente Tóth - Anthony - Neska Fé - Hari Dimitriou - Pa Dwipa - Mason Crowe - WONDERFUL PEOPLE - ravel - Oskar Antretter - Emilio Vacca - Clay Bailes - Xander Epelman - Zarralax - Hardcore Virgin - Erendrood - jErkan - nathan leblanc-limoges - JC System - SUPPORTERS - Joxev - Triak - Kevin Malaka - Ana Grace - Don Nguyen - Peter Siri - JOHN-ROBERT STEWART - Afflicted Mind - Pitou - Thomas Bing - Daniel Sibaja - kalmiya - Mayke Rodrigues - Juan Collin - Matte - Patrick P - Domantas Gudonis - MMUS - Matthew Courtnell - Onur - RO LI - Crazy Potato - Pooty Lim - Todd - Furkan Kursav -
Twitter
Facebook
Reddit
WhatsApp
Email
0 0 votes
What others thought of this tutorial
Subscribe
Notify of
guest
3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Damoeson
Damoeson
2 years ago

Very good tutorial thank you

K M
K M
2 years ago

Thank you for the great tutorials! I have a question about the part you change the core componet Init(). Why cant we just do the subscribe in the start function like below? It seems to me start funcitons are after every awake function, I don’t quite get why there will be a problem?

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

    Stats.HealthZero += Die;
  }

Mihail
Mihail
1 year ago

it is necessary to implement death as a state with animation of the fall of the player and the enemy. the reaction to the blow in the form of animation is also needed.