(If you enjoy any of my free tutorials, consider giving my game, Alchementalist, a wishlist on Steam!)

Simple Stats.

So, stats. They’re present in (almost) every game. Health, speed, damage, etc. They are what give your game variation. But what’s the best way to handle them? Naive implementations involve a simple variable, i.e. hp = 10;

This is totally fine if your intention is to only utilise the most basic of functionality from stats. But what happens when you want something more complicated. How about if you want to be able to multiply your speed by 50% for 5 seconds? Sure, you could do something like this:

[At some point when you want to apply the “buff”]

spd *= 1.5;
alarm[0] = 5 * game_get_speed(gamespeed_fps);

[In Alarm[0] Event]

spd /= 1.5;

That’ll work if you’re only ever able to increase speed by 50% and only ever able to have one instance of that “buff” active at any time. Things start getting complicated when you stack them though. If, for instance, you applied the above * 1.5 buff twice, you can’t “stack” two alarms to divide it by 1.5 twice. You can do a bunch of workarounds for this kind of thing, but surely, there has to be a better way.

Well, indeed there is. I’m going to introduce you to a method for handling statistics that is extremely flexible and relatively simple to implement (as long as you have a good understanding of structs, basically).

First, let’s consider what a statistic is at it’s core. A statistic is a basic number that represents some ‘real world’ value. This could be speed or damage, as we’ve already mentioned, or more esoteric things like poison resistance, or luck. Over the course of the game, it’s likely that the statistic can be altered in some way by having modifiers applied to it. These modifiers can either add or subtract from the value of the statistic, or multiply or divide it by some value. These modifiers could also be permanent, or temporary. So we have our code conditions: Represent a number, and allow the addition of many either multiplicative or additive values to this number, with these additions being easily removable.

So how could we implement something dynamic like this? Well, when we have a number of related values that we want to be grouped, and we might want to perform special functions on them, the first place we should be thinking of is a struct. Of course, it’s entirely possible to use other data structures (like an array) for this, but structs make intuitive sense because of their “understandable in plain english” organisational pattern, alongside their ability to store methods.

Since we are going to potentially want many different statistics, let’s implement a constructor, which we can print statistic structs out from.

///@desc Creates a statistic struct with an initial value of 0
function Statistic() constructor {
	value = 0;
}

This is the most rudimentary form of a statistic constructor we can make. Every time we call new Statistic(), we’ll get a struct outputted that holds a single field: value. value will always hold 0 when the struct is initially created. Let’s assign the struct to some variables:

hp = new Statistic();
spd = new Statistic();

Now we’ve got hp and spd variables that hold a Statistic struct, which holds a field called value which equals 0. If we wanted to read from hp, we’d do it like this:

var _my_hp = hp.value;

Since hp holds a struct, we need to use dot notation to access the field from the struct we are interested in, and the field we are interested in (and the only field that exists) is value. In this circumstance _my_hp will end up holding 0. But this is just adding extra steps for no real benefit, so let’s start getting a little bit more complicated.

The Next Level.

///@desc Creates a statistic, assigning it a value (or 0 if no value is provided)
///@param {Real} _value The initial number to set value to
function Statistic(_value = 0) constructor {
	value = _value;
	static GetValue = function() {
		return value;
	}
	
	static SetValue = function(_value) {
		value = _value;
	}
}

This is a little bit better. We’ve added a “getter and setter” to the constructor, and we’ve also allowed the coder to alter what number value holds when the struct is created through the arguments for the constructor. Let’s have a look at how this might be worked with:

[In the Create Event of the player instance]

hp = new Statistic(100);
spd = new Statistic(2);

[After getting damaged]

hp.SetValue(hp.GetValue() - dmg_amount);

[In the Step Event, to see when the player is dead]

if (hp.GetValue() <= 0) {
	instance_destroy();
}

This seems a little bit more useful, but still, there’s no real advantages compared to simply assign hp a plain number and manipulating it directly. So now I think it’s time to introduce you to modifiers.

Modification Implementation.

A Modifier is another constructor that builds little structs that you can add to Statistics, which automatically modify the value of a Statistic. Let’s have a look at one now:

///@desc Creates a modifier struct that can be applied to a statistic
///@param {Real} _value The value of the modifier
///@param {Real} _math_operation How the value should be applied to the statistic (should be a math_ops enum)
///@param {Real} _duration How long the modifier should last for, leave the argument blank for permanent modifiers 
function Modifier(_value, _math_operation, _duration = -1) constructor {
	value = _value;
	operation = _math_operation;
	duration = _duration;
	applied_stat = noone;
}

There’s a little bit going on here that we’ll need to break down. Firstly, this modifier holds four fields, value, operation, duration and applied_stat. _value and _math_operation are required arguments for the constructor, whereas _duration is optional. We can, of course, add more fields if we wanted to be storing something else in the modifier (and, indeed, we will do so later), but for now, these are enough. applied_stat is always set to noone to begin with, and we’ll assign it a value when the Modifier actually gets applied to a Statistic.

Now, I like to use an enum for the math operations, so let’s set that up

enum math_ops {
	ADD,
	MULTIPLY
}

As you can see, we’ve got two basic operations, either adding or multiplying (which both encompass subtracting and dividing). Now we need to update our Statistic constructor to handle this new modifier thing:

///@desc Creates a statistic, assigning it a value (or 0 if no value is provided)
///@param {Real} _value The initial number to set value to
function Statistic(_value = 0) constructor {
	base_value = _value;
	current_value = _value;
	modifiers = [];
	altered = false;
     
	static AddModifier = function(_mod) {
		// First we shove the Modifier into the modifiers array
		array_push(modifiers, _mod);
		// Now, we assign the applied_stat field of the Modifier to the Statistic it is being given to
		_mod.applied_stat = self;
		// Whenever we add a new Modifier, we set altered to true, so that we know we have to recalculate the current_value
		altered = true;
	}

	static GetValue = function() {
		// If we haven't added any modifiers since the value was last retrieved, we can simply return the current value
		if (!altered) return current_value;
          
		// Otherwise, we have to recalculate the current value, starting with the base value and going through all the modifiers, applying their operation, and then setting current value to the result

		// First we get the base value of the stat
		var _value = base_value;

		// Then we start looping through the modifiers
		for (var i = 0, _num = array_length(modifiers); i < _num; i++) {
			// We retrieve the current modifier
			var _mod = modifiers[i];
      
			// And then we want to check what operation that modifier wants to do, so we'll use a switch statement
			switch (_mod.operation) {
				case math_ops.ADD:
					// If the modifier holds an ADD math_ops enum, we simply add the value to our temporary _value variable
					_value += _mod.value;
				break;
				case math_ops.MULTIPLY:
					// Otherwise if it holds a MULTIPLY math_ops enum, we want to multiply the temporary value by that amount.
					_value *= 1 + _mod.value;
					// Here, I'm choosing to already add 1 to the modifiers value, which allows us to make the value 0.5 if we want to add 50%, and -0.5 if we want to subtract 50%. Without the 1 added here, then adding 50% would need a value of 1.5 and subtracting 50% would need a value of 0.5. I like the symmetry of 0.5 and -0.5 versus 1.5 and 0.5, so that's why I do it this way. 
				break;
			}
		}
    
		// We've done all the needed calculations on _value now, so we set current_value to _value.
		current_value = _value;
		// We set altered back to false, as we know that current_value is up to date right now, so we don't need to repeat the calculation until another modifier gets added.
		altered = false;
		// And we return current_value
		return current_value;
	}
}

Ok, we’ve updated our Statistic struct to hold a fair bit more stuff. Firstly, we’ve now got a base_value and a current_value, instead of simply a value field. We’ve also got a modifiers array, where we’ll store modifiers that have been applied to the Statistic, and we’ve got an altered boolean, which lets us know if we’ve added or removed a Modifier and the current_value needs to be recalculated.

We’ve also added a new method, AddModifier(), which allows us to push a new Modifier into the modifiers array and switch altered to true.

And finally, we’ve updated our GetValue() method. Now, we first check to see if the Statistic has changed, by checking our altered boolean. If it hasn’t, we can simply return the current_value, as we know it’s still valid. This is simply an optimisation technique, but since we might have a lot of statistics being read every frame, it’s a good one to put in prematurely.

If the Statistic has been changed, then we need to recalculate what it’s value really is. We start by setting a temporary _value variable to the base_value of the Statistic. Then we loop through all the modifiers that have been added to the Statistic, either adding to _value or multiplying _value depending on the math operation each Modifier has (there’s a little more detail about the particulars of what’s going on in the code comments). After we’ve gone through all the modifiers, we set current_value to the newly calculated _value, set altered to false, since we know that current_value is now up to date, and finally we return current_value.

The Countdown Begins.

Now, this setup all works fine for permanent Modifiers, but our duration field in the Modifier is currently doing nothing. We’ve got a bunch of options when it comes to dealing with how to perform the countdown, but to keep this all contained to a single script, I’ll use a global struct alongside a time source to keep track of the modifiers with a duration, perform their countdowns and remove them when their duration has ended.

///@ignore
function __TimesourceFunction() {
	// To begin with, we set the scope to the modifier tracker
	with (global.__modifier_tracker) {
		
		// Now we'll first check the size of the modifiers array, to see it it's at 0
		var _modifiers_size = array_length(modifiers);
		if (_modifiers_size <= 0) {
			// If there are no modifiers currently being tracked, then we don't need to keep the timesource running, so we'll pause it
			var _state = time_source_get_state(timesource);
			if (_state != time_source_state_paused) {
				time_source_pause(timesource);
			}
			// And then exit out of the function, seeing as there are no modifiers to loop through
			exit;
		}
		
		// If there are some modifiers, we'll start looping through them, remembering to start at the END of the array and looping backwards to the start, as we might be deleting positions in the array, which will lead to bugs if we loop from the start to the end
		for (var i = _modifiers_size - 1; i >= 0; i--) {
			var _mod = modifiers[i];
			
			// Here we check to see if the modifiers duration is less than or equal to 0, and then decrement the duration by one (using the double negative -- symbols)
			if (_mod.duration-- <= 0) {
				// If it is, we need to remove it from whatever stat is has been applied to, so we set the scope to that state
				with (_mod.applied_stat) {
					// And run the RemoveModifierById function in that stat
					RemoveModifierById(_mod);
				}
				// Then we delete the Modifier struct
				delete _mod;
				// And remove it from the modifier trackers array
				array_delete(modifiers, i, 1);
			}
		}
	}
}

// Here we'll setup the global timesource that the modifier tracker will use, telling it to run the __TimesourceFunction every frame, and repeat indefinitely
__modifier_tracker_timesource = time_source_create(time_source_game, 1, time_source_units_frames, __TimesourceFunction, [], -1);
// And now we start the timesource
time_source_start(__modifier_tracker_timesource);

__modifier_tracker = {
	
	// We want an array to keep track of the modifiers with duration
	modifiers : [],
	
	// We'll just store a reference to the global tracker timesource in the __modifier_tracker struct, this is not strictly necessary, but I think it's a little neater this way
	timesource : global.__modifier_tracker_timesource,
	
	// And we'll make the function that allows us to add modifiers to the modifier tracker
	AddModifier : function(_mod) {
		// First we push the Modifier to the trackers array
		array_push(modifiers, _mod);
		
		// Then we need to check the state of the timesource, as our __TimesourceFunction() can pause the timesource, and if it's paused AND we are adding a new modifier to be tracked, we want to unpause the timesource so it starts running again
		var _timesource_state = time_source_get_state(timesource);
		if (_timesource_state == time_source_state_paused) {
			time_source_resume(timesource);
		}
	}
}

Whoa, there’s quite a bit going on there. Let’s break it down.

Firstly, we are creating a function that our time source will run. Every time the time source counts down to 0 (which will be every frame, because that’s what we’re setting it to do), it will run the __TimesourceFunction function. This function will pause the time source if there are no modifiers being tracked by the __modifier_tracker, otherwise it will countdown the duration of each Modifier being tracked, and, if the duration is less than or equal to 0, it will remove them from the appropriate Statistic (via a RemoveModifierById() function in the Statistic constructor, which we have not added in yet), then it will tell the garbage collector that the Modifier struct is available to be cleaned up via the delete keyword, and finally it will remove the position in the modifiers array in the __modifier_tracker where that newly marked for deletion Modifier struct was held.

After creating that function, we then create the __modifier_tracker_timesource time source. We set it up to trigger every frame, to run the __TimesourceFunction when it triggers and to repeat indefinitely. And we then start it running.

Finally, we create the __modifier_tracker struct. Inside of that struct we have an array to hold the modifiers being tracked, a variable keeping track of the global timesource we are using, and we’ve also got a method called AddModifier(). This method takes the modifier being added as an argument. First it adds the modifier to the modifiers array. Then it checks to see if a time source has been paused (again, this happens if there are no modifiers being tracked, as there is no point in running the timesource every frame if it’s doing nothing), and resumes it if it has been (since we just added a new modifier, we know that it has something to keep track of).

Now we’ll also need to update our Modifier struct to add itself to the tracker if it has a duration.

///@desc Creates a modifier struct that can be applied to a statistic
///@param {Real} _value The value of the modifier
///@param {Real} _math_operation How the value should be applied to the statistic (should be a math_ops enum)
///@param {Real} _duration How long the modifier should last for, leave the argument blank for permanent modifiers 
function Modifier(_value, _math_operation, _duration = -1) constructor {
	value = _value;
	operation = _math_operation;
	duration = _duration;
	// By keeping track of the maximum length of the duration, we can compare it to the duration and figure out what percentage of the duration has been completed, useful if we want to display some sort of indicator for the duration left
	duration_max = _duration;
	applied_stat = noone;
	
	if (duration > 0) {
		// If the modifier has a duration, we want to add it to the modifier tracker so it can be counted down and removed when it's duration is up
		global.__modifier_tracker.AddModifier(self);
	}
}

As you can see, we’ve now added a bit of code that checks to make sure the duration is greater than 0, and if it is, it runs the AddModifier method that’s attached to our modifier tracker, giving itself as the modifier argument. We’ve also added a new field called duration_max, which simply keeps track of what the original duration was before any countdown happened. This lets us work out the percentage time left if we want to, which can be useful sometimes (for instance, if you have an cooldown animation for an icon, you could figure out what frame should be shown by something like var _current_frame = sprite_get_number(spr_cd_anim) * (1 - (duration / duration_max)), as an example), but it is not necessary.

The last thing we need to do is to add our RemoveModifierById() method to our Statistic constructor. We’ll also add a RemoveModifierByPosition() method, to allow us to remove modifiers even when we don’t have their id handy.

///@desc Creates a statistic, assigning it a value (or 0 if no value is provided)
///@param {Real} _value The initial number to set value to
function Statistic(_value) constructor {
	base_value = _value;
	current_value = _value;
	modifiers = [];
	altered = false;
     
	static AddModifier = function(_mod) {
		// First we shove the Modifier into the modifiers array
		array_push(modifiers, _mod);
		// Now, we assign the applied_stat field of the Modifier to the Statistic it is being given to
		_mod.applied_stat = self;
		// Whenever we add a new Modifier, we set altered to true, so that we know we have to recalculate the current_value
		altered = true;
	}
	
	static RemoveModifierById = function(_mod_id) {
		// Since we'll be deleting something from the modifiers array, it's important to loop backwards (not strictly necessary in this case, as we are returning immediately after deletion, but it's important to build the habit)
		for (var i = array_length(modifiers) - 1; i >= 0; i--) {
			// Retrieve the modifier from the array
			var _mod = modifiers[i];
			
			// If our supplied _mod_id is the same as the mod that was retrieved, we can remove it from the array
			if (_mod == _mod_id) {
				array_delete(modifiers, i, 1);
				// Remember to set altered to true, as we've changed what's modifying the statistic
				altered = true;
				// And we'll return true so that we can find out if the removal was successful or not, if we'd like to know that
				return true;
			}
		}
		// If we didn't find the modifier, we'll return false
		return false;
	}
	
	static RemoveModifierByPosition = function(_pos) {
		// Basically the same as removing the modifier by id, except we just need to check whether the position being supplied is within the bounds of the array
		if (_pos < array_length(modifiers)) {
			// If so, delete, set altered to true and return true
			array_delete(modifiers, _pos, 1);
			altered = true;
			return true;
		}
		// Otherwise return false
		return false;
	}

	static GetValue = function() {
		// If we haven't added any modifiers since the value was last retrieved, we can simply return the current value
		if (!altered) return current_value;
          
		// Otherwise, we have to recalculate the current value, starting with the base value and going through all the modifiers, applying their operation, and then setting current value to the result

		// First we get the base value of the stat
		var _value = base_value;

		// Then we start looping through the modifiers
		for (var i = 0, _num = array_length(modifiers); i < _num; i++) {
			// We retrieve the current modifier
			var _mod = modifiers[i];
      
			// And then we want to check what operation that modifier wants to do, so we'll use a switch statement
			switch (_mod.operation) {
				case math_ops.ADD:
					// If the modifier holds an ADD math_ops enum, we simply add the value to our temporary _value variable
					_value += _mod.value;
				break;
				case math_ops.MULTIPLY:
					// Otherwise if it holds a MULTIPLY math_ops enum, we want to multiply the temporary value by that amount.
					_value *= 1 + _mod.value;
					// Here, I'm choosing to already add 1 to the modifiers value, which allows us to make the value 0.5 if we want to add 50%, and -0.5 if we want to subtract 50%. Without the 1 added here, then adding 50% would need a value of 1.5 and subtracting 50% would need a value of 0.5. I like the symmetry of 0.5 and -0.5 versus 1.5 and 0.5, so that's why I do it this way. 
				break;
			}
		}
    
		// We've done all the needed calculations on _value now, so we set current_value to _value.
		current_value = _value;
		// We set altered back to false, as we know that current_value is up to date right now, so we don't need to repeat the calculation until another modifier gets added.
		altered = false;
		// And we return current_value
		return current_value;
	}
}

Ok, so our RemoveModifierById() method takes the id of a Modifier as the _mod_id argument, checks through the modifiers array to see if that Modifier is in the array and deletes if from the array if it’s found. Our RemoveModifierByPosition() does essentially the same, except that it’s checking whether the _pos argument is within the bounds of the array and then deletes that position if it is.

The Big Whole Thing.

As a final sanity check, here is everything that should be in the script for statistics in one codeblock lump:

enum math_ops {
	ADD,
	MULTIPLY
}

///@desc Creates a statistic, assigning it a value (or 0 if no value is provided)
///@param {Real} _value The initial number to set value to
function Statistic(_value) constructor {
	base_value = _value;
	current_value = _value;
	modifiers = [];
	altered = false;
     
	static AddModifier = function(_mod) {
		// First we shove the Modifier into the modifiers array
		array_push(modifiers, _mod);
		// Now, we assign the applied_stat field of the Modifier to the Statistic it is being given to
		_mod.applied_stat = self;
		// Whenever we add a new Modifier, we set altered to true, so that we know we have to recalculate the current_value
		altered = true;
	}
	
	static RemoveModifierById = function(_mod_id) {
		// Since we'll be deleting something from the modifiers array, it's important to loop backwards (not strictly necessary in this case, as we are returning immediately after deletion, but it's important to build the habit)
		for (var i = array_length(modifiers) - 1; i >= 0; i--) {
			// Retrieve the modifier from the array
			var _mod = modifiers[i];
			
			// If our supplied _mod_id is the same as the mod that was retrieved, we can remove it from the array
			if (_mod == _mod_id) {
				array_delete(modifiers, i, 1);
				// Remember to set altered to true, as we've changed what's modifying the statistic
				altered = true;
				// And we'll return true so that we can find out if the removal was successful or not, if we'd like to know that
				return true;
			}
		}
		// If we didn't find the modifier, we'll return false
		return false;
	}
	
	static RemoveModifierByPosition = function(_pos) {
		// Basically the same as removing the modifier by id, except we just need to check whether the position being supplied is within the bounds of the array
		if (_pos < array_length(modifiers)) {
			// If so, delete, set altered to true and return true
			array_delete(modifiers, _pos, 1);
			altered = true;
			return true;
		}
		// Otherwise return false
		return false;
	}

	static GetValue = function() {
		// If we haven't added any modifiers since the value was last retrieved, we can simply return the current value
		if (!altered) return current_value;
          
		// Otherwise, we have to recalculate the current value, starting with the base value and going through all the modifiers, applying their operation, and then setting current value to the result

		// First we get the base value of the stat
		var _value = base_value;

		// Then we start looping through the modifiers
		for (var i = 0, _num = array_length(modifiers); i < _num; i++) {
			// We retrieve the current modifier
			var _mod = modifiers[i];
      
			// And then we want to check what operation that modifier wants to do, so we'll use a switch statement
			switch (_mod.operation) {
				case math_ops.ADD:
					// If the modifier holds an ADD math_ops enum, we simply add the value to our temporary _value variable
					_value += _mod.value;
				break;
				case math_ops.MULTIPLY:
					// Otherwise if it holds a MULTIPLY math_ops enum, we want to multiply the temporary value by that amount.
					_value *= 1 + _mod.value;
					// Here, I'm choosing to already add 1 to the modifiers value, which allows us to make the value 0.5 if we want to add 50%, and -0.5 if we want to subtract 50%. Without the 1 added here, then adding 50% would need a value of 1.5 and subtracting 50% would need a value of 0.5. I like the symmetry of 0.5 and -0.5 versus 1.5 and 0.5, so that's why I do it this way. 
				break;
			}
		}
    
		// We've done all the needed calculations on _value now, so we set current_value to _value.
		current_value = _value;
		// We set altered back to false, as we know that current_value is up to date right now, so we don't need to repeat the calculation until another modifier gets added.
		altered = false;
		// And we return current_value
		return current_value;
	}
}

///@desc Creates a modifier struct that can be applied to a statistic
///@param {Real} _value The value of the modifier
///@param {Real} _math_operation How the value should be applied to the statistic (should be a math_ops enum)
///@param {Real} _duration How long the modifier should last for, leave the argument blank for permanent modifiers 
function Modifier(_value, _math_operation, _duration = -1) constructor {
	value = _value;
	operation = _math_operation;
	duration = _duration;
	// By keeping track of the maximum length of the duration, we can compare it to the duration and figure out what percentage of the duration has been completed, useful if we want to display some sort of indicator for the duration left
	duration_max = _duration;
	applied_stat = noone;
	
	if (duration > 0) {
		// If the modifier has a duration, we want to add it to the modifier tracker so it can be counted down and removed when it's duration is up
		global.__modifier_tracker.AddModifier(self);
	}
}

///@ignore
function __TimesourceFunction() {
	// To begin with, we set the scope to the modifier tracker
	with (global.__modifier_tracker) {
		
		// Now we'll first check the size of the modifiers array, to see it it's at 0
		var _modifiers_size = array_length(modifiers);
		if (_modifiers_size <= 0) {
			// If there are no modifiers currently being tracked, then we don't need to keep the timesource running, so we'll pause it
			var _state = time_source_get_state(timesource);
			if (_state != time_source_state_paused) {
				time_source_pause(timesource);
			}
			// And then exit out of the function, seeing as there are no modifiers to loop through
			exit;
		}
		
		// If there are some modifiers, we'll start looping through them, remembering to start at the END of the array and looping backwards to the start, as we might be deleting positions in the array, which will lead to bugs if we loop from the start to the end
		for (var i = _modifiers_size - 1; i >= 0; i--) {
			var _mod = modifiers[i];
			
			// Here we check to see if the modifiers duration is less than or equal to 0, and then decrement the duration by one (using the double negative -- symbols)
			if (_mod.duration-- <= 0) {
				// If it is, we need to remove it from whatever stat is has been applied to, so we set the scope to that state
				with (_mod.applied_stat) {
					// And run the RemoveModifierById function in that stat
					RemoveModifierById(_mod);
				}
				// Then we delete the Modifier struct
				delete _mod;
				// And remove it from the modifier trackers array
				array_delete(modifiers, i, 1);
			}
		}
	}
}

// Here we'll setup the global timesource that the modifier tracker will use, telling it to run the __TimesourceFunction every frame, and repeat indefinitely
__modifier_tracker_timesource = time_source_create(time_source_game, 1, time_source_units_frames, __TimesourceFunction, [], -1);
// And now we start the timesource
time_source_start(__modifier_tracker_timesource);

__modifier_tracker = {
	
	// We want an array to keep track of the modifiers with duration
	modifiers : [],
	
	// We'll just store a reference to the global tracker timesource in the __modifier_tracker struct, this is not strictly necessary, but I think it's a little neater this way
	timesource : global.__modifier_tracker_timesource,
	
	// And we'll make the function that allows us to add modifiers to the modifier tracker
	AddModifier : function(_mod) {
		// First we push the Modifier to the trackers array
		array_push(modifiers, _mod);
		
		// Then we need to check the state of the timesource, as our __TimesourceFunction() can pause the timesource, and if it's paused AND we are adding a new modifier to be tracked, we want to unpause the timesource so it starts running again
		var _timesource_state = time_source_get_state(timesource);
		if (_timesource_state == time_source_state_paused) {
			time_source_resume(timesource);
		}
	}
}

This is, in essence, everything we need for the stats. It’s a bit of work to get setup, but after that setup is done, it’s very easy to use and allows us a lot of flexibility. I also like to create a local package holding just that script, so I can easily import it into new projects and not having to worry about setting it all up again.

In terms of usage, let’s say I want to create a speed statistic that starts with the value of 1, and then boost that by 50% for 10 seconds.

spd = new Statistic(1);
var _mod = new Modifier(0.5, math_ops.MULTIPLY, game_get_speed(gamespeed_fps) * 10);
spd.AddModifier(_mod);

That’s all we need to do. Now, spd.GetValue() will return the value of 2 for 10 seconds, after which it will revert back to returning 1. We can add as many or as few Modifiers as we like, and they’ll all get properly calculated and applied. If your game had upgrades and the player purchased one, you’d simply apply a permanent modifier:

var _mod = new Modifier(1, math_ops.ADD);
spd.AddModifier(_mod);

Now spd has the value of 1 permanently added to it. You can even find out what the original value was: var _original_value = spd.base_value. I usually store my stats in an array though, instead of simple variables like spd, as it’s easier to loop through them in an array.

To show a more complete example, let’s setup a player with a few different stats and show some modifications:

[Create Event of player object]

// Normally I'd have a dedicated script holding all my enums and macros, but doing it here is fine
enum player_stats {
	HP,
	SPD,
	DMG,
	ARMOUR
}

// Initialise all the stats in the array to a Statistic struct with the appropriate values
stats[player_stats.HP] = new Statistic(100);
stats[player_stats.SPD] = new Statistic(2);
stats[player_stats.DMG] = new Statistic(5);
stats[player_stats.ARMOUR] = new Statistic(8);

[Player gets damaged, probably in a Collision Event with something]

var _dmg = 10; // The amount of health that should be lost
_dmg -= stats[player_stats.ARMOUR].GetValue(); // Subtract the armour from the damage
var _mod = new Modifier(-_dmg, math_ops.ADD); // Create a modifier that subtracts the _dmg
stats[player_stats.HP].AddModifier(_mod); // Add it to the players HP statistic
if (stats[player_stats.HP].GetValue() <= 0) { // Check to see if the players health is equal to or below 0
	instance_destroy(); // Destroy the player if it is
}

[Player picks up a new sword, in a Collision Event with a sword object]

// Assuming we are using visiblity to determine whether the sword object has been picked up or not
if (!other.visible) {
	exit;
}

// This also assumes that the sword object has a list of all the modifiers it will apply
// First we grab the list of modifiers from the current sword
var _current_mods = current_sword.modifiers;
// And remove any currently applied modifiers by comparing id's
for (var i = 0; i < array_length(_current_mods); i++) {
	stats[player_stats.DMG].RemoveModifierById(_current_mods[i]);
}

// Now we grab the list of modifiers from the sword object currently being collided with
var _new_mods = other.modifiers;
for (var i = 0; i < array_length(_new_mods); i++) {
	stats[player_stats.DMG].AddModifier(_new_mods[i]);
}

// Now we destroy the currently held sword
instance_destroy(current_sword);

// And set current_sword to the sword that was collided with
current_sword = other;

// And we set the visibility of the current sword to false in order to make it so we can't collide with it again, as per the beginning of this codeblock
current_sword.visible = false;

[Player uses an item that gives them a buff that applies 50% speed boost, as well as 25% extra damage, but reduces their armour by 15%, with the buff lasting for 5 minutes]

var _duration = game_get_speed(gamespeed_fps) * 60 * 5;
var _spd_mod = new Modifier(0.5, math_ops.MULTIPLY, _duration);
stats[player_stats.SPD].AddModifier(_spd_mod);
var _dmg_mod = new Modifier(0.25, math_ops.MULTIPLY, _duration);
stats[player_stats.DMG].AddModifier(_dmg_mod);
var _arm_mod = new Modifier(-0.15, math_ops.MULTIPLY, _duration);
stats[player_stats.ARMOUR].AddModifier(_arm_mod);

As you can see, it’s very simple to start stacking and manipulating different modifiers to different stats together. I think that’s about enough for the example though.

There are a ton of bells and whistles you can add to this kind of thing. One thing I use is to have a source field for the modifiers. Whenever something adds a modifier to a stat, the modifier stores the source of where it came from. Then when I show the player the stats, I can also show them where each modifier came from. Or I can remove all modifiers that come from a specific source (I have an added RemoveModifierBySource() method in the Statistic struct).

Or you can sort the modifiers array in the Statistic struct by the type of math operator each modifier has. This allows you to do things like have all additions to the stat done before any multiplication, or vice versa. Combining this with more entries to the math_ops enum allows you to have modifiers that are applied on the base value, whereas others are applied on the current value, giving you more flexibility when it comes to different types of modifier interactions in your game.

There’s lots of room for extensibility here, but even without adding anything to it, this stat setup makes managing statistics in your game much easier.

If you’ve enjoyed this tutorial and want to see some of this stuff in action, consider giving a wishlist to my game Alchementalist (or trying out the demo), it’s super helpful for me in regards to the Steam algorithm. I use this stats system as the basis for the extensive spellboost system in that game (such as the Lightning Lord spellboost shown below).