Tactics RPG Stats

In this lesson we will begin adding Stats to our game. Experience and Level are particularly important stats, so we will begin by focusing on those and show how we might distribute experience between heroes in a party. As enough experience is gained, the level of the hero can also be incremented. Along the way I will also address how one system might create an exception-to-the-rule in another system and offer a solution on how to allow them to interract while keeping things as decoupled as possible.

Preface

As usual, I internally debated on the implementation for this portion of the project – should I use GameObjects and Monobehaviours or simple C# objects? I know a lot of the more advanced users will complain if I use a Monobehaviour for stuff like this. Simple C# classes provide some nice flexibility, and reducing it further (to something like a database), would probably be even better.

In an effort to implement the game with native C# objects I created my own custom component based system. Because it was based on Unity, I found it very easy to adapt to and I had all the flexibility and speed I wanted without losing the features I needed. Unfortunately, very few people seemed interested in the system (almost no views), and the only feedback I received was negative. The comments I received were basically that it was a lot of extra work for no extra features, or that Unity’s component based architecture sucked and I should have gone for an Entity Component System if I was going to bother to change anything.

So what to do? I decided to embrace my toolset. I like Unity, and I like working with GameObjects and MonoBehaviours. By using Unity’s implementation we will be able to make use of a lot of great features including their component architecture (hopefully you don’t hate it), serialization (within a scene and or the project itself), editor visual aids (the inspector is great during development for debugging), etc. Sure they might not be the most efficient, but they are feature rich and good enough that you can certainly create a game, and even a nicely performant game at the level I am aiming for without worry. Perhaps if I were making the next multi-million dollar MMORPG I would need something more advanced, but I am not there yet, so I wouldn’t be a great teacher on that anyway.

If you are advanced enough to think my decision is too primitive, then you probably are skilled enough to make good use of my earlier mentioned system or an ECS, etc. Simply adapt the ideas I present here if you like them. Otherwise, don’t be so quick to judge. The speed at which Unity helps one prototype a game is hard to compete with.

Notifications

This lesson will be taking advantage of my notification center. I will be using the version I blogged about most recently, here. Of course you can also get a copy from the repository here.

Because I have already spoken a lot on the notification center I wont spend much time on it now. For a quick introduction, you can think of it as a messaging system which is similar to events. However, any object can post any notification and any object can listen to any notification (and you can specify whether you wish to listen to ALL senders and or from a targeted sender). The notification itself is a string, which means it can be dynamic, and this is something we will be taking advantage of in this lesson.

Base Exception

Don’t confuse this with C# language Exceptions (an error occuring during application execution). Here I am talking about the complex interactions between systems of an RPG. For example, you may normally be able to move, except you stepped in some glue and are waiting for the effect to wear off. Or you might normally do X amount of damage, but your sword has an elemental charge that the opponent is weak to so it now does extra damage. Rather than have all systems know about all other systems to handle each use-case, I have decided to create an exception class which is posted along with notifications where an exception might be needed. Listeners which need to produce the exception can then listen and alter a scenario as necessary.

The most basic level of exception I will model is one where a thing is normally allowed (but might be able to be blocked) or vice-versa. For this, create a new script named BaseException in the Scripts/Exceptions folder.

using UnityEngine;
using System.Collections;

public class BaseException 
{
	public bool toggle { get; private set; }
	private bool defaultToggle;
	
	public BaseException (bool defaultToggle)
	{
		this.defaultToggle = defaultToggle;
		toggle = defaultToggle;
	}
	
	public void FlipToggle ()
	{
		toggle = !defaultToggle;
	}
}

Modifiers

We are creating a stat system, and the exceptions we will be interested in will probably extend beyond whether or not to allow a stat to be changed and into the realm of how to change it. For example, if you are awarding experience to a hero, but your hero has equipped an amulet which causes experience to grow faster, then it will want a chance to modify the value which will be assigned.

When you post a notification, you wont know the order in which listeners are notified. However, the order which modifications are applied can be significant. For example, the following two statements would produce different results based on the order of execution indicated by the parentheses:

  • 532 * (0 + 10) = 5,320
  • (532 * 0) + 10 = 10

I want to allow multiple listeners the chance to apply changes but I also want some changes to be done earlier than other changes, or want to specify another change to happen after all other changes. If you have spent any time looking at damage algorithms in Final Fantasy you wont be surprised to see twenty or so steps along the way. They might start with a base damage formula, then add bonuses for equipment, then buffs, then the phase of the moon (possibly not joking here), and perhaps next would be the angle between the attacker and defender. Then they may clamp some values for good measure before continuing down the path. It is quite complex!

I chose to accomplish this in the following way. Any exception which includes modifiers will store them all as a list. All modifiers will have a sort order. After all modifiers have been added, they will be sorted based on their sort order, and then their modifications will be applied sequentially.

Create another subfolder in Scripts/Exceptions/ called Modifiers. Following is the implementation for the base Modifier.

using UnityEngine;
using System.Collections;

public abstract class Modifier
{
	public readonly int sortOrder;

	public Modifier (int sortOrder)
	{
		this.sortOrder = sortOrder;
	}
}

As I indicated, there may be lot’s of different kinds of modifiers taking place (particularly in regard to modifying a value), some for adding, some for multiplying, some for clamping values, etc. Here is the base abstract class for a value modifier followed by several concrete implementations:

using UnityEngine;
using System.Collections;

public abstract class ValueModifier : Modifier
{
	public ValueModifier (int sortOrder) : base (sortOrder) {}
	public abstract float Modify (float value);
}
using UnityEngine;
using System.Collections;

public class AddValueModifier : ValueModifier
{
	public readonly float toAdd;

	public AddValueModifier (int sortOrder, float toAdd) : base (sortOrder)
	{
		this.toAdd = toAdd;
	}

	public override float Modify (float value)
	{
		return value + toAdd;
	}
}
using UnityEngine;
using System.Collections;

public class ClampValueModifier : ValueModifier 
{
	public readonly float min;
	public readonly float max;

	public ClampValueModifier (int sortOrder, float min, float max) : base (sortOrder)
	{
		this.min = min;
		this.max = max;
	}
	
	public override float Modify (float value)
	{
		return Mathf.Clamp(value, min, max);
	}
}
using UnityEngine;
using System.Collections;

public class MaxValueModifier : ValueModifier 
{
	public float max;

	public MaxValueModifier (int sortOrder, float max) : base (sortOrder)
	{
		this.max = max;
	}

	public override float Modify (float value)
	{
		return Mathf.Max(value, max);
	}
}
using UnityEngine;
using System.Collections;

public class MinValueModifier : ValueModifier 
{
	public float min;

	public MinValueModifier (int sortOrder, float min) : base (sortOrder)
	{
		this.min = min;
	}
	
	public override float Modify (float value)
	{
		return Mathf.Min(min, value);
	}
}
using UnityEngine;
using System.Collections;

public class MultValueModifier : ValueModifier 
{
	public readonly float toMultiply;

	public MultValueModifier (int sortOrder, float toMultiply) : base (sortOrder)
	{
		this.toMultiply = toMultiply;
	}

	public override float Modify (float value)
	{
		return value * toMultiply;
	}
}

Value Change Exception

Based on the ideas I brought forth in the topics of value modifiers, here is a concrete subclass of the Base Exception which holds a list of value modifiers to modify the value which will be assigned in an exception use-case.

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

public class ValueChangeException : BaseException
{
	#region Fields / Properteis
	public readonly float fromValue;
	public readonly float toValue;
	public float delta { get { return toValue - fromValue; }}
	List<ValueModifier> modifiers;
	#endregion

	#region Constructor
	public ValueChangeException (float fromValue, float toValue) : base (true)
	{
		this.fromValue = fromValue;
		this.toValue = toValue;
	}
	#endregion

	#region Public
	public void AddModifier (ValueModifier m)
	{
		if (modifiers == null)
			modifiers = new List<ValueModifier>();
		modifiers.Add(m);
	}

	public float GetModifiedValue ()
	{
		float value = toValue;

		if (modifiers == null)
			return value;
		
		modifiers.Sort(Compare);
		for (int i = 0; i < modifiers.Count; ++i)
			value = modifiers[i].Modify(value);
		
		return value;
	}
	#endregion

	#region Private
	int Compare (ValueModifier x, ValueModifier y)
	{
		return x.sortOrder.CompareTo(y.sortOrder);
	}
	#endregion	
}

Stat Types

Individual stat types are just an abstract idea. Most every RPG out there, including those within the same series (like Final Fantasy), use a different set of stats to represent each game. Even if you have similarly named stats, the formulas you use for level growth, damage, etc can all be wildly different. There are often a lot of different stats, and because I am in a prototype stage it doesn’t necessarily make a lot of sense to hard code each stat. Instead, I will begin with an enumeration. By associating the enum type with a value, we can also make it really easy for other game features to target and or respond to a particular stat in a DRY (Dont Repeat Yourself) manner.

Create a new script named StatTypes in the Scripts/Enums folder. The implementation follows:

using UnityEngine;
using System.Collections;

public enum StatTypes
{
	LVL, // Level
	EXP, // Experience
	HP,  // Hit Points
	MHP, // Max Hit Points
	MP,  // Magic Points
	MMP, // Max Magic Points
	ATK, // Physical Attack
	DEF, // Physical Defense
	MAT, // Magic Attack
	MDF, // Magic Defense
	EVD, // Evade
	RES, // Status Resistance
	SPD, // Speed
	MOV, // Move Range
	JMP, // Jump Height
	Count
}

Stat Component

Anything which needs stats (heroes, enemies, bosses, etc) will have a Stat component added to it. This component will provide a single reference point from which we can relate a stat type to a value held by the stat.

Create a subfolder called Actor under Scripts/View Model Component and add a new script there named Stats.

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

public class Stats : MonoBehaviour 
{
	// Add Code Here
}

If you were confused earlier when I said we wouldn’t hard code our stats (but then I hard coded an enumeration), hopefully it is about to make more sense. Instead of having individual int fields for each of the stat types, I can make an array which aligns a stat type to an int. If I want to add, remove, or rename a stat I only have to worry about the enum and this will still work.

Note that I wont actually make the backing array public. Other classes dont need to know how or where I store the data. Instead, I will add an indexer which allows getting and setting values safely:

public int this[StatTypes s]
{
	get { return _data[(int)s]; }
	set { SetValue(s, value, true); }
}
int[] _data = new int[ (int)StatTypes.Count ];

The setter (via the SetValue method) will handle several bits of logic. Don’t miss the fact that I am able to reuse this logic regardless of which stat is changing!

One job of the setter will be to post notifications that we will be changing a stat (in case listeners want a chance to make some sort of exception) and another notification will be posted after we did change a stat (in case listeners want a chance to respond based on the change). For example, after incrementing the experience stat, a listener might also decide to modify the level stat to match. After incrementing the level stat, a variety of other stats might change such as attack or defense.

The notifications for each stat are built dynamically and stored statically by the class. This way both the listeners and the component itself can continually reuse a string instead of constantly needing to recreate it.

public static string WillChangeNotification (StatTypes type)
{
	if (!_willChangeNotifications.ContainsKey(type))
		_willChangeNotifications.Add(type, string.Format("Stats.{0}WillChange", type.ToString()));
	return _willChangeNotifications[type];
}

public static string DidChangeNotification (StatTypes type)
{
	if (!_didChangeNotifications.ContainsKey(type))
		_didChangeNotifications.Add(type, string.Format("Stats.{0}DidChange", type.ToString()));
	return _didChangeNotifications[type];
}

static Dictionary<StatTypes, string> _willChangeNotifications = new Dictionary<StatTypes, string>();
static Dictionary<StatTypes, string> _didChangeNotifications = new Dictionary<StatTypes, string>();

The first thing our setter checks is whether or not there are any changes to the value. If not we just exit early. If exceptions are allowed we will create a ValueChangeException and post it along with our will change notification. If the value does change, we assign the new value in the array and post a notification that the stat value actually changed.

public void SetValue (StatTypes type, int value, bool allowExceptions)
{
	int oldValue = this[type];
	if (oldValue == value)
		return;
	
	if (allowExceptions)
	{
		// Allow exceptions to the rule here
		ValueChangeException exc = new ValueChangeException( oldValue, value );
		
		// The notification is unique per stat type
		this.PostNotification(WillChangeNotification(type), exc);
		
		// Did anything modify the value?
		value = Mathf.FloorToInt(exc.GetModifiedValue());
		
		// Did something nullify the change?
		if (exc.toggle == false || value == oldValue)
			return;
	}
	
	_data[(int)type] = value;
	this.PostNotification(DidChangeNotification(type), oldValue);
}

Components which handle loading and or initialization can make use of the public method directly and specify that exceptions should not be allowed. Pretty well any other time that the stat values need to be set or modified should probably go through the indexer which does allow for exceptions.

Rank

Our hero actors will have a component called Rank which determines how the experience (EXP) stat relates to the level (LVL) stat. For example, as the experience stat increments so will the level stat (though not at the same rate). The leveling curve is based on an Ease In Quad curve, which means that low levels can be attained with less experience than high levels. For instance, it only takes 104 experience to go from level 1 to level 2, but it takes 20,304 experience to go from level 98 to level 99!

I think of this component as something like a wrapper because it doesn’t hold any new fields of its own, it simply exposes convenience properties around the existing Stats component’s values.

Add another script called Rank to the Scripts/View Model Component/Actor/ folder. The implementation follows:

using UnityEngine;
using System.Collections;

public class Rank : MonoBehaviour
{
	#region Consts
	public const int minLevel = 1;
	public const int maxLevel = 99;
	public const int maxExperience = 999999;
	#endregion

	#region Fields / Properties
	public int LVL
	{
		get { return stats[StatTypes.LVL]; }
	}

	public int EXP
	{
		get { return stats[StatTypes.EXP]; }
		set { stats[StatTypes.EXP] = value; }
	}

	public float LevelPercent
	{
		get { return (float)(LVL - minLevel) / (float)(maxLevel - minLevel); }
	}

	Stats stats;
	#endregion

	#region MonoBehaviour
	void Awake ()
	{
		stats = GetComponent<Stats>();
	}

	void OnEnable ()
	{
		this.AddObserver(OnExpWillChange, Stats.WillChangeNotification(StatTypes.EXP), stats);
		this.AddObserver(OnExpDidChange, Stats.DidChangeNotification(StatTypes.EXP), stats);
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnExpWillChange, Stats.WillChangeNotification(StatTypes.EXP), stats);
		this.RemoveObserver(OnExpDidChange, Stats.DidChangeNotification(StatTypes.EXP), stats);
	}
	#endregion

	#region Event Handlers
	void OnExpWillChange (object sender, object args)
	{
		ValueChangeException vce = args as ValueChangeException;
		vce.AddModifier(new ClampValueModifier(int.MaxValue, EXP, maxExperience));
	}
	
	void OnExpDidChange (object sender, object args)
	{
		stats.SetValue(StatTypes.LVL, LevelForExperience(EXP), false);
	}
	#endregion

	#region Public
	public static int ExperienceForLevel (int level)
	{
		float levelPercent = Mathf.Clamp01((float)(level - minLevel) / (float)(maxLevel - minLevel));
		return (int)EasingEquations.EaseInQuad(0, maxExperience, levelPercent);
	}
	
	public static int LevelForExperience (int exp)
	{
		int lvl = maxLevel;
		for (; lvl >= minLevel; --lvl)
			if (exp >= ExperienceForLevel(lvl))
				break;
		return lvl;
	}

	public void Init (int level)
	{
		stats.SetValue(StatTypes.LVL, level, false);
		stats.SetValue(StatTypes.EXP, ExperienceForLevel(level), false);
	}
	#endregion
}

Note that this component subscribes to the EXP Will Change stat notification. It creates a clamp modifier with the highest possible sort order (to make sure it is the last modifier to be applied). It makes sure that experience is ONLY allowed to increment – not decrement. No un-leveling in this game. Of course you may not wish to have this constraint in your own game. Perhaps the ability to lose experience could be a fun feature! Who knows?

We also subscribe to the EXP Did Change stat notification. This way we can make sure that the LVL stat is always correctly set based on how the experience growth curve would specify.

One final note is that I expose a couple of public static methods which allow you to convert between experience values and level values. These might be useful if you were creating a UI which showed some sort of progress bar of how close you are to the next level.

Experience Manager

Next let’s create a system which can distribute experience among a team of heroes. Perhaps in a normal battle, every enemy unit killed adds a certain amount of experience to a shared pool for later. If, and only if, the level/battle is conquered will the team actually receive the experience points and have a chance to “Level-up”. I would like heroes that are lower level to receive more experience than the heroes with a higher level, but I want to make sure that all heroes still gain experience points. Of course there can still be exceptions here like perhaps any KO’d heroes will not be able to receive experience.

Create a script called ExperienceManager in the Scripts/Controller folder. Following is the implementation:

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using Party = System.Collections.Generic.List<UnityEngine.GameObject>;

public static class ExperienceManager
{
	const float minLevelBonus = 1.5f;
	const float maxLevelBonus = 0.5f;

	public static void AwardExperience (int amount, Party party)
	{
		// Grab a list of all of the rank components from our hero party
		List<Rank> ranks = new List<Rank>(party.Count);
		for (int i = 0; i < party.Count; ++i)
		{
			Rank r = party[i].GetComponent<Rank>();
			if (r != null)
				ranks.Add(r);
		}

		// Step 1: determine the range in actor level stats
		int min = int.MaxValue;
		int max = int.MinValue;
		for (int i = ranks.Count - 1; i >= 0; --i)
		{
			min = Mathf.Min(ranks[i].LVL, min);
			max = Mathf.Max(ranks[i].LVL, max);
		}

		// Step 2: weight the amount to award per actor based on their level
		float[] weights = new float[party.Count];
		float summedWeights = 0;
		for (int i = ranks.Count - 1; i >= 0; --i)
		{
			float percent = (float)(ranks[i].LVL - min) / (float)(max - min);
			weights[i] = Mathf.Lerp(minLevelBonus, maxLevelBonus, percent);
			summedWeights += weights[i];
		}

		// Step 3: hand out the weighted award
		for (int i = ranks.Count - 1; i >= 0; --i)
		{
			int subAmount = Mathf.FloorToInt((weights[i] / summedWeights) * amount);
			ranks[i].EXP += subAmount;
		}
	}
}

Note that this sample is just a rough prototype and hasn’t really been play-tested. If your game’s party only consisted of 2 units, one at level 4 and one at level 5, it might seem odd for the first unit to get three times as much experience as the other unit. If you had a party of 6 or so units you may never notice. By keeping the system separate it should be easy to tweak to our hearts content without fear of messing up anything else.

Test & Demo

Let’s wrap this lesson up with a quick test which also serves as a demo. In this test I want to verify that my implementation of converting back and forth between LVL and EXP in the Rank component works for every level as expected. So I will loop from level 1 thru 99 and verify that the output from the static methods match.

For my second test / demo I will create an array of heroes, init them all to random levels, and then use the manager to award the party an amount of experience. I will use a variety of modifiers to tweak the value awarded and print each step along the way to verify that it all works as expected.

Create a new scene and add a new script to the camera called TestLevelGrowth. The implementation follows.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Party = System.Collections.Generic.List<UnityEngine.GameObject>;

public class TestLevelGrowth : MonoBehaviour 
{
	void OnEnable ()
	{
		this.AddObserver(OnLevelChange, Stats.DidChangeNotification(StatTypes.LVL));
		this.AddObserver(OnExperienceException, Stats.WillChangeNotification(StatTypes.EXP));
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnLevelChange, Stats.DidChangeNotification(StatTypes.LVL));
		this.RemoveObserver(OnExperienceException, Stats.WillChangeNotification(StatTypes.EXP));
	}

	void Start () 
	{
		VerifyLevelToExperienceCalculations ();
		VerifySharedExperienceDistribution ();
	}

	void VerifyLevelToExperienceCalculations ()
	{
		for (int i = 1; i < 100; ++i)
		{
			int expLvl = Rank.ExperienceForLevel(i);
			int lvlExp = Rank.LevelForExperience(expLvl);

			if (lvlExp != i)
				Debug.Log( string.Format("Mismatch on level:{0} with exp:{1} returned:{2}", i, expLvl, lvlExp) );
			else
				Debug.Log(string.Format("Level:{0} = Exp:{1}", lvlExp, expLvl));
		}
	}

	void VerifySharedExperienceDistribution ()
	{
		string[] names = new string[]{ "Russell", "Brian", "Josh", "Ian", "Adam", "Andy" };

		Party heroes = new Party();
		for (int i = 0; i < names.Length; ++i)
		{
			GameObject actor = new GameObject(names[i]);
			actor.AddComponent<Stats>();
			Rank rank = actor.AddComponent<Rank>();
			rank.Init((int)UnityEngine.Random.Range(1, 5));
			heroes.Add(actor);
		}

		Debug.Log("===== Before Adding Experience ======");
		LogParty(heroes);

		Debug.Log("=====================================");
		ExperienceManager.AwardExperience(1000, heroes);

		Debug.Log("===== After Adding Experience ======");
		LogParty(heroes);
	}

	void LogParty (Party p)
	{
		for (int i = 0; i < p.Count; ++i)
		{
			GameObject actor = p[i];
			Rank rank = actor.GetComponent<Rank>();
			Debug.Log( string.Format("Name:{0} Level:{1} Exp:{2}", actor.name, rank.LVL, rank.EXP) );
		}
	}

	void OnLevelChange (object sender, object args)
	{
		Stats stats = sender as Stats;
		Debug.Log(stats.name + " leveled up!");
	}

	void OnExperienceException (object sender, object args)
	{
		GameObject actor = (sender as Stats).gameObject;
		ValueChangeException vce = args as ValueChangeException;
		int roll = UnityEngine.Random.Range(0, 5);
		switch (roll)
		{
		case 0:
			vce.FlipToggle();
			Debug.Log(string.Format("{0} would have received {1} experience, but we stopped it", actor.name, vce.delta));
			break;
		case 1:
			vce.AddModifier( new AddValueModifier( 0, 1000 ) );
			Debug.Log(string.Format("{0} would have received {1} experience, but we added 1000", actor.name, vce.delta));
			break;
		case 2:
			vce.AddModifier( new MultValueModifier( 0, 2f ) );
			Debug.Log(string.Format("{0} would have received {1} experience, but we multiplied by 2", actor.name, vce.delta));
			break;
		default:
			Debug.Log(string.Format("{0} will receive {1} experience", actor.name, vce.delta));
			break;
		}
	}
}

Run the scene and look through the console’s output to verify that everything is as you would expect it to be.

Summary

That ended up being a lot longer than I expected, again. One might think that something as simple as adding some stats and tying EXP to LVL would be easy. But then we started adding exceptions to the rule, value modifiers, a manager to distribute the experience among a party, etc. and things got a lot more complex. Hopefully you were able to follow along with all of my examples and implementations. If not, feel free to add a comment below!

Advertisements

19 thoughts on “Tactics RPG Stats

  1. Hi, thanks for continuing these articles. Again I found an error !! In the SetValue method in this line

    // The notification is unique per stat type
    this.PostNotification (WillChangeNotification (type), exc);

    The PostNotification method has not been defined.

    Thanks

    Like

      1. Please look again under the heading for Notifications – you should see a link for the older post or you can just look through the history of my blog for Better than events. In the same section I also provided a link to my project repository where you can get the Notification and NotificationExtensions classes.

        Like

    1. Toward the beginning of the article I mentioned including the NotificationCenter class I wrote in another post and provided the link. However, I could have been more clear and also mention that I also included the companion script NotificationExtensions which is where PostNotification is actually declared.

      Like

  2. Hi, thanks for your answers. I have a couple of questions I hope not bother much with them. First the easy question, in the method:

    LevelForExperience public static int (int exp)
    {
    int lvl = maxLevel;
    for (; lvl> = MinLevel; –lvl)
    if (exp> = ExperienceForLevel (lvl))
    break;
    lvl return;
    }

    At the beginning of the cycle for not declare the variable only use; this is the first time I see it. The system then uses the variable declared in the previous line, this is so?

    My other question is that I read the scripts and see several times the test code, but can not find where you set the amount of experience needed to level up. This amount is exponential? I remember the expericia necessary to level up was in Final Fantasy Tactics Advance, always 1000 and each offensive or defensive action gives you some experience. I mention this because, in my game is that I would use a system similar to the FFTA experience. Allowing level up in the course of the fight both heroes and foes. Your system could be adapted for this? Once I appreciate your answers. Greetings

    Like

    1. 1.) In a for loop, all of the expressions are optional (that goes for the initializer, condition and iterator). You still need the semicolons though as I show by simply skipping the initializer and beginning with a semicolon. In this case I used a variable which was declared outside of the for loop so that I could determine at which point I break out of the loop.

      2.) You won’t see a place where I hard code an amount of experience required for each individual level up, because I used a curve to define it for me. The curve is based on start(0) and end(999,999) values which I interpolate over. It isn’t an exponential curve in the pure mathematical term, but it does require more experience with each gained level. I included this version because it could be used in other RPG’s and was probably more confusing for people to implement.

      A fixed level growth system would be far easier than a curve to implement. Define an int variable in the class to hold the amount of experience you need for each level: int growRate = 1000; The ExperienceForLevel method would be as simple as return growRate * level; and the LevelForExperience method would also be easy return exp / growRate;

      Like

  3. I really enjoyed this implementation of a stats system, and especially the examples of how to use the notification system. I had a quick question for you- In the Stats class, in the SetValue method, last line, you send oldValue through the PostNotification method. Why do you send the old value? Or am I misunderstanding what is going on here?

    Thank you for this great tutorial series!

    Liked by 1 person

    1. Great question Jordan. The reason I include the “oldValue” is because there are often a variety of reasons I want to know more than just what the current value is. I want to know by how much a value has changed and whether the change was positive or negative.

      For example, sometimes you may want to show text on the screen over a character as a stat changes. You are more likely to show the amount of change than the current value, so by passing the oldValue and the object from which you can get the current value, you can then determine the amount of change.

      Like

  4. Ok brother, I like it this way rather than fixed values. Simply fails to understand all the code correctly. If it’s not too much trouble, you could include an example? It really cost me follow this tutorial.

    Like

    1. I’m glad you like it. I’m always happy to elaborate on any area which is unclear, but I already included a demo so you will have to be more specific in what you need help with.

      Like

  5. Could you give me an example of calculating experience. Suppose a unit gain experience with each hit from this point then hitting the enemy and methods come into play when called until the hero gains a level.You receive as much exp per hit, how much you need to level up, etc.I am that what I ask is tedious, but I would greatly help to follow the logic of this lesson. Thank you very much for your willingness to serve all doubts.

    Like

    1. Hey Gustavo, there is no limit to the number of different ways experience could be gained and every game will do it a little different. I can’t cover every possible way but I could share a few examples.

      In the example I created here I had imagined a scenario where every opponent would have some sort of “Enemy” component which included information like how much experience they would give for being defeated, and what rewards they could drop such as gold, etc. In this scenario, I would wait for the battle to actually be completed, and then the game would loop through all of the enemies, sum up the total amount of experience, and distribute it among all of the heroes in the player’s party.

      It sounds like you want something different, where you gain experience at each hit. For this scenario, I would still have an “Enemy” component which determined how much experience could be gained from defeating an enemy. Then, when you actually hit the enemy I would award a percentage of its experience according to the percentage of its health you took away. For example, say an Enemy has 100 hit points and your attack did 10 damage. You would then award 10% of whatever experience the enemy could award. If you go this route you would have to keep in mind ways that players could kind of cheat the system. For example, if a particular enemy is worth a lot of experience and they get experience for each hit, then they could attack it, heal it, and attack it again. By not actually letting the enemy die they could farm experience as much as they wanted. You could get around this by holding two fields, one for the max amount of experience it could award (use this stat for determining the percentage to give) and another which begins at the full amount and decrements with each hit until zero. When you award experience you would take the amount of experience the enemy had left or the amount determined by the percentage – whatever was less. In this way you could cap the amount of experience that players could farm out of any one enemy.

      Other games also award experience based on the technique. So for example, Skyrim which awards experience in a variety of categories based on the action you take. In this case, you gain levels in each category, and leveling up in the categories levels up the overall player level. In this case I wouldn’t necessarily have an “Enemy” component with an experience award stat for defeating the enemy – instead, there would be a stat on each skill that would say something like, for successfully hitting an enemy with this magic spell, award X amount of experience to the magic category.

      Does that help?

      Like

  6. Let me start by saying I love these ideas even thought I haven’t fully understood them yet :p (I didn’t know about indexers and they seem super useful!) I also have a few questions about why you did some stuff but I’ll have to reread before I get to asking.

    just a quick doubt for now: You’d still need to make components to handle some stats, like health and mana, no? For example, to handle death, healing/damage particle effects and all that. On the other hand move speed and jump height wouldn’t need one..

    Like

    1. Yes, just like we made the Rank component to wrap up special functionality for the Experience and Level, we would be able to make a health and mana component to handle the relationships between HP & MHP, and MP & MMP. Its kind of funny the timing of the question because my next post should actually include examples of both of those.

      Like

      1. Would those be sub-components of the stats component or base components of the character? I’ll be waiting for that article then :p

        Like

      2. In my implementation they would be components on the root of the character (at the same level as the Stats component) although it doesn’t actually matter where they are located. In fact, you could even make manager style components or systems on completely separate objects that listen to the notifications for all characters and clamp values etc. The units don’t necessarily need their own.

        Like

  7. Hey, So I am experiencing a strange bug where when I do a normal attack, how much damage it does is being affected by my Magic Attack stat as well as my attack stat. To test this I set everything stat to 1, and for all my units it said they would do 5 damage. Then I raised my magic attack, and their damage went up for regular attacks. The peculiar thing is that something else must be messing with my stats, because when I did the same test in your repository, the units all did 1 damage when I put all their stats to 1, and yours didn’t have the same bug, obviously.

    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