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 System
s (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.
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 useBillboardGui
s, 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.
Step 1 / Setting Up Game Objects
Insert the following objects into ReplicatedStorage
: (view the image below)
Insert the following objects into StarterGui
: (view the image below)
Explanations
-
The
Remotes
folder is my method of organizing myRemoteEvents
andRemoteFunctions
. 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 certainnumericType
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 theRemotes
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
-
The
Settings
module script will contain information that will be used to display and format theTemplate
text label. This part of the tutorial is explained more thoroughly in Step 3.
This is the end of Step 1.
Step 2 / Scripting Server-Side Logic
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
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.
Step 3 / Scripting Client-Side Logic
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 referencednumericType
. -
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
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
.
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
andmaxY
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 withFade
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 to0
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
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.
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.
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.
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.
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.
Do not forget to leave feedback on this post! I would highly appreciate it if you do.
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