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 / Properties 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!
Cool blog, thanks for sharing your skills!
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
Sorry friend, I no agree the Notification Center attach to my proyect.
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.
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.
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
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 asreturn growRate * level;
and the LevelForExperience method would also be easyreturn exp / growRate;
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!
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.
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.
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.
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.
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?
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..
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.
Would those be sub-components of the stats component or base components of the character? I’ll be waiting for that article then :p
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.
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.
Found the bug. The Base Ability Power script was old code. Updated it to your newest, and its working.
Quick question, won’t AwardExperience return NaN if you only have one hero in your party?
the following line should give NaN since it’s dividing by zero:
“float percent = (float)(ranks[i].LVL – min) / (float)(max – min);”
I added the following custom code to the beginning of the function to account for this:
if (party.Count == 1)
{
Rank rank = party[0].GetComponent();
if (rank != null)
rank.EXP += amount;
return;
}
Great catch, you are right! Now that I look at it again, it could also happen even with multiple party members that all had the same LVL stat. In order to handle both cases, you might try something else like adding one to the max level and subtracting one from the min level so that there is always a range.
Ah, thanks, didn’t think of that.
Just wanted to thank you again for this tutorial, Unity was a bit intimidating at first but I feel like I have a much deeper understanding of its utilities after following along here.
I was stuck a little bit. I am trying to implement the experience gain in the endbattlestate. Everything seems to be running correctly with the test code here, except that the value for the experience is always -2147483648 (I placed a debug.log to read the subamount).
Then I did as Chris (if I may call you Chris ;)) and you mentioned here, and it was fixed!
if (max == min) {
max = max + 1;
// max += max;
}
On a side note, if a character gains 1000 exp and levels from 1 to 10 for example, is there a way I can count easily the number of levels gained? Or can I make the onlevel up post notification happen each time a unit gains a level, not just the final level? I was using the onlevelup listener to trigger something else, however the onlevelup only seems to trigger one time for the final new level.
Yes, you can easily determine the amount of levels earned. Take a look at the “OnExpDidChange” method… if you were to store the new “LevelForExperience” stat result in a temporary variable before updating the real LVL stat, then it should be pretty trivial to determine the difference between the current LVL stat and the new temporary variable. The code might look something like:
var nextLevel = LevelForExperience(EXP);
var changeInLevels = nextLevel - stats[StatTypes.LVL];
stats.SetValue(StatTypes.LVL, nextLevel, false);
Or, you could separately observe the “DidChangeNotification” for the LVL stat which includes the “oldValue” as an argument. This approach guarantees that any modifiers would also be taken into account.
Hey, your tutorials are great! I followed it up so far and managed to finish it.
But now I want to tweak it a bit, and I’ve stumbled into a problem. I wanted to change the combat system from using ATK, DEF, and Evade (and MATK and MDEF) into a system where hit or miss (and damage) is determined by the attacker rolling d10s and seeing how many rolls under a certain number.
So, now I have an ATKROLL stat (which is how many dice rolled in an attack), and ATKRATING stat (which is the number the rolls must beat). Each roll that pass is a hit, and vice-versa each roll that failed is a miss. Now, the problem is that each ability (and each weapon) would have different ATKROLL and ATKRATING stats, with maybe the Unit itself having an increased ATKROLL or ATKRATING with leveling up.
So, is attaching a Stat component (with only those two stats in it) onto the Ability or Equipment gameObject the right way to do it, or should I do something else?
I also want to show the rolls visually, how do I track the dices being rolled?
Thanks for the time, I really need a second opinion on this.
Hey, your question is very applicable to the next post in this series, “Tactics RPG Items and Equipment”. In particular, I had imagined that weapons and armor would modify a unit’s stats by the “StatModifierFeature”. Your other questions are outside the scope of this tutorial, but if you want to open a thread on my forum I would be happy to go into more depth there.
Hey there, thanks again for all you’ve done for us with this set of tutorials!
I have a quick question regarding the numbers I’m getting in the Test scene. For instance, I see these results in the console logs:
Name:Josh Level:3 Exp:416
Josh would have received 90 experience, but we multiplied by 2
Name:Josh Level:4 Exp:1012
Of course, it’s kind of obvious that in this case we multiplied Josh’s *total* EXP by two, after first adding the 90. This is not how I imagine it should go, and seems that the order of operations is wrong. I’m having an issue untangling exactly where/what to change, as I’m still somewhat new to events, handlers and arguments, and keeping a clear head about what we’ve done from previous lessons is a bit rough.
Is there some way to make the multiply modifier apply to the 90 experience (in this case), or are we stuck modifying the total? Am I correct that this is a ‘bug’ or is this intentional?
You are right, it was a bug – I didn’t notice it until part 17: Status Effects. You can read all about it and how to fix it there.
Thank you for the reply! As per the forum post, I think I tracked down the functionality and understand why it’s happening. I just didn’t give a second thought as to whether or not it was supposed to be this way and assumed I was missing something.
Glad to know it’s touched on later and I don’t have to deviate from your excellent tutorial just yet 🙂
Hi, thank you again for your amazing content. I have hit a bug but I can’t find the solution to fix it, the first time I ran the scene everything worked as expected but then an error appeared and not the console does not log anything at all on subsequent tests. The console logs:
ArgumentNullException: Argument cannot be null.
Parameter name: key
System.Collections.Generic.Dictionary`2[System.Object,System.Collections.Generic.List`1[System.Action`2[System.Object,System.Object]]].ContainsKey (System.Object key) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:458)
NotificationCentre.RemoveObserver (System.Action`2 handler, System.String notificationName, System.Object sender) (at Assets/Scripts/Model/NotificationCentre.cs:79)
NotificationCentre.RemoveObserver (System.Action`2 handler, System.String notificationName) (at Assets/Scripts/Model/NotificationCentre.cs:57)
NotificationExtensions.RemoveObserver (System.Object obj, System.Action`2 handler, System.String notificationName) (at Assets/Scripts/Model/NotificationExtensions.cs:29)
TestLevelGrowth.OnDisable () (at Assets/Scripts/Temp/TestLevelGrowth.cs:14)
Any help is appreciated.
Without seeing your code I can only guess. The logs make me think the problem is related to your use of the Notification Center. My primary guess is that you have tried to use “RemoveObserver” more than once with the same key (The part that looks something like this: Stats.DidChangeNotification(StatTypes.LVL)). It’s possible you have additional issues if you aren’t seeing notifications any more. Perhaps you accidentally used “RemoveObserver” when you meant to use “AddObserver” or perhaps you tried to “RemoveObserver” at the wrong time.
Are you merely following along with the “Test & Demo” section, or have you started adding your own code as well?
Hi,
I haven’t changed anything, but I did find a “return” that i missed and now everything is working fine, thank you for your help and sorry if this was trivial, again amazing work as always.
Hi, I thanks for the tutorial, I’m really enjoying it so far.
I just noticed a minor typo in some of your code, since you seem to like you job well done (otherwise the tutorial wouldn’t have this quality) you might want to correct it ^^
In the first lines of the “ValueChangeException” script you wrote #region Fields/Properteis” I guess you wanted to say properties.
It’s no big deal tho so take your time.
Thanks for the good work!
Fixed 🙂
Hello, love these posts. I am making a little tactics game in my free time and the focus on architecture and design has been very helpful, especially this post about exceptions, which was a real stumbling block for me.
I have a question about modelling derived stats. Say you have an armor class, that is based off your Agility, but can be adjusted separately from Agility. Derived stats wouldn’t need to be stored in job or class data or save data, but calculated at run time based on base stats and exceptions. Curious how would you expand the design from the post to model something like this? I’ve been trying to come up with an elegant solution and coming up.. inelegant…
Have you completed the series yet? The next lesson on Items and Equipment might help explain the general idea, because the resulting stat will be based on an event like equipping or removing an item. If that doesn’t quite cover what you meant, then I’d recommend starting up a new thread on my forum so we can go a bit more in depth. Hope that helps.
I am pretty sure this wouldn’t cover the case that I am thinking of.
To the forums!
Hello JON, I really appreciate all your precious work here. I’m planning to make my own game (not yet firmly fixed the game design, but it might be a type of Board Game + CCG that has a lot of things to learn from your works!), so I’m following your tutorial (4 years from the first post! lol).
While following yours, however, I’ve met a NullReferenceException at your Stats.SetValue Method. Below is my new debugged code :
******************************************************************************
public void SetValue (StatTypes type, int value, bool allowExceptions)
{
int oldValue = this[type];
if (oldValue == value)
return;
//Allow exceptions to the rule here
/* I’ve put it outta the if phrase! */
ValueChangeException exc = new ValueChangeException(oldValue, value);
if(allowExceptions)
{
//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), exc);
/* The original post, the second parameter was ‘oldValue’, not ‘exc’! */
}
******************************************************************************
Without this fixation, the script meets problem at Rank.OnExpWillChange :
******************************************************************************
void OnExpWillChange(object sender, object args)
{
ValueChangeException vce = args as ValueChangeException;
vce.AddModifier(new ClampValueModifier(int.MaxValue, EXP, maxExperience));
}
******************************************************************************
Here we are trying to change ‘object args’ to ‘ValueChangeException’ type. The original code just passed an int value (oldValue), which can’t translated to ValueChangeException type whose form is (fromValue, toValue).
My scripts have no problem after changing the ‘oldValue’ to ‘exc’.
If this is a definite problem, however, I can’t understand that why no other guys have experienced same problems. There were no reply same to mine!
Could you please check this, JON? Do you think I am doing right, or there might be something missing?
(It’s hard to post my full code here, basically I’ve made almost same codes as you posted.. If you want to see other method or classes, please tell me where to post as comment here.)
The reason my code works is because there are two different notifications posted. The first one, “WillChangeNotification” passes along the “ValueChangeException” object so that there is a single place where each observer can know the old value and the potential (but changeable) new value. This is the notification that “Rank.OnExpWillChange” is observing, so there is no problem casting the args accordingly.
The second notification, “DidChangeNotification” doesn’t need to provide the new value, because that is simply the stat’s current value, but it still might be interesting to observers to know what the old value had been, so I pass that along. I didn’t pass the “ValueChangeException” in the “DidChangeNotification” because exceptions may not have even been allowed, and in that case, I wouldn’t even bother creating that object. Either way both the old value and new (current) value are available at that time.
Also, the info that is passed with each notification helps indicate the purpose and potential of the notification. I didn’t intend for anyone to modify stat values in a “DidChangeNotification” – because at that point I want to indicate that everything is “final”. Had I passed a “ValueChangeException”, then people might think it is still ok to modify the stat at that time, but any changes made wouldn’t actually be kept and so this would be misleading.
Thank you for the quick reply. Now I know that you intended to use the ‘oldValue’ because you don’t want ‘exceptions’ allowed here.
After reverting back to the original code, however, I meet error as below (which doesn’t occur at the fixed code) :
****************************************************************************
NullReferenceException: Object reference not set to an instance of an object
Rank.OnExpWillChange (System.Object sender, System.Object args) (at Assets/Scripts/View Model Component/Actor/Rank.cs:64)
NotificationCenter.PostNotification (System.String notificationName, System.Object sender, System.Object e) (at Assets/Scripts/Common/NotificationCenter/NotificationCenter.cs:185)
NotificationExtensions.PostNotification (System.Object obj, System.String notificationName, System.Object e) (at Assets/Scripts/Extensions/NotifitcationExtensions.cs:15)
Stats.SetValue (StatTypes type, System.Int32 value, System.Boolean allowExceptions) (at Assets/Scripts/View Model Component/Actor/Stats.cs:83)
Rank.Init (System.Int32 level) (at Assets/Scripts/View Model Component/Actor/Rank.cs:94)
TestLevelGrowth.VerifySharedExperienceDistribution () (at Assets/Temp/TestLevelGrowth.cs:44)
TestLevelGrowth.Start () (at Assets/Temp/TestLevelGrowth.cs:21)
****************************************************************************
As I mentioned at the first reply, the problem occurs at Rank.OnExpWillChange() :
******************************************************************************
void OnExpWillChange(object sender, object args)
{
ValueChangeException vce = args as ValueChangeException;
// After using ‘Debug.Log()’, I could find the ‘vce’ have some problem.
// My assumption is that, the problem caused because we’re trying to throw ‘oldValue(int)’ to ‘ValueChangeException’ type.
// If I want to preserve the original code as you intended, what I have to do now?
vce.AddModifier(new ClampValueModifier(int.MaxValue, EXP, maxExperience));
}
******************************************************************************
It sounds like there must be a difference between your code and the project code. If you download the project repository, check out this lesson’s commit, and run the sample it should build and play without errors. My guess is that your implementation of Rank might accidentally have linked the “DidChangeNotification” to the “OnExpWillChange” handler instead of the “OnExpDidChange” handler. You will find that connection made in the “OnEnable” method.
Oh, I’ve found your reply just now, then SUCCEED to find the reason!!
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}WillChange”, type.ToString()));
return _didChangeNotifications[type];
}
the ctrl + c v PROBLEM was here….. I added “Stats.{0}Willchange” again to DICTIONARY _didChangeNotifications!
Sorry for bothering you with such a trivia :d And REALLY THANKS!!!
Hello Jon,
My question for you today (sorry I post so much, I’m devouring your tutorials during confinement) is about a certain syntax you use in this tutorial.
I don’t understand how the syntax works for the modification of the ValueChangeException. I tested the whole thing and everything works, so it’s not a debugging question, I just need to understand what exactly is happening.
In the stats script, inside of the SetValue method we have:
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;
}
So the valueChangeException in this script is stored in a variable exc, which is local to this method.
In the rank script, we have this method to prevent EXP from decreasing:
void OnExpWillChange(object sender, object args)
{
ValueChangeException vce = args as ValueChangeException;
vce.AddModifier(new ClampValueModifier(int.MaxValue, EXP, maxExperience));
}
I don’t understand why the vce variable, which is local to this OnExpWillChange method, affects the other exc variable. Somehow, they point to the same ValueChangeException, but how?
Shouldn’t this be a case of
int a = 4;
int b = a;
b+=3;
so b==7 but a==4 still?
In programming there are two main types of data: value-types and reference-types. In your example with the “int” data-types what you have is a value-type. Value-types are copied by their value, which is why `b` is able to hold the same value as `a` without modifying the value of `a`. Reference-types are passed by a reference, which is like a memory address of where it exists. So when we pass this type of object in a parameter to a method, even though it looks like a local variable, it is actually still pointing to the same memory as from earlier. It wont matter “where” we modify it.
You can know something is a Reference type because it is defined as a “class”. If you see something defined as a “struct” or which uses one of the base level data types like an “int”, then those will be value-types.
Ah, I’ll have to get used to that. Thanks for the explanation!
Hey John, been following along quietly these past few weeks in what time I can afford. I’ve rewritten my state machine (3 times) based on your earlier articles in the series and I’m finally at a place I am comfortable with it.
I finally have a question for you, so I will throw in a bit of feedback as well. I haven’t finished the whole project series yet (and probably won’t for a while), so if you’ve addressed anything in a later article feel free to send me packing.
1) First to say, thanks for all this, and for continuing to reply and engage with your audience 5 years (!) later. I have looked several times over the past 8 months for actual source code of a tactics rpg in a somewhat complete state, and yours is the only thing I found worthwhile.
2) Looking ahead in the series, that last article was from Dec 2016. Is this project complete? Do you have any articles that address some of the significant changes in C# and Unity versions since then? I’m thinking Unity’s input system, GUI, and some C# features like events/delegates, expression functions, LINQ, netcore, etc. I am impressed by your explorations of the reasoning behind the decisions you make so I’d love to hear your opinions on where you would take this project with new features in Unity and .NET.
3) What’s the deal with Unity code style? Coming from several years of professional C# dev (and Java before that), Unity’s naming conventions and style like you use here confuse me. I’m all for adopting a new convention, but it seems people are all over the place with Unity’s style vs MS’ style. I haven’t been able to find any standards guidelines either. What’s your take?
4) Finally, actually related to this specific article: I’ve been pretty on board with everything you’ve done so far, but this one has me scratching my head. Can you elaborate on your reasons to make each stat its own component rather than simple properties (perhaps with property change notifications), and why you index them by an enum rather than hard-coded property names?
The other thing that is a bit strange to me is calling SetValue again from OnExpDidChange. I understand why it works, but it seems weird and prone to confusion/bugs, since the SetValue gets called twice, and the ExpWillChange notification only getting fired when exceptions are allowed, with the ExpDidChange notification getting skipped the second time based on that == check. Is there actual value in having that second SetValue call? Without experimenting with it yet, it seems to me that the SetValue should be expected to set the correct value from the modifiers everytime.
Again, I only ask because I find your explanations (in comments here and on reddit) to be almost as useful as the articles themselves.
Once again, thanks for all of these guides!
I just realized that OnExpDidChange sets the *Level* not the Exp again, so ignore that comment :/
You’re very welcome. It is rewarding to me to know that my work has maintained value for so long, so I am happy to help out where I can.
I wouldn’t say the project is “complete”, but that it is “complete enough” – my primary goal was to make something that resembled a complete battle engine for a Tactics RPG which I felt would be the greatest challenge in programming that kind of game. I feel like I accomplished that much. There is always room for polish, more features, maintenance to keep things up-to-date, etc and it is possible that I will revisit it, but I don’t have active plans to do anything at the moment.
I somewhat intentionally stayed away from implementing too many Unity specific features like GUI because I knew that those kinds of things would be the most likely to change over the years. As for advancements in things like .NET – I would be happy to update those, but my job has shifted to Swift rather than C# and so I am out of date myself 🙂
Unity’s code style? My honest guess would be that a large part of the Unity community are not professional programmers. There are a ton of self-taught hobbyists who learn by doing and then share what they did. I am a professional, but am a self-taught programmer as well, and I have dabbled in so many languages simultaneously that my style probably got a bit blurred. In addition, a lot of best practices (like using properties and not fields directly) are abandoned in favor of ease-of-use within Unity for things like serialization to appear in the inspector pane.
As for the stat architecture, just to make sure we are on the same page, each stat is NOT its own component. All stats live within a single component and are stored by an array of stat values. Regarding their access via an enumeration, I wanted to make something very flexible design-wise and also easy to use in Unity. Using things like strings or enums are very easy to use in the inspector and are easy for other beginners to understand as well. For example, I want to make a piece of equipment with a feature that boosts a specific type of stat – to build all of that at design time, I felt like it would be far simpler to just let it save an enum representing the stat to modify since it can already be serialized in the inspector. With all of that said, if you know of a better way to accomplish the same goals, feel free to mention it here or in the forums as I am sure other readers would benefit as well.
After working on implementing this and playing around with it, I’m not sure if I mispoke or just misunderstood, but yes I see now the stats collection is it’s own component.
I did not think about how the enum based stat would affect it’s interaction with modifying effects… that is a very good point.
I was thinking of just representing them as first class fields on the component. eg:
class Stats {
public int level;
public int exp;
public int maxHp;
//…etc
}
but that’d be a lot of messy control statements for handling items, skills, etc that would be configured to increase a stat. So you’ve convinced me!
My plan right now is to try to stick as close as possible to your tutorial, with some minor low level modifications (eg. I used a dictionary instead of the array for stats). Then, see where I might like to experiment. And if I find something I think works better, I will definitely bring it up for your feedback.
Wrt. hobby programming vs learning professionally, game development has been completely a hobby project for me, and developing large-scale web services is an entirely different ball game. The optimization concerns and design patterns are really different and at times counter-intuitive. I am having trouble sometimes wrapping my head around the differences, but explanations like you provide and discussions like these comments have been great, so thanks!
Question aout the set value method and the exception class.
Doesn’t this generate garbage everytime a value is changed?! So using this system for an RTS would be suicidal or am I talking stupid? Isn’t there a way to turn exceptions into structs?
The architecture here wasn’t designed for an RTS, it was designed for a turn-based RPG and so I was able to take a much more relaxed position with my architecture. With that said, I don’t know that I would go so far as to say the pattern is suicidal – feel free to profile to see how big of an impact the extra garbage collection will make. You could certainly opt to use other architectural patterns that create less objects, but it would not be trivial to change “this” pattern to use structs because structs cant use inheritance and even using it with the notification pattern will still result in boxing (the creation of another object).
Well I’mnot making an RTS, nor am I planning to. In fact given my current project this aproach seems like a better option.
I am mostly asking for pure curiosity, I respect you a lot as a programmer, as reading trough your blog has helped me improve a lot. And I was looking for your input, sorry if that sounded condescending. But I would love to hear what approach would you use for an RTS… If possible.
During my research I came across this https://www.youtube.com/playlist?list=PLm7W8dbdflohqccuwJxjYRKuDUnk131XO It seem lika good aproach too!
On another note. 1.- Would the rough ideas on your pokemon game translate well to unity’s DOTS? I’m looking to learn it, and I feel like a full game would be a way to learn 1.- Have you considered a youtube channel? As much as I preefer written tutorials, blogging seems to be dying, and I feel like a youtube channel would get your work much more widespread.
No worries, I didn’t think you were being condescending. I just wanted to point out that I wasn’t intending to create a one-size-fits-all architecture. In my personal opinion, trying to do that often results in an inferior experience for all. DOTS would probably (eventually at least) be a great approach for an RTS.
I watched the series you linked to. It’s also a nice solution, and you could do a lot with it. Of course, every approach will have pros and cons. Some things to think about:
1. Both the StatModifier class and CharacterStat class will grow in length as more types are added to the StatModType. I usually prefer to solve this problem with polymorphism (sub types of StatModifier that know how to apply themselves), though in some cases like with percent add it may actually be easier to work with all of them in a wrapping system rather than within the sub type itself.
2. You must apply the modifier statically not dynamically, and it must be per character. Say for example that you want some sort of feature that applies to all units within a party. Can that feature turn on and off? Is it easy to have an event where you apply and remove the feature as needed? Are there other flows you might forget to think about? For example, say a unit is spawned after the fact – will you remember to go back and look for environment features that were already turned on that should apply to it, and how easy will it be to find and apply them? My system is already dynamic and I wont have to think about state or flows like this because of it.
3. Persistence – what do you want to be able to save and how to you want to save it? Would you want to save the list of stat modifiers within the character stat class? If you need to load something and a character already had equipped items, does it unserialize already equipped, or do you need to instantiate and then re-equip the items the unit should have been wearing? Will that get confusing by potentially re-adding modifiers to the stat modifier list? Note that my tutorial project doesn’t cover persistence either, but I do feel like some of the pain points will be avoided in my approach.
Regarding your other questions, I’ve looked at DOTS a few times, hoping each time that it has evolved a bit more. It looks very powerful, but it is nowhere near as intuitive to use. It also looks like overkill for many types of games, including turn-based games like this one or my pokemon board game. Some things may translate, but I, for one, have not yet felt like it is ready to really try to. Perhaps as I use it more it will be more obvious how to structure heavily intertwined and dynamic stat tracking systems, but at the moment I would rather a more traditional object oriented / component based approach.
I have considered a youtube channel, but to me it all seems a bit overwhelming. The amount of production necessary to record, edit, etc, vs just typing some text is a lot. Plus, I don’t consider myself interesting to listen to. It doesn’t mean I wont ever try it, but I don’t have plans to at the moment.
Hey Jon, I’m a new developer (attempting a transition from production side to dev!) and have been loving this tutorial so far. I’ve been following closely up to now, but this is the step where I feel like my game needs to diverge and adapt from what you’ve presented.
The idea is that rather than a large party, the player will control a hero/sidekick duo–much smaller of a party than you would traditionally see in the genre. My plan is to have the hero “level up” in the form of combat proficiency, gained from utilizing different weapon types in combat. Proficiency levels with a particular weapon will give access to new abilities, bonuses, and styles of play for that weapon.
For example’s sake, let’s say there are ten weapon types they’ll have access to. I decided it would be better to have the levels and current experience for each weapon type to be tied to the hero rather than the weapons, as I wasn’t sure how to go about maintaining the level and experience in the case that a weapon is Destroyed. However, this has resulted in the Fields/Properties region of my code to be extremely long, with int/float declarations for each weapon’s proficiency level, experience, and exp percentage. Not inherently bad, I suppose, but I’m afraid I’m writing “wet” code.
Anyway, the crux of the issue — given my use case, I’m unsure how to best proceed with the OnEnable function, whether a nested if or maybe a switch statement with cases for each weapon is sufficient or if there’s a better approach. I’m very curious to hear how you might approach this!
These are excellent questions. Since it is a complex topic, and is off topic of this tutorial, it may be better to ask in the forums, but I’ll give a quick answer for now. You need to take advantage of something called polymorphism. Basically you need to identify things that are common for each weapon, such as having a unique identifier, level, experience, a concept of bonuses etc. so that you can treat them all the same without worrying about specific implementation details of a weapon at the hero level. The hero could simply have a List of weapons and treat them all the same.
Hi Jon!
Sorry to bother you again but I’ve been stuck on this problem for 2 days and I should probably address it before it becomes a bigger problem.
So for some reason my Stat panels are not hiding nor refreshing? I put a bunch of Debug.Log()s everywhere and pinpointed that if(obj.panel.CurrentPosition != target) is never true and if I remove the if statement, I get a null error.
(also, if it helps, I completely removed all references of ShowPrimary and it’s STILL showing up even when HidePrimary is being called??? I don’t know if you can hear just how confused I am)
Do you have any idea what the issue might be? If not, that’s okay! I just figured I should ask before I break my head against the wall haha
For anyone reading this question/answer, it refers to the “Stat Panel” lesson. The “StatPanelController” is the script that shows and hides the panels, and so the problem is likely related to mistyping something in that script or not configuring gameobjects properly in the scene.
I would start by copying/pasting the code from the website just to eliminate the possibility of having typed something wrong. If that doesn’t fix the issue, then I would look very carefully over the images in the lesson to compare setups. For example, make sure you haven’t attached the same panel to both the primary and secondary panel references. Make sure that both panels have a “Panel” script and that it has “Hide” and “Show” correctly configured. Also double check the references on the “Stat Panel” script.
If you still are stuck, my next best recommendation would be to take a brief detour to learn about source control (if you don’t already know it). You can use something like Atlassian SourceTree (it’s free) to download the repository (see repo link in the summary section of the lesson). Then you can look at the commit history of the project, and “checkout” (double click one of the commit history lines) the state of the project at that time. Then you may find it easier to compare what the project had vs what you have. Good luck!
Oh my god, I think I finally figured out the problem. Thank you, even though I thought I checked and double checked, I didn’t think to check the Panel script itself. I had forgot to add in the Hide and Show on the position list.
Here’s hoping it all works now! Thank you!
(Also, I apologize for putting this in the wrong place I was hopping around trying to figure out my mistake)