Previous tutorials have discussed the automatic damage dealing logic provided by collision relationships. Full games often need to modify how much damage is dealt depending on a variety of factors such as element weaknesses. Similarly, games may need to respond to damage dealt such as by providing a visual indication of an entity taking damage.
This tutorial shows how to assign events on IDamageable entities to modify damage dealt and to visually indicate that damage has been dealt.
Entities which implement the IDamageable interface are required to have an event named ReactToDamageDealt. This is automatically generated by the FlatRedBall Editor, so the event is available in custom code. This can be used to add custom code to handle an entity receiving damage.
Keep in mind this event occurs after damage checks have been made. It will only be raised if the IDamageable entity has a different team index than the argument IDamageArea, and before the IDamageable is killed (if enough damage is dealt to kill the IDamageable). For example, we can make the enemy flash when it takes damage. First, we'll change the enemy's AxisAlignedRectangleInstance color to red in the FlatRedBall Editor.
Next, we can handle the event in code. In this case we'll use an async
call to set the color to white temporarily, then set it back to red. Keep in mind that for a full project you may want additional logic to allow the enemy to receive damage multiple times without flashing the rectangle back. For the sake of simplicity, we'll ignore this and write simpler code which automatically sets the rectangle back to its original color after a short delay.
Now the Enemy flashes white when taking damage. Note that to test this you may want to disable the enemy shooting logic added in the previous tutorial. Also, you may want to adjust the health of the enemy to take more hits.
The purpose of ReactToDamageDealt is to react to damage visually, or to perform other game logic outside of typical IDamageable logic. Therefore, ReactToDamageDealt should not:
Kill the damage receiver unless there are special circumstances
Deal more damage to the damage receiver
Destroy the IDamageArea (damage dealer) unless there are special circumstances
The ModifyDamageDealt event can be used to perform logic that can change how much damage an entity receives. For example, an enemy may be weak to a particular type of bullet such as an elemental bullet. For this example, we will add an IsFireBullet property to the Bullet, and an IsIceEnemy property to the Enemy. Note that a real game may handle these variants through entity inheritance, but we will add properties through code to keep the tutorial shorter. Add the following code to Bullet.cs:
Next, add the following code to Enemy.cs:
Now we'll modify Player.cs to create fire bullets when pressing left Alt key:
Next, we'll modify the code that creates Enemies in GameScreen.cs to create ice enemies on a right-click:
Now we can create fire enemies and ice bullets by right clicking the mouse and pressing the left Alt key, respectively.
Now that we have two different types of enemies and two different types of bullets, we can write logic in the Enemy's ModifyDamageDealt event in response to the elements:
Now ice enemies die after 5 hits instead of 10 since each bullet will do an effective 20 points of damage.
This tutorial discusses two events:
IDamageable.ReactToDamageReceived
IDamageable.ModifyDamageReceived
The IDamageable and IDamageArea entities provide additional events which can be used to handle damage modification, reaction to taking damage, and death. This is the full list of events provided by each interface:
IDamageable.ModifyDamageReceived - Allows an IDamageable to modify how much damage it takes
IDamageable.ReactToDamageReceived - Allows an IDamageable to respond to taking damage
IDamageable.Died - Allows an IDamageable to respond to dying, such as by playing a sound effect or creating particles
IDamageArea.ModifyDamageDealt - Allows an IDamageArea to modify how much damage it deals
IDamageArea.ReactToDamageDealt - Allows an IDamageArea to respond to dealing damage
IDamageArea.KilledDamageable - Allows an IDamageArea to respond to killing an IDamageable
IDamageArea.RemovedByCollision - Allows an IDamageArea to respond to being removed by collision
This tutorial has used both of the terms *delegate* and *event*, primarily using event to indicate that these are methods which are invoked in response to something happening in the game such as the enemy taking damage. Before moving on to a new topic we should cover some of the technical details of how these delegates work.
Technically, these are delegates and not events. In other words, the IDamageable and IDamageArea interfaces do not define these using the event
keyword. Therefore, these can be invoked externally, allowing the DamageableExtensionMethods.TakeDamage
methods to call these methods as necessary.
If you are familiar with event syntax in C#, then you are probably used to the +=
operator when assigning a handler. This approach works for the delegates defined on the IDamageable and IDamageArea interfaces, but we should take a moment to mention a subtlety related to the ModifyDamageDealt
and ModifyDamageReceived
delegates. These delegates use the Func
type, which means they return the modified damage value. If you use the +=
operator for these two delegates, you may end up dealing or receiving an unexpected amount of damage. The reason for this is because only the last handler added to these two delegates decides how much damage to deal. To understand why this might happen, let's look at an example.
Consider a game with an Enemy entity which has multiple variants (using inheritance). You may have an IsInvulnerable
property on the Enemy, resulting in the following code for handling damage:
You may also decide to have additional logic for an enemy variant which can hide in its shell. If the enemy is in its shell, it would receive half damage. The implementation may look like this:
In this situation, the base CustomInitialize
is called first, then the derived CustomInitialize
. This means that whenever the enemy receives damage, the base HandleDamageReceived
is called first, then the derived HandleDamageReceived
. Unfortunately, when HandleDamageReceived
is called on the derived, the initialDamage
is not modified by the first call. In other words, the initialDamage
will never be affected by the check for IsInvulnerable
because each method returns its value independent of the other. In effect this results in the TurtleEnemy never respecting its IsInvulnerable variable.
Therefore, it's best to never += on any of the Func
delegates - ModifyDamageDealt
and ModifyDamageReceived.
Instead only the =
operator should be used for clarity. In this case, we can resolve the problem by creating a virtual method for handling damage which derived classes can override, as shown in the following code:
Now the derived variant can override the method as shown in the following code:
This tutorial discusses how to handle events to modify damage and display visual feedback to the player when damage is dealt. The next tutorial covers the removal of IDamageAreas and how to implement damage over time.