How to Implement a Damage Indication System

Introduction

Hey! Welcome to my first tutorial post.

Whether you are making an RPG game with tons of weapons, items, and enemies to a multiplayer fighting game, a lot of action is taking place, sometimes in quick succession. It is important to let your players understand their current situation and inform them of their performance: a common feature like a Damage Indication System is a perfect one to utilize.

Damage Indication Systems (we will be abbreviating this to DIS) display the amount of damage dealt to certain enemies and/or objects and can sometimes be used to display the amount of health being restored as well. UI text labels pop up on the screen, displaying a number: an example would be “-20” or “20.” Both possess the same purpose of letting players know how they are performing in terms to damage dealt.

This in-depth tutorial goes over how you can implement a DIS into your Roblox game.

:warning: Please consider the following before we get started:

  • This tutorial is classified as Intermediate → Advanced difficulty.

  • I (@theplushedguy3) may explain the methods I used in this tutorial in a different manner than what you would normally interpret it as. Please understand that I have learned Roblox game development differently from you, there may be cases where you would have to restate some information for you to properly understand. If there are any constructive criticisms you would like to inform me of, please leave behind a message on this post or directly message me about it. I appreciate it a lot as I am able to learn from criticism.

  • We will be using a ScreenGui to display the text. If you would like to use BillboardGuis, you will have to rework your way towards achieving that.

  • We will be displaying said text locally. As stated previously, you will have to rework the system if you want the text to display to all players at once.

This is the end of the Introduction.


:toolbox: Step 1 / Setting Up Game Objects

Insert the following objects into ReplicatedStorage: (view the image below)
Hierarchy I

Insert the following objects into StarterGui: (view the image below)
Hierarchy II

Explanations

  • The Remotes folder is my method of organizing my RemoteEvents and RemoteFunctions. You may change this hierarchy to fit your game if need be.

  • The Indicators folder is where all of the cloned text labels will be parented. This keeps the hierarchy of the screen GUI less cluttered.

  • The Sounds folder contains hitmarker sounds that will play when a certain numericType is being referenced. Feel free to use any sound. Adding these objects to your game is optional, you may disregard adding these objects. Furthermore, this part of the tutorial is explained more thoroughly in Step 3.

  • The Template text label is the object that will display the text. We will create clones of this object whenever the Remotes remote event is fired towards the client. Feel free to customize this text label to fit your game.

    • The Shadow text label helps its parent object visually stand out against all the other UI elements on the screen.
  • The Settings module script will contain information that will be used to display and format the Template text label. This part of the tutorial is explained more thoroughly in Step 3.

This is the end of Step 1.


:scroll: Step 2 / Scripting Server-Side Logic

:warning: This part of the tutorial does not cover how to send information all from one server-side script. If you would like to assist with this part of the tutorial, please consider leaving me a message stating your method of execution.

Continuing from the warning above, I obviously do not know what your scripts look like. I can however tell you where the following code should be placed inside of your server-side scripts that code the logic of inflicting damage.

Referencing

Add the following code to where you normally define your variables in your script(s):

local RS = game:GetService("ReplicatedStorage") -- Skip this line if you have already defined access to ReplicatedStorage.
    local remotes = RS:WaitForChild("Remotes", 30) -- Getting the Remotes folder that contains the RemoteEvent needed to send clients information.
        local numericText = remotes:WaitForChild("NumericText", 30) -- Defines access to use the RemoteEvent.

Sending the Client Data

Add the following code to where you define the logic for inflicting damage (in the same script):

numericText:FireClient(player, -- The player (client) to which to fire the remote event to.
    CFrame, -- The CFrame from where to draw the text. It is recommended to use the enemy/object's current CFrame in the Workspace for this parameter.
    number, -- The number the text label should display as. It is recommended to define this as a variable prior to firing the RemoteEvent.
    numericType -- This parameter is what I use to show what the displayed number is for. This will be explained more thoroughly in Step 3.
)

An example of where to add the code above and some tips:

local damage = 20
local enemyHumanoid = part.Parent:FindFirstChildWhichIsA("Humanoid")

if enemyHumanoid and enemyHumanoid.Health > 0 then
    enemyHumanoid:TakeDamage(damage)
    --------------------------------
    numericText:FireClient(player,
        enemyHumanoid.RootPart.CFrame, -- Use the enemy/object's current CFrame in the Workspace for this parameter.
        damage, -- Define this parameter as a variable prior to firing the RemoteEvent.
        "-0"
    )
end

:bangbang: A reminder to add the lines of code above to every single server-side script that codes the logic of inflicting damage.

Explanations

  • I recommend you define the parameters as variables prior to firing because of efficiency and convenience. You do not want to keep changing the parameters every time you make a change.

This is the end of Step 2.


:page_with_curl: Step 3 / Scripting Client-Side Logic

:warning: This step of the tutorial is quite lengthy. This step has been divided into sub-steps to help ease your way through.

Head over to your NumericText screen GUI. Continue with the sub-steps below.

Step 3.1

Setting Up the Settings ModuleScript

Step 3.1 deals with writing pre-defined settings to be used by our local script. We will reference said settings later as variables.

Write the following code into the Settings ModuleScript:

local module = {
    --//Numbers\\--
	Offset = 20; --// Displaces each text label away from the exact on-screen position (pixels).
	FadeAmountX = 0; --// How far the text labels tween out during fade out in pixels (horizontally (left/right)).
	FadeAmountY = 8; --// How far the text labels tween out during fade out in pixels (vertically (up/down)).
	FadeOutDuration = 0.4; --// How long each text label takes for before fading out (TWEENING).
	FadeInDuration = 0.15; --// How long each text label takes to fade in (TWEENING).
	FadeDelay = 0.25; --// How long each text label takes before the fade out phase.

    --//Animation\\--
	FadeOutTween = Enum.EasingStyle.Quart; --// Selects the easing style; how each text label is animated during fade out.
	FadeInTween = Enum.EasingStyle.Linear; --// Selects the easing style; how each text label is animated during fade in.

    --//Tables\\--
	["Colors"] = { --// Composes entries which determine the color of the text labels according to the referenced "numericType".
		["1"] = Color3.fromRGB(255, 0, 0), -- Damage has been inflicted.
		["2"] = Color3.fromRGB(150, 0, 255), -- Damage has been received.
		["3"] = Color3.fromRGB(255, 200, 0), -- Damage inflicted was critical.
		["4"] = Color3.fromRGB(0, 255, 0), -- HP was restored.
		["5"] = Color3.fromRGB(255, 255, 255), --Neutral, the "numericType" parameter was not defined.
	};
	
	["Sizes"] = { --// Composes entries which determine the size of the text labels according to the referenced "numericType".
		Normal = UDim2.new(0.12, 0, 0.016, 4),
		Critical = UDim2.new(0.12, 0, 0.020, 6),
		Critical2 = UDim2.new(0.12, 0, 0.024, 8),
	};
}

return module

Explanations

  • The Settings module script is fully customizable. Feel free to alter these settings to fit the theme of your game.

  • You may have seen references of this parameter called numericType. numericType helps us organize our text labels by color coding and resizing the labels when being displayed. For example, if the damage dealt was critical, the code will format the text label to be larger and have a brighter color to emphasize. (view example images below)

    • The Colors table will be used to change the color of the text labels. You must remember to label which color is associated with the referenced numericType.

    • The Sizes table will be used to change the size of the text labels. Be sure to test out different sizes before you defined them.

    • These two tables can be used interchangably, just remember to label them with a referenced numericType.

    • You can add the following comments to the top of the module script to use as a legend for numericType: (you may likely not utilize all of these parameters)

--[[ 

Numeric Types Legend:
"-0" / Damage has been dealt, plays sound.
"-1" / Critical damage has been dealt, plays sound.
"-2" / Damage has been dealt, does not play sound.
"-3" / Critical damage has been dealt, does not play sound.
"-4" / Damage has been received.
"+0" / HP has been restored.
"_0" or "" / Neutral, does nothing but display text.

]]--

IMAGES: Damage Display Examples

Normal Damage Dealt
Normal Damage Dealt
Critical Damage Dealt
Critical Damage Dealt

This is the end of Step 3.1.

Step 3.2

Coding the Numeric_Text_Manager LocalScript

Step 3.2 will be the main step of this tutorial. This step covers the logic of displaying the text labels to the player, the main idea of DIS.

:bangbang: For this tutorial, we will be using the Camera object to convert the CFrame parameter into an on-screen position (in pixels) and positioning the labels to said on-screen position.

First in this step, we need to define our variables. Write the following code into your Numeric_Text_Manager local script:

--//Variables\\--
local RS = game:GetService("ReplicatedStorage")
	local remotes = RS:WaitForChild("Remotes", 30)
		local numericText = remotes:WaitForChild("NumericText", 30)

-- The lines above are the same to the lines defining access to these objects in our server-side scripts.

local scriptsFolder = script.Parent
    local Settings = require(scriptsFolder:WaitForChild("Settings", 30)) -- The "Settings" module script we worked on earlier.

    local gui = scriptsFolder.Parent
        local sounds = gui:WaitForChild("Sounds", 30) --// Comment/delete this line if you wish to.	

	local indicators = gui:WaitForChild("Indicators", 30)
	local template = gui:WaitForChild("Template", 30)

local currentCamera = workspace.CurrentCamera

--//Pre-connections
local maxX = (gui.AbsoluteSize.X / 6)
local maxY = (gui.AbsoluteSize.Y / 6)

-- Built-in functions as variables.
local tonum = tonumber
local tostr = tostring
local ceil = math.ceil
local random = math.random
local clamp = math.clamp

local uD2 = UDim2
local enum = Enum
local pca = pcall

-- Accessing settings from the module script.
local offset = Settings.Offset
local fadeDelay = Settings.FadeDelay
local fadeAmountX, fadeAmountY = Settings.FadeAmountX, Settings.FadeAmountY
local fadeOutDuration, fadeOutTween = Settings.FadeOutDuration, Settings.FadeOutTween
local fadeInDuration, fadeInTween = Settings.FadeInDuration, Settings.FadeInTween

local fadeRate = Settings.FadeRate

local normalSize, criticalSize, criticalSize2 = Settings.Sizes.Normal, Settings.Sizes.Critical, Settings.Sizes.Critical2

local color1, color2, color3, color4, color5 = Settings.Colors["1"], Settings.Colors["2"], Settings.Colors["3"], Settings.Colors["4"], Settings.Colors["5"]

Next, we need to define our custom functions. Write the following code below the lines above:

--//Custom Functions\\--
function ClampPosition(CF)
	local screenPosition = currentCamera:WorldToViewportPoint(CF.Position)
	local indicatorX = screenPosition.X
	local indicatorY = screenPosition.Y
	
	indicatorX = clamp(indicatorX, maxX, (gui.AbsoluteSize.X - maxX))
	indicatorY = clamp(indicatorY, maxY, (gui.AbsoluteSize.Y - maxY))
	
	return indicatorX, indicatorY
end

--// Comment/delete this function if you wish to.
function PlayHitSound(numericType)
	local soundName = nil
	
	if numericType == "-1" then --// Critical damage
		soundName = "Critical" 
	elseif numericType == "-0" then --// Damage dealt
		soundName = "Hit"
	end
	
	pca(function()
		if soundName then
			sounds[soundName]:Play()
		end
	end)
end

function Customize(element, number, numericType)
	local displayingNumber = ceil(number)

	if numericType == "-0" or numericType == "-2" then --// Damage dealt
		element.TextColor3 = color1
	elseif numericType == "-1" or numericType == "-3" then --// Critical damage
		element.TextColor3 = color3
	elseif numericType == "-4" then --// Damage received
		element.TextColor3 = color2
	elseif numericType == "+0" then --// HP restored
		displayingNumber = "+" .. displayingNumber
		element.TextColor3 = color4
    else --// Neutral
        element.TextColor3 = color5      
	end
	
	element.Text = tostr(displayingNumber)		
		
	--//Display number on shadow text.
	element.Shadow.Text = element.Text
end

function Fade(element)
	element:TweenSizeAndPosition(element.Size, element.Position + uD2.fromOffset(fadeAmountX, -fadeAmountY), enum.EasingDirection.In, fadeOutTween, fadeOutDuration, true, function()
		element:Destroy()
	end)
end

function Animate(element, numericType)	
	local tweenSize = normalSize --// Dealing damage
	if numericType == "-1" or numericType == "-3" then --// Critical damage
		tweenSize = criticalSize
	elseif numericType == "-4" then	--// Taking damage		
		tweenSize = criticalSize2
	end
	
	element:TweenSize(tweenSize, enum.EasingDirection.Out, fadeInTween, fadeInDuration, true)
end

function CreateIndicator(CF, number, numericType)
	--// Clamp position.
	local indicatorX, indicatorY = ClampPosition(CF)
	
	--// Set up clone.
	local numberIndicator = template:Clone()
	numberIndicator.Name = "Indicator"
	numberIndicator.Visible = true	
		
	numberIndicator.Position = uD2.new(0, indicatorX + random(-offset, offset), 0, (indicatorY + random(-offset, offset)))
	numberIndicator.Parent = indicators
		
	--// Customize text label according to "numericType."
	Customize(numberIndicator, number, numericType)
		
	--// Animate text label acccording to "numericType" and play sound.
	Animate(numberIndicator, numericType)
	PlayHitSound(numericType) --// Comment/delete this line if you wish to.
	
	if fadeDelay > 0 then wait(fadeDelay) end -- If "fadeDelay" was less than or equal to zero, the label would instantly fade.
	Fade(numberIndicator)
end

Finally, we need a connection to when the remote event has been fired. Add the last lines of code below the lines of code above:

--//Connections\\--
numericText.OnClientEvent:Connect(CreateIndicator)
math.randomseed(tick())

Explanations (in order of lines of code)

  • The maxX and maxY variables help the text labels to display inside the boundaries of the player’s screen. It would be pointless to display the text labels off-screen since the player cannot see them.

  • The built-in functions that are defined as variables help the script to execute code slightly faster since we defined access to them locally.

  • Using currentCamera:WorldToViewportPoint will account for the GUI inset, which ultimately places the text labels in the correct place when testing.

  • The Fade function does not work like the function sounds. It will only tween the position of the text label, no fading of the text involved. The text will instantly vanish once the tween has finished. You will have to implement text fading on your own accord to achieve this effect.

  • The Animate function animates the text onto the screen with Fade following up.

  • When positioning the text label, we use the offset variable to add some variety in displaying the text. You can set this setting to 0 if you would like to disregard this effect.

  • The math.randomseed(tick()) line just makes sure the offset positions are actually random. Without this line, you may start to see that said offset positions are not so random.

This is the end of Step 3.2.

Step 3.3

Implementing Hitmarker Sounds

:bangbang: This is an optional step. If you do not want to play sounds with this system, you may disregard this step.

Not only are visual queues important for players, audio queues also have a significance! For comparison, many shooter games have “hitmarker sounds” that inform the player their attacks have connected with their target. The type of sound being played also need to be accounted for.

:bangbang: For a better example, view the final product video in Step 4 with audio on.

Navigate your way to the Sounds folder in the screen GUI.
I recommended the following sound IDs below. You may use your own audio if you would like to.

"rbxassetid://160432334" --// Normal hit.
"rbxassetid://208341701" --// Critical hit.

This is the end of Step 3.3.

This is the end of Step 3.


:gear: Step 4 / Test Out Newly Implemented DIS

It is time to test out the DIS. This is a very crucial step in making sure this system is compatible with your game.

If done correctly, the final product should be similar as shown in this YouTube video.

:bangbang: Remember to conduct more tests if you are unsure of the results.

This is the end of Step 4.


Conclusion / Final Thoughts

Congratulations! You have reached the conclusion of this tutorial. :confetti_ball: :partying_face:

You have now learned how to implement a DIS into your game, fully compatible with any game you want to work on. Go ahead and continue to customize this system to fit the theme of your game. Another bonus is that this system does not need to be a DIS as you can modify this to display whatever you wish!

Thank you for taking the time to read through this post. Like I mentioned before, this is my first time posting a tutorial for Roblox game developement. I was quite surprised by the fact no one else here on the Forums have made a tutorial on this topic, so I decided to take matters into my own hands.

:bangbang: Do not forget to leave feedback on this post! I would highly appreciate it if you do. :grinning:

With that, I hope you enjoyed this tutorial.

Once again, thank you for your time.
-theplushedguy3

POLL: On a scale of 1-5, how would you rate the quality of this tutorial?

  • 1
  • 2
  • 3
  • 4
  • 5

0 voters

165 Likes

Are you up for publishing the place uncopylocked so that people can take it apart instead of following the tutorial?

8 Likes

I think OP would recommend you follow the tutorial as it is both useful learning wise, and for future cases that may be similar

6 Likes

I’m sure there enough people who are familiar enough with programming to script a system like this themselves. However perhaps some would like to cut some work by taking this system and editing it.

I would guess there are enough of these people to warrant posting an uncopylocked version to save us the work of having to copy and paste each segment.

Is it not better to provide your readers with more options?

11 Likes

This was a long, descriptive post. It was well explained as well! :slightly_smiling_face:

5 Likes

Hey!

I totally understand where you are coming from. These long tutorials are a bit overwhelming to follow. However, I want to politely disagree.

Like @RealNoobBot stated, I want my readers to learn new things from this tutorial. If I do provide an uncopylocked place to download and edit, I would have wasted time writing this tutorial with actual in-depth information. I took from my own experience and elaborated on it so that people will be able to understand.

So, for now, I will withdraw from created said uncopylocked place and posting the link to it here. I am, however, grateful that you shared your concerns.

Thank you very much for reading. :slightly_smiling_face:

10 Likes

I agree. If anyone could just grab the uncopylocked place and mash it onto their own place, it would just act like a free model, not a learning experience. Even experienced scripters should follow the tutorial. If they’re experienced, they can tweak things as they go along to make their own, personalized system, that works well with the game they’re making!

3 Likes

Some people don’t have the time nor the will to learn, it may be best for you to respect their choices.

Even if you provide the Uncopylocked place if they want to learn they would proceed to do so

Our time is very limited especially for Game Development.

I strongly advise you to put out an Uncopylocked place to provide more value to the readers, as you written this article for them is it not?

I don’t believe that code that has been written once should be written twice, it’s a waste of time and inefficient.

However the choice is yours, I’m only suggesting what I think is right and what I would do.


12 Likes

Nobody’s disrespecting your choice here. It’s not the duty of OP to make sure that games get published on time and yadda yadda. They contributed to the community by providing a tutorial that people can learn from. Why tell them that they are wrong because they didn’t do more? Why tell them that they are wrong because they didn’t make it easier for you to do less?

Many beginner programmers don’t know how to learn from a script just by looking at it. By offering a step-by-step tutorial, they can learn how it works and why it works, rather than just staring at a bunch of code.

You could say this for any tutorial. Using your argument, any tutorial is wasting the readers’ time as they should instead provide a place with a bunch of scripts in it and say “good luck”.

Sure, it would be cool to have an uncopylocked place or a Free Model to drop in and call it done. But that’s not the learning experience OP wanted to foster by posting this tutorial here.

I wouldn’t put the blame on OP like you’ve done here. It’s not their fault if you want to drop something into your place. That’s what Free Models are for, and this is not a Free Model.

10 Likes

I am very sorry you feel this way, but I still stand by what I had previously stated.

If you are not sure how the final product will turn out to be, I have provided a video link that demonstrates it in Step 4. That is a great resource to utilize, visually seeing the final product would lock in what you are expecting to be rewarded when following the tutorial. I am glad you have shared your concerns.

Thank you, again, for reading. :slightly_smiling_face:

7 Likes

I’d argue that the purpose of this category is for tutorials, not to post free models. OP could’ve just made a post on resources or made it a free model, instead, he chose to teach so users could learn from the methods and possibly implement this in a different way in the future.

Some people don’t have the time nor the will to learn, it may be best for you to respect their choices.

I agree with this, but if OP would’ve given an uncopylocked place, then the purpose of a tutorial is pretty useless, but hey, that’s just me! :man_shrugging:

and as @TerrodactyI said, it’s a learning experience which comes with any tutorial. I’m sure if someone didn’t understand the tutorial - or some parts of it then they would reach out to OP and ask questions

10 Likes

Thank you. I have tried numerous free models and tutorials on youtube and this one did the trick.
I was able to implement it into my server script that controls all guns as well as each individual sword.
I think it really helps make a player feel like they progress when they see bigger dps numbers as they play.
One thing I was having trouble with is the color of the numbers is not changing. I have it set to red now by the text itself, but when healing it will say +X but the number will not change to green.

Thank you for such a well made and articulate guide. It also helped me understand remote events better.

3 Likes

Thank you for taking the time to go through this tutorial. I am beyond grateful and happy for you that this tutorial helped out with your knowledge of Roblox development.

2 Likes

Thank you so much for this tutorial, it’s really useful.

3 Likes

First of all, thanks for the tutorial, but I’ve run into a little problem.
I have an issue with this bit of code here

evidence

The output says I’m subtracting a number by a table, and since maxX and maxY are numbers, I assume that gui.AbsoluteSize.X and gui.AbsoluteSize.Y references a table?
evidence2

2 Likes

Actually the order of the error matters so its not the absolute size or the indicator size but your maxX and maxY
just do print(type(maxX),type(maxY) ) to see which ones a table then just do
for i, v in pairs(TheOneThatsATable) do
print(i,v)
end
then get the info you need from there

3 Likes

Oh, I actually just made a silly mistake, I encased the AbsoluteSize in squiggly brackets {} instead of these (). Also, I didn’t realize you could print the type of value, thanks for teaching me something new :slight_smile:

2 Likes

Hi, im having some trouble with the UI part, are you able to share it as a model? (I read the earlier comments, I dont mean you sharing everything, just the Template and Shadow textlabels!)

Somehow my gunfire sound is overriding the hit sounds, the way I found out is to lower the fire sound volume, are there any ways that I can make the sounds not override each other while remaining the volume they are?

Hey!

I’m sorry that your sounds are overriding and mingling with each other. I’ve run into that problem myself actually, but I’ve yet to find a solution for said issue.

The best answer that I’m currently able to provide is to experiment with your sounds’ volumes and adjust them according to your game. Maybe try lowering the gunfire sounds and slightly raising the hitmarker sounds, play test, and determine if that helps solve the issue. Keep experimenting until you’re satisfied with your sounds.

If you’d rather not deal with the hitmarker sounds, just omit them entirely! Best to not linger on the issue for too long, otherwise you’d impede progress on other aspects of your game.

Again, I sincerely apologize for not giving you the best and professional answer. I haven’t really gotten that far in terms of sound design (as you can clearly tell, I’m no sound designer either!).

Good luck with your development endeavors!

1 Like