The Basics of Combat Games: Health and Damage

This is the first in a series of posts about different vital parts about games revolving around combat and how to make them. RPGs, Fighting Games, and pretty much anything that isn’t just an FPS game. Of course, you don’t need to use everything in every tutorial, but these will cover just about everything you need.

The reccomended level of experience for this tutorial is intermediate. You should know the basics of scripting and how to use Roblox Studio.

Today’s post is about how to create a health system beyond what the default humanoids in Roblox provide. For this, we will use ModuleScripts.

What is a ModuleScript?
A ModuleScript is a type of script that can be accessed by other scripts. Another script can edit variables inside of a ModuleScript and those changes will be seen by the other scripts. More importantly, functions can be created inside a ModuleScript and be called by other scripts as well, allowing for on-site calculations for editing variables.

Compared to using Attributes or BoolVariables, Modulescripts have the following advantages:

  • Much easier to access the variables.
  • Many more types of variables can be stored, such as tables and newproxy()'s.
  • The previously mentioned functions reduce the need to copy and paste code.

There are two drawbacks though, that being changes on one client/the server do not replicate to the others, and two modulescripts cannot access each other, only a one-way connection is allowed.

The Basics
To keep track of a player’s health and other relevant stats, we will being using a ModuleScript to store all of those values. It should look something like this to start:

--//Let's say the module is named.. StatManager.
local StatManager= {} --//This doesn't have to be the name of the module, but this is what is returned when the module is require()'d
StatManager.Health = 100
StatManager.MaxHealth = 100
StatManager.WalkSpeed = 16
StatManager.JumpPower = 50

return StatManager

For everything we want to be hurt, we insert one of these scripts into the model of the character. Now, I’m going to assume you already have a way to harm the player, so I won’t go into the details of how to make an attack, but I will show you how to require() a module.

--//This should be a serverscript.
local TargetStats = require(PersonWeHit.StatManager)
print(TargetStats.Health) --//100
TargetStats.Health -= 20
print(TargetStats.Health) --//80
--//We will need to use require() again once we no longer reference the module.

Taking Damage Functions
I imagine the game you want to make is a bit more complicated and some calculations will need to be preformed to determine the result of an attack. We will now make a function in the ModuleScript to calculate what to do by simply giving values and letting the module calculate it.

local StatManager= {} --//This doesn't have to be the name of the module, but this is what is returned when the module is require()'d
StatManager.Health = 100
StatManager.MaxHealth = 100
StatManager.WalkSpeed = 16
StatManager.JumpPower = 50
--//Here we shall add a new stat, defense. It reduces damage by it's value minus any armor piercing.
StatManager.Defense = 15
--//Then another, a stat that indicates if a player is blocking. If blocking, any attack that doesn't break block will be ignored.
StatManager.Blocking = false


function StatManager.TakeDamage(AttackInfo)
	--//I prefer to have Damage Variables as a dictionary, making it much easier to add and remove values.
	if StatManager.Blocking == true and not AttackInfo.BlockBreak then return "Blocked" end --//If blocked and not block broken, then the function is terminated here.

	TotalArmor = math.clamp(StatManager.Defence - AttackInfo.Piercing or 0,0,math.huge) --//math.clamp limits how low or high a number can be, preventing us from getting negative total armor. The 'or' means if no Piercing Parameter is given, the 0 will be used instead of giving us an error.
	StatManager.Health = (AttackInfo.Damage - TotalArmor)

	if StatManager.Health <= 0 then
		--//Man I'm dead.
		return "Kill"
	end

	return "Hit" --//return sends a value back to the script that called this function. Perhaps a move will have a lower cooldown if it hits.
end

return StatManager

And we will have to update our attacking script:

--//This should be a serverscript.
local AttackData = {
	Damage = 30;
}
local TargetStats = require(PersonWeHit.StatManager)

TargetStats.TakeDamage(AttackData)
print(TargetStats.Health) --//85

--//Let's give us some piercing.
AttackData.Piercing = 10
TargetStats.TakeDamage(AttackData)
print(TargetStats.Health) --//65

--//And now, we shall be blocked.
TargetStats.Blocking = true
TargetStats.TakeDamage(AttackData)
print(TargetStats.Health) --//65

You can increase the complexity of the TakeDamage function to whatever you need for your game. Maybe add a value that reduces damage by a %, or make a player take bonus damage if their HumanoidRootPart’s position’s X value is higher than their Y. You’ll probably want to make new parameters and functions for knockback.

Stun and Status Effects
Now any fighting game that calls itself one is going to need a stun function, preferably one that allows itself to be interrupted so new stun takes priority over the old. This will require the generation of newproxy()'s, empty but always unique values. Or you could be lame and use a number that’s increased by one.

--//Inside of the StatManager module, duh.
StatManager.StunProxy = newproxy()
StatManager.IsStunned = false

function StatManager.BeStunned(StunTime)
	--//Create a new proxy to let any other running BeStunned() functions know they've been interrupted.
	FreshProxy = newproxy()
	StatManager.StunProxy = FreshProxy
	
	--//Set the player's walkspeed and jumppower here.
	StatManager.Stunned = true

	task.wait(StunTime)
	
	if FreshProxy == StatManager.StunProxy then
		--//If this function hasn't been called again, then it means stun hasn't been interrupted.
		
		--//Anyways, set the player's speed and stats back to normal.
		StatManager.Stunned = false
	end
end

However, there is a better way to write this function to be usable for other statuses. We’ll use a dictionary to reference the effects and current state of a status effect. Another function will also be made to interpret how to apply/remove the effect of the function.

--//Still inside of the StatManager. Also we don't need this dictionary to be sent to requiring scripts.
EffectList = {
	Stun = { --//Name of status
		Info = { --//What the status does and it's stats.
			Immobilize = true; 
			CanInterrupt = true;
			Type = "Delay";
		};
		State = {
			RunProxy = newproxy();
			Active = false;
		};
	};
}
StatManager.Immobilized = false --//For stun.

--[[StatusInfo Example:
	Selected = "Stun";
	Time = 4;
]]
function StatManager.SetStatus(StatusInfo)
	--//Status info is a dictionary like AttackInfo

	--//Figure out how to handle the function.
	if EffectList[StatusInfo.Selected] then
		local ChosenStatus = EffectList[StatusInfo.Selected]
		--//Determine if we can go ahead.
		if (ChosenStatus.Info.CanInterrupt == nil or not ChosenStatus.Info.CanInterrupt == false) or ChosenStatus.State.Active == false then

			--//Set the new states of the status.			
			ChosenStatus.State.Active = true

			FreshProxy = newproxy()
			ChosenStatus.State.RunProxy= FreshProxy 

			--//Now figure out what type of execution method it is.
			if ChosenStatus.Info.Type == "Delay" then
				ApplyEffect(ChosenStatus.Info,true)
				task.wait(StatusInfo.Time)

				if FreshProxy ~= ChosenStatus.State.RunProxy then return end --//Stop if interrupted.

				ApplyEffect(ChosenStatus.Info,false)
				ChosenStatus.Active = false

			elseif ChosenStatus.Info.Type == "other type" then
				--//more functions here
			end
		end
	end
end

--//Heres the function that will figure out how to apply the status effects.
function ApplyEffect(InfoOfStatus,Apply)
	--//There are better ways to do this but this works for the tutorial.
	if InfoOfStatus.Immobilize then
		StatManager.Immobilized = Apply
		if Apply == true then
			--//Set humanoid's walkspeed/jumppower to zero.
		else
			--//Set humanoid's walkspeed/jumpower to StatManager.WalkSpeed/JumpPower
		end
	end
	if InfoOfStatus.Damage and Apply == true then
		StatManager.Health -= InfoOfStatus.Damage
	end
	if InfoOfStatus.Speed then
		--//If unapplying, reverse change.
		StatManager.Health += InfoOfStatus.Speed * (Apply == true and 1 or -1)
	end
end

And just like that, you have the means to add all the character states and status effects any good fighting game will need.

29 Likes

AAAnd… Bookmarked! Most “tutorials” are youtube models that you just drop in and learn nothing from or not optimized so seeing something generally useful even in non combat genres is shocking

5 Likes