Design - In Concept
This explains concepts and design principles for the system's internals.
Last updated
This explains concepts and design principles for the system's internals.
Last updated
There are very few magic buttons in gamedev that solve your problems with no effort and offer unlimited complexity. If you're planning to scale your characters, create formulas for attributes, serialize or extend the attribute system then you need to know how it works!
Expect the system to calculate and manage the current value of character stats, along with any modifiers which are affecting those values that may have been applied by external systems (you).
You (Le Developer) are expected to put the components on to your characters and interact with them when you need to. In the stock system: Attributes
offers access to the stats, and Vitals
offers access to health/energy. For example if you want to calculate how much damage your sword does then you need to ask the Attributes class how much strength it has and use that as a multiplier for your own calculation of damage.
Note that this entire page explains how the system works internally, which is very useful to know especially if you are extending it, but if you just want a black box that handles your attributes then you may not care about the information in here. The last section has the most interesting material.
There are so many different ways to handle attributes and there are many different goals when creating an attribute system. It's important that users establish what kind of complexity their game needs to have on the Attributes system and how flexible the Attributes system needs to be.
Users could have different needs and demands ranging from very complex D&D formulas on abstract systems and some users just prefer to hardcode their stats and have very simple add/subtract formulas.
What works for you?
Performance can get away from you quickly if you're planning a game with many agents. Fortunately, that's not something you have to worry about anymore.
The Attributes system is focused on providing a lot abstraction and flexibility while still offering the efficiency of hardcoded systems. This is a thin line to balance since you can easily get carried away when you think about having hundreds of Agent's on the screen and all of them walking around processing formulas for 10+ Character Attributes which all contain base/min/max/aff/current values and stacks of modifiers all being calculated on the fly and managed by handler components.
The design choices made we deliberate, and not without their own caveats. For example, since we use an enum to dictate the attributes available, and the attribute values are stored in an array - so we can simply cast the enum (int) as the array index and map the data that way. However, while arrays and this type of O(1) lookup is extremely performant at scale, the configurator tool cannot refactor your code when you rename your attributes, which can be annoying, but it highlights that there are tradeoffs to the core architecture choices.
Attributes exist like other Vault data in the Database. They are an AttributePreset
which you can do whatever you want with but the intended function is that you clone the reference data from Vault into the target Agent at runtime.
After your Agent has attributes, it will be able to receive and manage any AttributeModifier
you throw at it. This is done very efficiently with events, and minimal computation is spent on the timers for the modifiers. In practice they're extremely cheap. Like really cheap.
That may change if you decide to extend the system and add more complex calculations, scans, lists etc of course, but basically you can expect that out of the box the Attributes system will be operating at ludicrous speed.
When you boil down the stock Attributes they're not terribly complicated. Like we prefaced, there are a multitude of ways to solve Attribute/Stat implementation but we should always strive to find a balance between complexity, ease of use and comprehension. Lets look at a character with normal characteristics and break down what they're composed of.
The "Level" of a character is not an Attribute, it's simply a value which can be manipulated inside the class and is used as a scalar for calculating Attribute values. Since almost every game with Attributes tracks a Level too, it made sense to make it a variable.
How attributes start
STR: 13
END: 12
WIS: 10
AGI: 11
These are standard growth attributes that players will interact with. When they level up they'll move extra points around to make certain things about their character stronger or weaker. These are the base values of an Attribute. You might think of them as the "root", "core", or maybe even "floor" values.
But what if they're corrupted, you say? How will you math on the original base value? Well, just reference the Preset values instead of the Runtime values. Remember that the original values are always available on the back-end data asset.
However, there's also the issue of Attribute growth per level. Let's now break apart how these Attributes grow.
How attributes grow
STR: 13 : 2.0
END: 12 : 1.0
WIS: 10 : 0.6
AGI: 11 : 0.8
Here we have a second value for use called Affinity - this is how much an Attribute increases every level. With this value we can determine what any Attribute's current value is by passing it across a formula.
Pretty simple when you look at it this way, isn't it? So if STR
(in the stack above) were on a Level 5
character then it would be easily calculated as 13 + (2 * 5) = 23
. This allows us to constantly make the significant and fundamental design assumption that Current values are always formulaic.
This is really important, let's write it again.
Current values are always formulaic.
Great! Moving on.
How attributes change
Now what about modifiers? When Steve The Assassin pokes Bob The Orc with his Knives Of Terrible Poison then Bob should experience a temporary modifier called Poison which reduces his Max Health by 3%.
Here's where design or 'preset' is compartmentalized from runtime. The design portion is shown above and in the database as hard values that get cloned into the runtime values in the Awake()
method.
The runtime values contain one more value called Offset. When we apply modifiers to an Agent it simply increases the target attribute's Offset by x and the formula is updated to add the Offset last, thus accounting for anything affecting the value of an attribute in a formulaic way.
Go ahead and slap them with a rather large trout
The goal of a modifier is to examine at it's own 'configuration', examine it's origin 'caster', examine the target 'victim' and figure out how much numerical effect it has on its target. Once it knows the delta effect value it will apply it to the target and store how much it affected it locally for safe removal later. These work closely with the Offset value in order to create a stable and performant environment.
This distinction allows safe control of base values and modified values with minimal overhead.
What if I want an Attribute to increase health instead of using Endurance?
This is common. Like we mentioned at the start, there are so many different ways to design your implementation and game design around Attributes and their formulas within the gameplay. You may choose to use the ENDURANCE attribute to increase health, for example, but you may also choose to use that strictly for damage resistance while having a separate Attribute for increasing Max Health.
Either way is entirely acceptable and up to you as a game designer. We don't enforce either path in the core source code, although the example project will choose one path and show you how it can be implemented but you're free to design how you see best for your game.
In the end, you can have as many Attributes you want and communicate with them however you wish, using whatever formulas you decide. If you want Banana Resistance, you can have Banana Resistance. Go bananas.
Health, Energy, Experience and the like. These aren't Attributes, they aren't calculated by a formula and are constantly fluctuating. These are what we consider "Vitals". We do have HealthMax
, EnergyMax
, ExperienceMax
, and the like - unless you decide to calculate them from other data-points like Endurance and Wisdom - those Attributes surely do grow with Affinities but the current 'health' and similar values are separated for simplicity. Tie them to any other data you prefer.
You can interact with Vitals in the AgentVitals.cs
If you have questions, feel free to drop by our Discord Channel.