Ability scores are an important game mechanic that relate the skills and abilities of creatures and characters to a simple number.
Overview
When playing games like Pathfinder or Dungeons and Dragons, you have the opportunity to create the character you will be role playing as. The first step of generating a character is to determine something called ability scores. These are six attributes that are used to loosely define an ability to successfully accomplish varying types of tasks:
The score of a given attribute is just a number. It would usually fall somewhere between values of 3 to 18, with 10 being considered average. There are only so many points that can be assigned, so they are usually distributed based on the types of activities a character is most likely to be performing. For example, fighters and monks are likely to engage in melee combat, and so would want high scores in Strength. As spell casters, a wizard would probably favor Intelligence.
All characters will have each of the above ability score attributes. Most creatures also have them, though in some cases they may not have a particular ability at all. It will be important to be able to distinguish having a score of ‘0’ from not having an ability.
There is also a concept of a “modifier” that is derived from an ability score. It is calculated as half the score minus five, and is also used in a variety of formulas for various tasks and skills. The modifier for a creature that doesn’t have an ability attribute is just zero.
Getting Started
Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.
Create a new folder in Assets/Scripts/Component named AbilityScore. Next create a new folder in Assets/Tests/Component named AbilityScore. All of the scripts we create in this lesson will be placed in one of those two folders or pre-existing folders.
Ability Score
Create a new C# script in Assets/Scripts/Component/AbilityScore named AbilityScore. Copy the following:
[System.Serializable] public struct AbilityScore { public int value; public int Modifier => value / 2 - 5; public AbilityScore(int value) { this.value = value; } }
In my mind, an ability score is really just a number, and so could be implemented as an int. I went a different route and made it a struct that has a value field. This is because I decided to think of an ability score as “having” a Modifier property. A possible alternative would be needing to create a Modifier from an ability score, perhaps by accessing a method of another system. See the difference in the following (fake) examples:
// Example of getting a strength modifier with the current solution hero.Strength.Modifier; // Example of getting a strength modifier with an alternative solution someSystem.Modifier(hero.Strength);
I don’t think one choice was necessarily more correct than the other, but I do like that the first option is a little shorter.
Add the following snippets inside the body of AbilityScore:
public enum Attribute { Strength, Dexterity, Constitution, Intelligence, Wisdom, Charisma }
There are six basic attributes of Ability Score, which I have implemented as an enum. In many cases, I will know exactly what attribute I wish to work with and can use it explicitly, but I can imagine scenarios where I need to be able to target a score dynamically and in that case being able to refer to it by one of these values will be helpful.
public static implicit operator int(AbilityScore score) => score.value; public static implicit operator AbilityScore(int score) => new AbilityScore(score);
Since I mostly think of an AbilityScore as being a “number”, I have added some implicit operators so that I can treat an AbilityScore as an int, or vice-versa. For example:
// 'hero' is an Entity instance hero.Strength = 15; // implicit AbilityScore from an int int strength = hero.Strength; // implicit int from an AbilityScore
Strength System
Create a new C# script in Assets/Scripts/Component/AbilityScore named StrengthSystem. Copy the following:
public partial class Data { public CoreDictionary<Entity, AbilityScore> strength = new CoreDictionary<Entity, AbilityScore>(); } public interface IStrengthSystem : IDependency<IStrengthSystem>, IEntityTableSystem<AbilityScore> { } public class StrengthSystem : EntityTableSystem<AbilityScore>, IStrengthSystem { public override CoreDictionary<Entity, AbilityScore> Table => IDataSystem.Resolve().Data.strength; } public partial struct Entity { public AbilityScore Strength { get { return IStrengthSystem.Resolve().Get(this); } set { IStrengthSystem.Resolve().Set(this, value); } } }
Hopefully the code you see here will be familiar. It is almost exactly the same as the code we added for the NameSystem. It starts by adding a partial implementation of our Data where we added a mapping from an Entity to an AbilityScore (the CoreDictionary named strength).
Next is an interface, IStrengthSystem that inherits from IDependency so that it is injectable, and from IEntityTableSystem to inherit some basic “Table” related functionality. The class, StrengthSystem inherits from EntityTableSystem and conforms to our IStrengthSystem interface.
Finally I also added a convenience partial definition of Entity so that I could get the Strength as a wrapped property of our system.
On Your Own
We need systems for the other five ability score attributes: Dexterity, Constitution, Intelligence, Wisdom, and Charisma. The code will look nearly identical to the example I have provided with the StrengthSystem – the only real difference is found in their names and the field of the Data that they point to. Therefore, I have decided to leave adding them as an exercise for the reader. If you get stuck, you can always download the completed project example for help.
Ability Score System
Create a new C# script in Assets/Scripts/Component/AbilityScore named AbilityScoreSystem. Copy the following:
using System.Collections.Generic; using System.Linq; using UnityEngine; public interface IAbilityScoreSystem : IDependency<IAbilityScoreSystem> { AbilityScore Get(Entity entity, AbilityScore.Attribute attribute); void Set(Entity entity, AbilityScore.Attribute attribute, AbilityScore value); void Set(Entity entity, IEnumerable<int> scores); } public class AbilityScoreSystem : IAbilityScoreSystem { public void Set(Entity entity, IEnumerable<int> scores) { Debug.Assert(scores.Count() == 6, "Incorrect ability score count"); IStrengthSystem.Resolve().Set(entity, scores.ElementAt(0)); IDexteritySystem.Resolve().Set(entity, scores.ElementAt(1)); IConstitutionSystem.Resolve().Set(entity, scores.ElementAt(2)); IIntelligenceSystem.Resolve().Set(entity, scores.ElementAt(3)); IWisdomSystem.Resolve().Set(entity, scores.ElementAt(4)); ICharismaSystem.Resolve().Set(entity, scores.ElementAt(5)); } public AbilityScore Get(Entity entity, AbilityScore.Attribute attribute) { switch (attribute) { case AbilityScore.Attribute.Strength: return IStrengthSystem.Resolve().Get(entity); case AbilityScore.Attribute.Dexterity: return IDexteritySystem.Resolve().Get(entity); case AbilityScore.Attribute.Constitution: return IConstitutionSystem.Resolve().Get(entity); case AbilityScore.Attribute.Intelligence: return IIntelligenceSystem.Resolve().Get(entity); case AbilityScore.Attribute.Wisdom: return IWisdomSystem.Resolve().Get(entity); case AbilityScore.Attribute.Charisma: return ICharismaSystem.Resolve().Get(entity); } return (AbilityScore)0; } public void Set(Entity entity, AbilityScore.Attribute attribute, AbilityScore value) { switch (attribute) { case AbilityScore.Attribute.Strength: IStrengthSystem.Resolve().Set(entity, value); break; case AbilityScore.Attribute.Dexterity: IDexteritySystem.Resolve().Set(entity, value); break; case AbilityScore.Attribute.Constitution: IConstitutionSystem.Resolve().Set(entity, value); break; case AbilityScore.Attribute.Intelligence: IIntelligenceSystem.Resolve().Set(entity, value); break; case AbilityScore.Attribute.Wisdom: IWisdomSystem.Resolve().Set(entity, value); break; case AbilityScore.Attribute.Charisma: ICharismaSystem.Resolve().Set(entity, value); break; } } } public partial struct Entity { public AbilityScore this[AbilityScore.Attribute attribute] { get { return IAbilityScoreSystem.Resolve().Get(this, attribute); } set { IAbilityScoreSystem.Resolve().Set(this, attribute, value); } } }
AbilityScore has an enum named Attribute. This system allows access to an Entity’s various scores via the cases of this enum. It also allows setting all scores as an enumerable collection of values. Thanks to the partial definition on Entity, we can also get or set ability scores by an indexer. That could look something like this:
// hero is an Entity var attribute = AbilityScore.Attribute.Strength; // could be any case var score = hero[attribute]; var modifier = hero[attribute].Modifier;
Unit Tests
Create a C# Test Script in Assets/Tests/Component/AbilityScore named AbilityScoreTests. Copy the following:
using NUnit.Framework; using UnityEngine; public class AbilityScoreTests { [Test] public void Modifier_Success() { Assert.AreEqual(-5, new AbilityScore(1).Modifier); Assert.AreEqual(-4, new AbilityScore(2).Modifier); Assert.AreEqual(-4, new AbilityScore(3).Modifier); Assert.AreEqual(0, new AbilityScore(10).Modifier); Assert.AreEqual(0, new AbilityScore(11).Modifier); Assert.AreEqual(1, new AbilityScore(12).Modifier); Assert.AreEqual(1, new AbilityScore(13).Modifier); Assert.AreEqual(5, new AbilityScore(20).Modifier); Assert.AreEqual(17, new AbilityScore(45).Modifier); } }
The calculation for getting a modifier from an ability score is pretty simple. I grabbed several expected values from a table just to verify that values appear correctly, including negative and positive results.
Summary
In this lesson we introduced Ability Scores – a game mechanic where simple numbers represent attributes related to skills and abilities of characters and creatures. We added the score itself, along with some systems for working with the various score types.
If you got stuck along the way, feel free to download the finished project for this lesson here.If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!
Thanks for the tutorials! Keep the good work!
You’re welcome! Glad you are enjoying the new series so far.