Outline | Previous: HoverGuy | Next: GameController
todo
- explain enabled check (not redundant) and why trigger and collider check more.
- suggest you could make the explosion a child of the character
- define singleton better?
- why use an event / action
Spawn an explosion and animate the character's death.
- 6.1) Death Effect Pattern
- 6.2) Kill the Character
- 6.3) Create an Explosion
- 6.4) Destroy Explosions
- 6.5) Spawn Explosions
- 6.6) Animate the Character's Death
YouTube | Source before | Source after
Create a pattern to use instead of destroying GameObjects directly, allowing an opportunity for objects to animate on death.
How
Create DeathEffect:
- Create script Code/Death/DeathEffect:
using UnityEngine;
[RequireComponent(typeof(DeathEffectManager))]
public abstract class DeathEffect : MonoBehaviour
{
public abstract float PlayDeathEffects();
}Note there will be a compile error until we add DeathEffectManager.
Create DeathEffectManager:
- Create script Code/Death/DeathEffectManager:
using UnityEngine;
public class DeathEffectManager : MonoBehaviour
{
bool isInProcessOfDying;
public static void PlayDeathEffectsThenDestroy(
GameObject gameObjectToDestroy)
{
DeathEffectManager deathEffectManager
= gameObjectToDestroy.GetComponent<DeathEffectManager>();
if(deathEffectManager == null)
{
Destroy(gameObjectToDestroy);
return;
}
deathEffectManager.PlayDeathEffectsThenDestroy();
}
void PlayDeathEffectsThenDestroy()
{
if(isInProcessOfDying)
{
return;
}
isInProcessOfDying = true;
DeathEffect[] deathEffectList
= gameObject.GetComponentsInChildren<DeathEffect>();
float maxTimeTillDestroy = 0;
for(int i = 0; i < deathEffectList.Length; i++)
{
DeathEffect deathEffect = deathEffectList[i];
float timeTillDestroy = deathEffect.PlayDeathEffects();
maxTimeTillDestroy = Mathf.Max(
maxTimeTillDestroy,
timeTillDestroy);
}
Destroy(gameObject, maxTimeTillDestroy);
}
}Note that there is nothing to test yet.
Explain the code
DeathEffect:
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.RequireComponentAttribute
- UnityEngine.MonoBehaviour
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a DeathEffectManager component on the GameObject.
[RequireComponent(typeof(DeathEffectManager))]We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
DeathEffect is abstract, meaning only classes which inherit from this may be added as a component to the GameObject. Those classes also inherit all the behavior that MonoBehaviour provides.
public is optional here. Used for consistency.
public abstract class DeathEffect : MonoBehaviour
{This is a method which must be implemented by classes which inherit from DeathEffect. Called by the DeathEffectManager when the entity dies and should not be called directly.
public abstract float PlayDeathEffects();
}
DeathEffectManager:
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.GameObject
- UnityEngine.Mathf
- UnityEngine.MonoBehaviour
using UnityEngine;We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class DeathEffectManager : MonoBehaviour
{This is a bool which indicates if death effects have already begun for this GameObject.
bool isInProcessOfDying;This is a method for other components to call in order to kill a GameObject, which may or may not have DeathEffects.
It is public static so that another component can call this method without first getting a reference to the DeathEffectManager component. This allows us to add logic to fallback to destroy when the GameObject does not have any death effects.
public static void PlayDeathEffectsThenDestroy(
GameObject gameObjectToDestroy)
{Here we get a reference to the DeathEffectManager component on this object, if there is one.
DeathEffectManager deathEffectManager
= gameObjectToDestroy.GetComponent<DeathEffectManager>();Check if there is no DeathEffectManager on this GameObject
if(deathEffectManager == null)
{This destroys the GameObject immediately.
Destroy(gameObjectToDestroy);There is nothing more to do, return so that the logic below is not executed.
You could use an else condition around the rest of the method instead. I often prefer this 'fail fast' approach, but it is a style decision.
return;
}This will initiate death effects on this GameObject.
deathEffectManager.PlayDeathEffectsThenDestroy();
}This is a helper method to be called by the static method above when it should play its death effects and then destroy.
void PlayDeathEffectsThenDestroy()
{If we are already playing death effects, then return - there is nothing to do. This prevents effects from doubling up (e.g. only spawn one explosion).
if(isInProcessOfDying)
{
return;
}Here we set isInProcessOfDying to true so that any future calls to play death effects on this GameObject don't cause the effects to double up.
isInProcessOfDying = true;Here we get an array of each of the death effects on this GameObject. There may be 0 or more (however if 0, the GameObject did not need to have a DeathEffectManager component).
DeathEffect[] deathEffectList
= gameObject.GetComponentsInChildren<DeathEffect>();Each death effect will return a time to wait before destroying the GameObject. maxTimeTillDestroy will track the max wait time, to be used when we call Destroy below.
float maxTimeTillDestroy = 0;Here we loop over each of the death effects. There may be 0 or more.
for(int i = 0; i < deathEffectList.Length; i++)
{This will start each death effect. timeTillDestroy is how long this death effect needs to complete, before Destroy may be called.
DeathEffect deathEffect = deathEffectList[i];
float timeTillDestroy = deathEffect.PlayDeathEffects();Here we use max with the current maxTime and the time required for this effect to track the max across all effects.
maxTimeTillDestroy = Mathf.Max(
maxTimeTillDestroy,
timeTillDestroy);
}This will destroy the GameObject after maxTimeTillDestroy has elapsed.
Destroy(gameObject, maxTimeTillDestroy);
}
}Why not just play effects OnDestroy?
When an entity dies in the game, we call DeathEffectManager.PlayDeathEffectsThenDestroy instead of the usual Unity Destroy method.
This allows us to defer the actual Destroy call, and to spawn an explosion or play an animation on the sprite as it dies. Also it allows us to differentiate between a request to immediately destroy a GameObject (e.g., for a scene change) vs a death that should maybe animate and spawn an explosion.
Additionally, OnDestroy is called anytime the object is destroyed but we only want the death effects to trigger in certain circumstances. For example, when we quit back to the main menu, we do not want explosions spawning for character being destroyed while closing level 1.
This pattern was selected because:
- It gives us easy control over when DeathEffects should be considered, vs promptly destroying the object.
- It gracefully falls back to Destroy when there are no DeathEffects to play.
- It allows for several separate DeathEffects to be combined, creating a new kind of effect.
As always, there are probably a thousand different ways you could achieve similar results.
Why is there a public method that 'should not be called directly'?
PlayDeathEffects() in the DeathEffect class has a public method with a comment saying it 'should not be called directly'. So why is it public?
In order to support multiple DeathEffects and to be able to fallback gracefully when an object does not have one, we always start effects by calling the public static method in DeathEffectManager, PlayDeathEffectsThenDestroy.
Since DeathEffectManager is a class of its own, we would not be able to call a private or protected method in DeathEffect.
'internal' could be an option to consider, but typically when working in Unity you are working in a single project - therefore internal is effectively the same as public.
You might also consider using nested classes. For simplicity in the tutorial, we're not using nested classes as they can be a bit confusing. If you are familiar with this topic, briefly you could make DeathEffectsManager a class nested inside DeathEffect and then make PlayDeathEffects() private, and the rest pretty much works the same.
Why are you using Mathf and not System.Math?
Unity offers the UnityEngine.Mathf class to try and make some things a little easier. Basically it's the same APIs which are offered from the standard System.Math class (which is also still available to use if you prefer). The main difference is all of the APIs in Mathf are focused on the float data type, where the System.Math class often prefers double. Most of the data you interact with in Unity is float.
What does GetComponentsInChildren do?
GetComponent returns a reference to the component (or script) which is the type specified or inherits from the type specified.
GetComponents returns an array with every matching component.
GetComponentInChildren returns one match, from this GameObject or one of its child GameObjects.
GetComponentsInChildren returns an array with every matching component from this GameObject and all of its children (and their children).
What's C# abstract do and how's it different from an interface?
In C#, abstract refers to a class which is incomplete and may not be instantiated directly. In order to create an object, a sub class inherits from the abstract class and you can then instantiate the sub class.
The sub class has access to everything created in the parent class, similar to if you had copy pasted everything from the parent into the child.
public abstract class MyParentClass
{
public int points;
}
public class MySubClass : MyParentClass
{
public void PrintPoints()
{
print(points);
}
}An abstract class may include an abstract method when the parent knows a method should exist, but not how it should be implemented.
public abstract class MyParentClass
{
public int points;
public abstract void PrintPoints();
}
public class MySubClass : MyParentClass
{
public override void PrintPoints()
{
print(points);
}
}This allows you to create an API that works with all sub classes of the parent.
public void Print(MyParentClass a)
{
a.PrintPoints();
}Methods may also be virtual, meaning the parent has an implementation but the child my optionally extend or replace it.
public abstract class MyParentClass
{
public int points;
public virtual void PrintPoints()
{
print(points);
}
}
public class MySubClass : MyParentClass
{
public override void PrintPoints()
{
print("You have... ");
base.PrintPoints();
}
}In C#, an interface is similar to an abstract class that has no data or non-abstract methods (including virtual). Interfaces are a way of defining a common API for classes to leverage. The name of an interface always starts with "I", by convention.
public interface IMyInterface
{
void PrintPoints();
}
public class MyClass : IMyInterface
{
public int points;
public void PrintPoints()
{
print(points);
}
}Other methods can leverage an interface without knowing the class that implemented the method like we did with the abstract class.
public void Print(IMyInterface a)
{
a.PrintPoints();
}Why not make DeathEffectManager a static class?
In order to ensure that we don't trigger death effects multiple times on the same GameObject, we have a bool, isInProcessOfDying, which stores if there is already a death in progress. For example, if an enemy walks into the Character and dies, while the Character is animating he may bump into the enemy again.
To save isInProcessOfDying per GameObject, we store it in the DeathEffectManager component for that object. Alternatively you could create a Dictionary or store this information in the DeathEffects themselves.
YouTube | Source before | Source after
When the character comes in contact with an enemy, kill him!
How
Create LayerMaskExtensions:
- Create script Code/Utils/LayerMaskExtensions:
using UnityEngine;
public static class LayerMaskExtensions
{
public static bool Includes(
this LayerMask mask,
int layer)
{
return (mask.value & (1 << layer)) > 0;
}
}
Create KillOnContactWith:
- Create script Code/Death/KillOnContactWith:
using UnityEngine;
[RequireComponent(typeof(Collider2D))]
public class KillOnContactWith : MonoBehaviour
{
[SerializeField]
LayerMask layersToKill;
protected void Start() { }
protected void OnCollisionEnter2D(
Collision2D collision)
{
TryToKill(collision.gameObject);
}
protected void OnTriggerEnter2D(
Collider2D collision)
{
TryToKill(collision.gameObject);
}
void TryToKill(
GameObject gameObjectWeHit)
{
if(enabled == false)
{
return;
}
if(layersToKill.Includes(gameObjectWeHit.layer))
{
DeathEffectManager.PlayDeathEffectsThenDestroy(
gameObjectWeHit);
}
}
}
Configure Enemies
- Select the SpikeBall prefab in Assets/Prefabs:
- Add KillOnContactWith:
- Update 'Layers To Kill' to Character.
- Add KillOnContactWith:
- Repeat for the HoverGuy prefab.
Test:
- Come in contact with the HoverGuy, confirm the Character dies.
- For now, to test again stop and hit play again. We'll respawn the player later in the tutorial.
- Repeat, coming in contact with the SpikeBall.
Explain the code
LayerMaskExtensions:
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.LayerMask
using UnityEngine;Extension methods must be in a public static class. The name of this class is irrelevant as it's not used when calling an extension method.
public static class LayerMaskExtensions
{Extension methods must be public static. This is an extension method for a LayerMask. Given a layer, return true if the layer is included in the mask.
public static bool Includes(
this LayerMask mask,
int layer)
{This is the bit shifting operations required to determine if a layer is part of a LayerMask.
return (mask.value & (1 << layer)) > 0;
}
}
KillOnContactWith:
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.Collider2D
- UnityEngine.Collision2D
- UnityEngine.GameObject
- UnityEngine.LayerMask
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
- UnityEngine.SerializeFieldAttribute
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a collider component on the GameObject.
[RequireComponent(typeof(Collider2D))]We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class KillOnContactWith : MonoBehaviour
{This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]If the GameObject we come in contact with is one of the layers defined in this mask, it will be killed. Set in the Inspector.
LayerMask layersToKill;Start is a Unity event which is called once for a component, the first time it is enabled.
protected is optional here. Used for consistency.
This method does nothing, it's here so that enable/disable options appear in the Inspector.
protected void Start() { }OnCollisionEnter2D is a Unity event which is called any time another collider comes in contact with a collider on this GameObject.
protected is optional here. Used for consistency.
protected void OnCollisionEnter2D(
Collision2D collision)
{Call a helper method below to consider killing the GameObject we just collided with.
TryToKill(collision.gameObject);
}OnTriggerEnter2D is a Unity event which is called anytime another collider overlaps a collider on this GameObject and either, or both, collider is a 'Trigger'.
protected is optional here. Used for consistency.
protected void OnTriggerEnter2D(
Collider2D collision)
{Call a helper method below to consider killing the GameObject we just overlapped.
TryToKill(collision.gameObject);
}This is a helper method to consider killing a GameObject we just came in contact with.
void TryToKill(
GameObject gameObjectWeHit)
{ If this component has been disabled, do not process the collision.
if(enabled == false)
{
return;
}This uses the LayerMaskExtensions to check if this GameObject's layer is part of the mask of layers to kill.
GameObject gameObjectWeHit = collision.gameObject;
if(layersToKill.Includes(gameObjectWeHit.layer))
{Here we initiate death effects, if any, and then destroy the GameObject.
DeathEffectManager.PlayDeathEffectsThenDestroy(
gameObjectWeHit);
}
}
}Why check the layer instead of using the Collision Matrix?
Layers are defined per GameObject. The GameObject we will be adding this script to, already have a layer defined to support other use cases. This means that the KillOnContactWith component will get event calls for collisions with other objects such as the platforms.
In order to do this with a Collision Matrix, a child GameObject with its own Layer could be added to hold this component.
What is this '& 1 <<' black magic?
Bitwise operations... which are beyond the scope of this tutorial. More specifically, this is 'bitwise and' and 'bit shifting' if you would like to read more about this. Here is a Stackoverflow post on the topic.
What is a C# extension method?
TODO just shorthand for calling a public static method.
Extension methods are a way of adding additional methods to a class or struct you don't own. In this example, Unity has a struct 'LayerMask'. That struct does not offer an easy way to determine if a layer is part of that LayerMask. Using extensions, we are able to create an 'Includes' method that then can be used as if Unity had written it for us.
This allows us to focus on intent and forget the gory details. For example this statement:
if((layersToKill.value & 1 << gameObjectWeJustHit.layer) > 0)
...Can now be written like so, which should be easier for people to follow.
if(layersToKill.Includes(gameObjectWeJustHit.layer))
...Why is there an empty Start method and why check if enabled?
We will need the ability to disable this component later in the tutorial.
A disabled component will not get called for events such as Update. However it does still receive some calls while disabled, including OnTriggerEnter. This is why we check if enabled vs depending on Unity to do that for us.
Unity only allows you to use the enable / disable feature if it detects that there is a method in the script which would be impacted. We added an empty Start method to get the enable / disable feature since Unity does not enable enable by checking 'if(enabled)' in code.
YouTube | Source before | Source after
Create an explosion particle system with a sound effect.
How
Create Particle System:
- Create an empty GameObject:
- Name it "Explosion".
- Add a ParticleSystem:
- Set 'Renderer' Material: Default-Particle
- Set 'Renderer' Max Particle Size: 1000
- Back at the top of the Particle System component, set:
- Looping: Off
- Start Lifetime: 0.5
- Start Size: 30
- Update the Transform scale to about (.05, .05, .05)
- Enable Color over Lifetime, and then:
- Click the color to open the Gradient editor.
- Click above the color bar, about 1/5th in from the left - creating a keyframe.
- Click on the top left keyframe, change Alpha to 0. Do the same for the top right.
- Click on the bottom left keyframe, change the color to Hex 'FFF3DD'.
- Under 'Emission':
- Rate over Time: 0
- Click the '+' under 'Bursts' to create an entry, change
- Min: 3
- Max: 3
Add sound effect:
- Add AudioSource to the GameObject:
- Select an AudioClip. We are using lowDown.
- Adjust the volume if needed.
- Select an AudioClip. We are using lowDown.
Test:
- Select the Explosion and in the Scene click 'Stop' and then 'Simulate' to preview the particle system.
- When you hit play, the Explosion should display.
What's a particle / particle system?
A particle is a small 2D image managed by a particle system. It's optimized to display a large number of similar particles at the same time, possible with different colors, sizes, etc.
A Particle System component animates a number of particles to create effects such as fluid, smoke, and fire. Read more about Particle Systems from Unity.
Briefly about the changes recommended above:
- Set the material: the default may be broken due to a Unity Bug, we are simply selecting what should have been the default.
- Particle Size: this limits the size of the effect you may see on the screen. We crank it up so that while previewing the in Scene window we can zoom in.
- Looping: just one explosion.
- Start Lifetime: Defines how long until each particle should be destroyed.
- Start Size: How large each particle is.
- Scaling Mode: Enables us to scale the size of the explosion using Transform scale.
- Color over Lifetime: Changes the coloring to add to the effect.
- Emission: Defines when and how many particles to create. We are using exactly 3 particles for each explosion.
Could you RNG select the audio clip to play?
Anything is possible. Here's a little code sample that may help you get started.
On a related note, you could also randomize the pitch to get some variation between each clip played. e.g., this could be a nice addition to a rapidly firing gun.
[SerializeField]
AudioClip clip1;
[SerializeField]
AudioClip clip2;
protected void OnEnable()
{
AudioSource audioSource = GetComponent<AudioSource>();
switch(UnityEngine.Random.Range(0, 2))
{
case 0:
audioSource.clip = clip1;
break;
case 1:
audioSource.clip = clip2;
break;
}
audioSource.Play();
}YouTube | Source before | Source after
The explosion should destroy after a few the effect completes.
How
Create SuicideIn:
- Create script Code/Death/SuicideIn:
using UnityEngine;
using System.Collections;
public class SuicideIn : MonoBehaviour
{
[SerializeField]
float timeTillDeath = 5;
protected void Start()
{
StartCoroutine(CountdownToDeath());
}
IEnumerator CountdownToDeath()
{
yield return new WaitForSeconds(timeTillDeath);
DeathEffectManager.PlayDeathEffectsThenDestroy(gameObject);
}
}
Configure Explosion:
- Add SuicideIn to the Explosion.
- Drag the Explosion GameObject into Assets/Prefabs.
- Delete the Explosion GameObject.
Test:
- Hit play and then drag the Explosion prefab into the scene. Shortly after the explosion completes, its GameObject should disappear from the Hierarchy.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- System.Collections.IEnumerator
- UnityEngine.Debug
- UnityEngine.MonoBehaviour
- UnityEngine.SerializeFieldAttribute
- UnityEngine.WaitForSeconds
using UnityEngine;
using System.Collections;We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class SuicideIn : MonoBehaviour
{This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This defines how long before this GameObject should be killed. You can change the default value in the Inspector.
float timeTillDeath = 5;Start is a Unity event which is called once for a component, the first time it is enabled.
protected is optional here. Used for consistency.
protected void Start()
{This starts the coroutine below.
StartCoroutine(CountdownToDeath());
}This is the coroutine which will start the death effects after a period of time, and then ultimately destroy the GameObject.
IEnumerator CountdownToDeath()
{Here we pause this coroutine until we are ready to trigger death effects.
yield return new WaitForSeconds(timeTillDeath);This starts the death effects on this GameObject, if there are any, and then destroy it.
DeathEffectManager.PlayDeathEffectsThenDestroy(gameObject);
}
}Why not Destroy instead?
You could.
When PlayDeathEffectsThenDestroy is called on a GameObject that does have a DeathEffectManager or any DeathEffect components (like the Explosion), the GameObject will be destroyed instantly.
By creating the component using DeathEffects, we have something that can be used for other use cases. This script will come up again later in the tutorial.
YouTube | Source before | Source after
Add a script which spawns the explosion prefab when the character dies.
How
Create DeathEffectSpawn:
- Create script Code/Death/DeathEffectSpawn:
using UnityEngine;
[RequireComponent(typeof(Collider2D))]
public class DeathEffectSpawn : DeathEffect
{
[SerializeField]
GameObject gameObjectToSpawnOnDeath;
public override float PlayDeathEffects()
{
Collider2D collider = GetComponent<Collider2D>();
Instantiate(
gameObjectToSpawnOnDeath,
collider.bounds.center,
Quaternion.identity);
return 0;
}
}
Configure Character:
- Add DeathEffectSpawn to the character (this will automatically add a DeathEffectManager as well).
- Game Object To Spawn: Explosion
Test:
- Walk into an enemy, when the Character dies an explosion should spawn.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.RequireComponentAttribute
- UnityEngine.Collider2D
- UnityEngine.Debug
- UnityEngine.GameObject
- UnityEngine.SerializeFieldAttribute
- UnityEngine.Quaternion
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a collider component on the GameObject.
[RequireComponent(typeof(Collider2D))]We inherit from DeathEffect which is a MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class DeathEffectSpawn : DeathEffect
{This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This is a reference to the prefab which will be instantiated when this GameObject dies, set in the Inspector.
GameObject gameObjectToSpawnOnDeath;This overrides DeathEffect's method which will be called when this GameObject dies.
public override float PlayDeathEffects()
{Here we get a reference to the collider on this GameObject, of any type such as Box and Circle.
Collider2D collider = GetComponent<Collider2D>();This creates a copy of the prefab, at the center of this GameObject's collider with a the default rotation.
Instantiate(
gameObjectToSpawnOnDeath,
collider.bounds.center,
Quaternion.identity);Return 0, indicating that this GameObject may be destroyed now.
return 0;
}
}What's bounds represent?
The Unity Bounds struct represents the axis aligned bounding box for the collider. This means if you were to contain the collider in a cube which cannot be rotated - what is the position and size of the smallest possible surrounding cube.
For 2D, the Bounds struct still has a z but it will be 0 and everything else will work as expected.
Unity has a number of APIs available for bounds. Here we are using .center, which represents the center of the collider which may differ from the transform position - particularly for the character since the pivot point is Bottom.
Why not spawn the explosion at transform.position instead of bounds.center?
The character sprite was configured with Pivot 'Bottom'. The transform.position refers to the location of this pivot point. If we were to target transform.position instead, the explosion would center around the character's feet.
This component could be reused on other GameObjects which may have a different pivot point. It will work correctly so long as the object has a collider.
We use the collider's bounds to determine where to spawn the explosion. The bounds struct has a number of convenient methods for things like determining the center point of an object.
Why bother destroying, the explosion is not visible after a few seconds?
Similar to how we destroyed balls which rolled off the bottom of the screen in chapter 1, we need to ensure the explosion GameObjects are destroyed at some point.
The explosion effect on-screen only lasts for a few seconds, but Unity does not realize this on its own. Destroying the GameObject prevents Unity from wasting resources on the old GameObjects which are never going to be visible again.
In other words, this script ensures that our explosions do not result in a memory leak.
YouTube | Source before | Source after
Add a scaling effect for the character dying, in addition to the explosion.
How
Create DeathEffectThrob:
- Create script Code/Death/DeathEffectThrob:
using UnityEngine;
using System.Collections;
public class DeathEffectThrob : DeathEffect
{
[SerializeField]
float lengthOfEffectInSeconds = 1;
[SerializeField]
int numberOfPulses = 5;
Vector3 originalScale;
protected void Awake()
{
originalScale = transform.localScale;
}
public override float PlayDeathEffects()
{
StartCoroutine(ThrobToDeath());
return lengthOfEffectInSeconds;
}
IEnumerator ThrobToDeath()
{
float timePerPulse
= lengthOfEffectInSeconds / numberOfPulses;
float timeRun = 0;
while(timeRun < lengthOfEffectInSeconds)
{
float percentComplete
= timeRun / lengthOfEffectInSeconds;
float sinValue
= Mathf.Sin(Mathf.PI * timeRun / timePerPulse);
float pulse = .5f + Mathf.Abs(sinValue);
float scale = (1 - percentComplete) * pulse;
gameObject.transform.localScale
= originalScale * scale;
yield return null;
timeRun += Time.deltaTime;
}
}
}
Configure Character:
- Add DeathEffectThrob to the Character.
Test:
- When the Character dies, it should scale in and out before disappearing.
- Note that the explosion should also still play on death.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- System.Collections.IEnumerator
- UnityEngine.Debug
- UnityEngine.Mathf
- UnityEngine.SerializeFieldAttribute
- UnityEngine.Vector3
using UnityEngine;
using System.Collections;We inherit from DeathEffect which is a MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class DeathEffectThrob : DeathEffect
{This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This defines the amount of time to scale this object up and down before it is gone. You can change the default in the Inspector.
float lengthOfEffectInSeconds = 1;This is the number of times to scale up and down before the GameObject is gone.
[SerializeField]
int numberOfPulses = 5;originalScale holds the transform scale as defined in the prefab or scene.
Vector3 originalScale;Awake is a Unity event which is called once for a component when it's first added to a Scene.
protected is optional here. Used for consistency.
protected void Awake()
{Here we store the original scale, so that when we scale up and down we can do that relative to its original size.
originalScale = transform.localScale;
}This overrides DeathEffect's method which will be called when this GameObject dies.
public override float PlayDeathEffects()
{This starts the coroutine below.
StartCoroutine(ThrobToDeath());Returns the length of the throb effect so that the DeathEffectManager knows how long till the GameObject may be destroyed.
return lengthOfEffectInSeconds;
}This is the coroutine which plays out the throb effect.
IEnumerator ThrobToDeath()
{Here we calculate the time per pulse based on the values entered in the Inspector.
float timePerPulse
= lengthOfEffectInSeconds / numberOfPulses;This will track the run time, and loop until we have run for the amount of time requested.
float timeRun = 0;
while(timeRun < lengthOfEffectInSeconds)
{Here we calculate the percent complete from 0 to 1.
float percentComplete
= timeRun / lengthOfEffectInSeconds;Here we calculate a sin value based on the time run so far. By multiplying by PI and dividing by timePerPulse, we are requesting a value that goes from 0 to 1 every timePerPulse.
float sinValue
= Mathf.Sin(Mathf.PI * timeRun / timePerPulse);The sinValue will oscillate between -1 and 1. This adds .5 to the absolute value, giving a curve that oscillates between .5 and 1.5
float pulse = .5f + Mathf.Abs(sinValue);pulse is a value from .5 to 1.5 which represents the amount of throb to apply.
1 - percentComplete is a value which goes from 1 to 0 as the effect progresses.
By multiplying these, we calculate a scale here that goes up and down while overall getting smaller until it reaches size 0.
float scale = (1 - percentComplete) * pulse;Taking the scale calculated above, which will be a value from 0 to 1.5, and multiply by the original scale to set the scale for this frame.
gameObject.transform.localScale
= originalScale * scale; This will pause this coroutine until the next frame.
yield return null;This tracks how long the coroutine has been running by adding the length of each frame that passes.
timeRun += Time.deltaTime;
}
}
}What does yield return null do?
Enumerators are methods which can 'yield return' and then later be resumed from where they left off. Coroutines in Unity are enumerators.
With Coroutines, "yield return null" is shorthand for wait for one frame.
Each of these accomplishes the same, the coroutine resumes on the next Update:
yield return null; // Preferred
yield return new WaitForSeconds(0); // Same, but longer
yield return 0; // Less efficientWhy not use an animation instead?
You could. There are numerous ways to create animations and effects - in this tutorial we cover a few different approaches just for the experience.
We will be introducing Unity 'animations' later in this tutorial.
Why use Mathf.Sin?
Sin is used frequently in game dev because of the nice curve it creates:
We will be taking the absolute value, so the curve from 0 to Pi repeats over and over. The result oscillates smoothly between 0 and 1.
We add .5 to the result, giving us .5 -> 1.5. That's used as a multiple when scaling, creating the throb effect.
More about how you can use Sin and Cos to create nice curves from OSU.edu.
How might we disable movement when the character is dying?
After the character dies and the throb animation begins, you can still walk around. This could be addressed, but we are leaving it like this for the tutorial for simplicity and because it's kind of funny looking.
To stop movement, you could disable the PlayerController or the Rigidbody. You might also want to stop the current animation as well.
Why not make the originalScale readonly?
We basically want a readonly field here - the value is known when the game starts and will never change.
The reason we cannot use readonly here is that Unity MonoBehaviours do not support constructors, and C# does not allow you to set a readonly variable in a method such as Awake.
Testing / debugging tips
- Try different particle system settings for the explosion.
- Cut a test build and try it outside of the Unity editor environment.
- You can disable the Spawner component at any time to help debugging when you are focused on another area.
How do you sort components on a GameObject?
Consider sorting components on your GameObject, as it's starting to look a little cluttered.
The order does not impact anything. So why bother? Just tidiness really. As the number of components grows it may be nice to have them presented in an order you find more intuitive.
- To sort, select the GameObject and in the Inspector
- Start by collapsing everything.
- Click and drag components in the Inspector to change the order.
On a related note, order does matter when for some scripts in terms of which component executes before another. If you need to manage the order scripts run in, you can use Project Settings -> Script execution order.
Unity's Script Execution Order is how you can declare the order scripts should be called. Normally you would not add many scripts to this, reserve it for only when the order will have a real impact.
Sometimes when it seems script execution order is required, you could instead use different events to get the desired behaviour. For example, every component will execute its Awake before each of them start to execute Start - which may allow you to initialize dependent data in one component for another to use in Start.
Questions, issues, or suggestions? Please use the YouTube comments for the best fit section.
Support on Patreon, with Paypal, or by subscribing on Twitch (free with Amazon Prime).
License. Created live at twitch.tv/HardlyDifficult August 2017.
