Tactics RPG Status Effects

During our last lesson I suggested possible implementations for a few status effects, such as Haste, Slow and Stop. In this lesson we will actually add them. We will also learn how to manage the way multiple conditions might be keeping a status active. For some variation we will also add a Poison status effect and see how it can be tied to an item as an equip feature – a cursed sword.

Refactoring

It turns out I was only mostly right with the way I suggested we implement our status effects in the last lesson. The current implementation of our ValueModifier classes only modify the overall value, not the amount of change of a value.

Imagine the following scenario: we have a unit with a stat CTR value of 500, and on an update “tick” will increment the value by 100. If we “catch” the notification and attach a MultValueModifier with a multiplier of 2 in an attempt to implement haste, then the actual result is “(500 + 100) * 2 = 1200”. What I wanted for haste is “500 + (100 * 2) = 700”.

In order to allow us a way to modify the amount of change of a value we will have to refactor our code. Open the VauleModifier script and change the signature of our Modify method to the following:

public abstract float Modify (float fromValue, float toValue);

Each of the subclasses will also need to use the new signature. As you update the signatures and implementation bodies, use the “toValue” parameter to replace the original “value” parameter. If you try to “Build” you will see errors until you fix each subclass instance. Refer to the code in my repository if you get stuck.

Now that we have modified the ValueModifiers, we will also need to modify the way that a ValueChangeException determines the modified value. Use the following:

public float GetModifiedValue ()
{
	if (modifiers == null)
		return toValue;

	float value = toValue;
	modifiers.Sort(Compare);
	for (int i = 0; i < modifiers.Count; ++i)
		value = modifiers[i].Modify(fromValue, value);
	
	return value;
}

By knowing the value we were changing “from” and the value we are changing “to”, it is a simple matter to determine the delta. Because of this I can now add modifiers which modify the result based on that delta. Add a new script named MultDeltaModifier to the Scripts/Exceptions/Modifiers folder.

using UnityEngine;
using System.Collections;

public class MultDeltaModifier : ValueModifier 
{
	public readonly float toMultiply;
	
	public MultDeltaModifier (int sortOrder, float toMultiply) : base (sortOrder)
	{
		this.toMultiply = toMultiply;
	}
	
	public override float Modify (float fromValue, float toValue)
	{
		float delta = toValue - fromValue;
		return fromValue + delta * toMultiply;
	}
}

Status Conditions

There might be multiple reasons why a status is active on a Unit. Note that this is different than the cause of the status. For example, you could apply Blind by casting a magic spell or by hitting something with a special item – these are the cause of the status. When one of these causes occur, we add the status effect and add a “condition” for how long the effect remains active. In these cases we might say that the condition is some sort of timer or “duration” and once the time requirement is met, the condition for the status is removed, which also removes the status effect itself assuming that no other conditions were still active.

Normally I create an “abstract” base class, but in this case I decided to leave the base class as “usable” – it will be a “manual” condition which doesn’t take care of removing itself, and instead something else will decide when to add and remove it. Later, this base class will be used as part of an Equip Feature, where the condition is that a status will be applied for as long as the item is equipped.

Add a new script named StatusCondition to the following folder path Scripts/View Model Component/Status/Conditions.

using UnityEngine;
using System.Collections;

public class StatusCondition : MonoBehaviour
{
	public virtual void Remove ()
	{
		Status s = GetComponentInParent<Status>();
		if (s)
			s.Remove(this);
	}
}

The only thing this script does is to know how to remove itself. That’s not much, but its really the sole purpose of this class. It is there as a sort of “lock” to keep a status effect applied, but it doesn’t care what the status effect is, it only needs to know about itself and how to remove itself. The Status component manages the relationship between these “locks” and the effects, but we will get to that later.

Subclasses of the StatusCondition class will be able to remove themselves through some sort of more specific event. For example, time – add another script named DurationStatusCondition to the same folder.

using UnityEngine;
using System.Collections;

public class DurationStatusCondition : StatusCondition 
{
	public int duration = 10;

	void OnEnable ()
	{
		this.AddObserver(OnNewTurn, TurnOrderController.RoundBeganNotification);
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnNewTurn, TurnOrderController.RoundBeganNotification);
	}

	void OnNewTurn (object sender, object args)
	{
		duration--;
		if (duration <= 0)
			Remove();
	}
}

This subclass listens for notifications that a new round has begun, and with each new round reduces a duration counter by one. Note that you can specify how many rounds a particular effect will last because the duration field is public.

Status Effects

Create a new script named StatusEffect in the Scripts/View Model Component/Status/Effects folder.

using UnityEngine;
using System.Collections;

public abstract class StatusEffect : MonoBehaviour 
{

}

Yep – its a completely empty script. Why would I ever do such a thing? Even though this base class has no functionality whatsoever, I wanted to make sure that all status effects share a common base class. This way, if as I am implementing them I do see some common functionality, it will be easy to move it to the base class and allow it to be reused. In addition, it makes the intent of my code more clear in other classes. For example, the Status component which I am about to create will work based on pairs of StatusEffect and StatusCondition components. If I didn’t specify a base class for the StatusEffect, then there would be nothing stopping a particularly “clever” user from adding “any” MonoBehaviour he wanted which may lead to unexpected consequences. Because I did specify a base class, the intentions of the architecture are much more obvious.

Haste

Add another script named HasteStatusEffect to the same folder as before.

using UnityEngine;
using System.Collections;

public class HasteStatusEffect : StatusEffect 
{
	Stats myStats;

	void OnEnable ()
	{
		myStats = GetComponentInParent<Stats>();
		if (myStats)
			this.AddObserver( OnCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), myStats );
	}

	void OnDisable ()
	{
		this.RemoveObserver( OnCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), myStats );
	}

	void OnCounterWillChange (object sender, object args)
	{
		ValueChangeException exc = args as ValueChangeException;
		MultDeltaModifier m = new MultDeltaModifier(0, 2);
		exc.AddModifier(m);
	}
}

In this case we register for the WillChangeNotification of the CTR stat. In the notification handler we add our brand new MultDeltaModifier to the ValueChangeException with a multiplier of “2”. This will cause the amount by which the stat changes to be doubled.

Slow

Add another script named SlowStatusEffect to the same folder:

using UnityEngine;
using System.Collections;

public class SlowStatusEffect : StatusEffect 
{
	Stats myStats;

	void OnEnable ()
	{
		myStats = GetComponentInParent<Stats>();
		if (myStats)
			this.AddObserver( OnCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), myStats );
	}
	
	void OnDisable ()
	{
		this.RemoveObserver( OnCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), myStats );
	}
	
	void OnCounterWillChange (object sender, object args)
	{
		ValueChangeException exc = args as ValueChangeException;
		MultDeltaModifier m = new MultDeltaModifier(0, 0.5f);
		exc.AddModifier(m);
	}
}

The Slow status effect is nearly identical to the Haste status effect. The only difference (besides the name of the class) is the multiplier value. At this point you should probably be thinking “Oh no, I’ve just repeated myself!” and if you are then you should give yourself a gold star.

There are a few ways we could have reduced the amount of code here. One way is that the Haste and Slow status effects could share a common base class. Another is that we could simply use a single script with a public field for the multiplier value to use. Perhaps this script would be called ModifyCounterSpeedStatusEffect. Unfortunately it isn’t quite as intuitive that this single component would be used for both the implementation of Haste and Slow – I could see myself forgetting how or where it was implemented. Furthermore, as this game is fleshed out more in the future I still might prefer they be separate classes due to new implementation details such as the different ways that visual aids (the things on-screen which indicate that a unit is under the haste or slow status effect) might appear. I’ll leave the final decision on this sort of architecture up to you.

Stop

Add another script named StopStatusEffect to the same folder:

using UnityEngine;
using System.Collections;

public class StopStatusEffect : StatusEffect 
{
	Stats myStats;

	void OnEnable ()
	{
		myStats = GetComponentInParent<Stats>();
		if (myStats)
			this.AddObserver( OnCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), myStats );
	}
	
	void OnDisable ()
	{
		this.RemoveObserver( OnCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), myStats );
	}
	
	void OnCounterWillChange (object sender, object args)
	{
		ValueChangeException exc = args as ValueChangeException;
		exc.FlipToggle();
	}
}

This script also looks very similar to both Haste and Slow. In fact, it would have been possible to use the same script for all three if I had used a public field for the multiplier value. In this case, I would just use a multiplier of 0. However, because I mentioned in the last post that you could simply flip the toggle, I wanted to show that implementation. Also, flipping a toggle is more “strict” than multiplying by zero. For example, if I had multiple value modifiers in play, one might multiply by zero, and another could add some other amount so that the final result was still non-zero. When the toggle is flipped, it doesn’t matter what the final value would have been, a change simply isn’t allowed.

Poison

Add our final status effect named PoisonStatusEffect to the same folder.

using UnityEngine;
using System.Collections;

public class PoisonStatusEffect : StatusEffect 
{
	Unit owner;

	void OnEnable ()
	{
		owner = GetComponentInParent<Unit>();
		if (owner)
			this.AddObserver(OnNewTurn, TurnOrderController.TurnBeganNotification, owner);
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnNewTurn, TurnOrderController.TurnBeganNotification, owner);
	}

	void OnNewTurn (object sender, object args)
	{
		Stats s = GetComponentInParent<Stats>();
		int currentHP = s[StatTypes.HP];
		int maxHP = s[StatTypes.MHP];
		int reduce = Mathf.Min(currentHP, Mathf.FloorToInt(maxHP * 0.1f));
		s.SetValue(StatTypes.HP, (currentHP - reduce), false);
	}
}

This status effect operates on a different notification, one that we haven’t actually added yet (but will in a moment). I had originally allowed it to work on the beginning of each new round, but then I realized there were several rounds before our units build up enough CTR to actually take a turn. I decided that it felt better to see the effect of the poison just before the unit takes a turn.

When the notification handler executes it gets a reference to the Stats component and reduces HP by one-tenth of the MHP or the current HP of the unit, whichever is less. Note that I could have relied on a Clamp Value Modifier which existed elsewhere (like a Health component) to ensure that HP never goes below zero (or above MHP). However, in this case I used the SetValue method with the AllowExceptions parameter set to false. I decided that the effect of Poison would be unalterable, but this also may not be a design you agree with. Feel free to modify things as you desire.

Don’t forget that we will need to add the new notification to the TurnOrderController script. It would look like the following:

public const string TurnBeganNotification = "TurnOrderController.TurnBeganNotification";

And it would be posted immediately before the yield statement in the Round method:

...
if (CanTakeTurn(units[i]))
{
	bc.turn.Change(units[i]);
	units[i].PostNotification(TurnBeganNotification); // ADDED
	yield return units[i];
...

Extensions

In Unity, when you Destroy a GameObject or Component, the thing which you destroyed is still there until the next frame. Imagine for example, that I have added a component, destroy it, and then do a GetComponent from somewhere else. The GetComponent call can find the component which is being destroyed and this can lead to some unfortunate problems. In addition, Unity doesn’t provide any sort of field which can be referenced to know that the object is scheduled for destruction.

In order to fix the problem mentioned above, and also for the sake of clear debugging in the hierarchy, I will use a system where I add components to children objects. When I want to destroy an object, I can first unparent the transform so that calls to GetComponentInChildren will not succeed in finding the object which is going to be destroyed.

A good polish step in the future might be to use the GameObjectPoolController and reuse GameObjects rather than constantly creating and destroying them. I’m not that worried at the moment because the frequency of the creation and destruction of these objects is so sporadic.

Add a new script named GameObjectExtensions to the Scripts/Extensions folder. This script will make it easy to create a new child object which is parented to the indicated game object, and attach the component of type you specify with generics.

using UnityEngine;
using System.Collections;

public static class GameObjectExtensions
{
	public static T AddChildComponent<T> (this GameObject obj) where T : MonoBehaviour
	{
		GameObject child = new GameObject( typeof(T).Name );
		child.transform.SetParent(obj.transform);
		return child.AddComponent<T>();
	}
}

Status

Add a new script named Status to the Scripts/View Model Component/Status folder. This component will be responsible for determining how long a status effect should remain applied to a unit. It handles this by checking for the existance of status conditions. As long as there is a condition tied to an effect, the effect remains applied. Note that the script itself doesn’t know or need to know anything specific about the status effects or status conditions themselves.

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

public class Status : MonoBehaviour
{
	public const string AddedNotification = "Status.AddedNotification";
	public const string RemovedNotification = "Status.RemovedNotification";

	public U Add<T, U> () where T : StatusEffect where U : StatusCondition
	{
		T effect = GetComponentInChildren<T>();

		if (effect == null)
		{
			effect = gameObject.AddChildComponent<T>();
			this.PostNotification(AddedNotification, effect);
		}

		return effect.gameObject.AddChildComponent<U>();
	}

	public void Remove (StatusCondition target)
	{
		StatusEffect effect = target.GetComponentInParent<StatusEffect>();

		target.transform.SetParent(null);
		Destroy(target.gameObject);

		StatusCondition condition = effect.GetComponentInChildren<StatusCondition>();
		if (condition == null)
		{
			effect.transform.SetParent(null);
			Destroy(effect.gameObject);
			this.PostNotification(RemovedNotification, effect);
		}
	}
}

Add Status Feature

So far I have shown examples of “what” a status effect can do, and “when” it should be active. I haven’t shown examples of “how” to actually apply something. I did suggest a common way would be to use a magic spell or attack to deliver the status effect along with a duration condition, but now I want to do something a little different. We will use our Feature component so that we can make the addition of a status effect one of the features of equipping an item.

Most of the time you will choose something nice, like special shoes which provide haste or something like that. Sometimes it can be interesting to mix things in an unexpected way, such as a sword which is exceedingly powerful, but which is also cursed so equipping it causes you to be poisoned.

Add a new script named AddStatusFeature to the Scripts/View Model Component/Features folder.

using UnityEngine;
using System.Collections;

public abstract class AddStatusFeature<T> : Feature where T : StatusEffect
{
	#region Fields
	StatusCondition statusCondition;
	#endregion

	#region Protected
	protected override void OnApply ()
	{
		Status status = GetComponentInParent<Status>();
		statusCondition = status.Add<T, StatusCondition>();
	}
	
	protected override void OnRemove ()
	{
		if (statusCondition != null)
			statusCondition.Remove();
	}
	#endregion
}

Next you can add a subclass called AddPoisonStatusFeature:

using UnityEngine;
using System.Collections;

public class AddPoisonStatusFeature : AddStatusFeature<PoisonStatusEffect> 
{

}

One thing to consider with this architecture, is the potential for an “explosion of classes” by which I mean that the more kinds of status effects we add, the more kinds of specific “Add Status Feature” subclasses we might also add. If the method of deliveries also included something like an “OnHitAddStatus” class then we may likewise have subclasses of that for each type of subclassed status effect. The classes themselves are empty but it is unfortunate to need so many.

An alternative architecture pattern could be to simply provide a public System.Type field which determines what type of feature to add. A single class would be able to be used no matter how many types of status effects we wanted to add. Unfortunately, you lose some of the readability and constraints that generics provided. Also, there isn’t a good method for attaching a “Type” through the inspector for your prefabs. You could use a string and get a type from the string, but that is also vulnerable to abuse and lacks compile time checking. Furthermore, you can’t directly use the “Type” with generics, but would need to use Reflection and that also feels a little wrong to me. Just my opinions – feel free to pick whatever feels best to you.

Demo

Let’s continue to use our Battle Scene, but this time as each unit moves, we will do “something” regarding status effects. One of our units will equip our cursed sword (and therefore get poisoned), and each of the units will get one of the CTR based status effects as well. Experiment with moving the pieces on the board, but for now don’t actually attack. Simply observe that one of the units will be faster than the others (Haste), one will get turns but very slowly in comparison (Slow), and another wont be getting turns at all (Stop). Eventually the status effects will all wear off, and each of the unit’s speeds will return to normal. Also note that even though no attacking took place, the unit that had equipped the poison sword will have lost hit points.

Before we begin the demo, add a Status and Equipment component to the Hero prefab in the project pane.

Next, add a tempory script named Demo and attach it to the Battle Controller GameObject in the scene.

using UnityEngine;
using System.Collections;

public class Demo : MonoBehaviour 
{
	Unit cursedUnit;
	Equippable cursedItem;
	int step;

	void OnEnable ()
	{
		this.AddObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification);
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification);
	}

	void OnTurnCheck (object sender, object args)
	{
		BaseException exc = args as BaseException;
		if (exc.toggle == false)
			return;

		Unit target = sender as Unit;
		switch (step)
		{
		case 0:
			EquipCursedItem(target);
			break;
		case 1:
			Add<SlowStatusEffect>(target, 15);
			break;
		case 2:
			Add<StopStatusEffect>(target, 15);
			break;
		case 3:
			Add<HasteStatusEffect>(target, 15);
			break;
		default:
			UnEquipCursedItem(target);
			break;
		}
		step++;
	}

	void Add<T> (Unit target, int duration) where T : StatusEffect
	{
		DurationStatusCondition condition = target.GetComponent<Status>().Add<T, DurationStatusCondition>();
		condition.duration = duration;
	}

	void EquipCursedItem (Unit target)
	{
		cursedUnit = target;

		GameObject obj = new GameObject("Cursed Sword");
		obj.AddComponent<AddPoisonStatusFeature>();
		cursedItem = obj.AddComponent<Equippable>();
		cursedItem.defaultSlots = EquipSlots.Primary;

		Equipment equipment = target.GetComponent<Equipment>();
		equipment.Equip(cursedItem, EquipSlots.Primary);
	}

	void UnEquipCursedItem (Unit target)
	{
		if (target != cursedUnit || step < 10)
			return;

		Equipment equipment = target.GetComponent<Equipment>();
		equipment.UnEquip(cursedItem);
		Destroy(cursedItem.gameObject);

		Destroy(this);
	}
}

Note that I wont have saved the scene changes or demo script to my repository. They are merely fun little snippets to verify that our code works and to help you understand how things work together.

Summary

In this lesson we refactored some of our code to support new ways to work with value change exceptions. Using the new method of value modifications, we were easily able to implement several status effects including Haste, Slow, and Stop. For variety we also added Poison. Next, we provided a system which could manage the “lifespan” of a status effect by keeping it active for as long as any status condition was also applied. Finally, we showed a means of actually applying a status effect. We went for a specialty route and applied a status effect by making it a Feature of an equipped weapon.

Don’t forget that the project repository is available online here. If you ever have any trouble getting something to compile, or need an asset, feel free to use this resource.

Advertisements

3 thoughts on “Tactics RPG Status Effects

  1. If you are taking requests for upcoming lessons I have a few requests. Would you cover what you feel is the best way to implement AI for out battles and how to scale the difficulty of that AI? Also, would you cover how to make a shop that is capable of sorting items by various fields like cost, atk dmg, lvl requirement etc? Keep up the great work! I’m learning tons!

    Liked by 1 person

    1. I’m always glad to hear suggestions for topics. AI is definitely something I want to cover, but I haven’t even completed a “true” attack yet, much less a variety of the other abilities, so it will still be a bit down the road. On the other hand, I would suggest that AI isn’t necessarily as hard as many make it out to be – especially with an RPG. Many people could be fooled into believing the AI got “smarter” simply by boosting the count of enemies or by boosting their stats.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s