Hello !
I’m currently working on a game called Tanks and Planes.
I’ve already added a tutorial to help players understand how the game works. However, I’ve noticed that many new players still get confused when they join they don’t realize they need to go to a spawner to get a tank and start playing even if there is a beam with a message box guiding them, so they end up quitting early.
Do you have any tips or ideas on how I can make sure players better understand the goal of the game right from the start?
Here’s the game link if you want to check it out:
https://www.roblox.com/fr/games/119637200038904/BETA-Tanks-And-Planes
Thank you !
1 Like
Make the beam as obvious as possible.
I quickly played, and honestly I didn’t find much wrong with the tutorial. Just remember that some players will be extremely clueless so you have to make arrows and tutorial text as obvious as possible. You may want to consider having text that types itself out instead of appearing at once to guide the user’s eyes to the tutorial text if that makes sense.
The only other issue I found was the the “red highlighted” objects were nowhere near obvious enough. The red just blends into the colour so it looks red-tinted, not highlighted in any way.
2 Likes
Thank you for your reply, i’ve made the message box bigger, and made the highlight more red (without outline because it look really bad with it) and there was already a typing effect
1 Like
Roblox has analytics systems built in.
While I hate analytics, you should do the following:
Add an analytics event to every action they do during the tutorial.
Also add events for what they do for a while after they finish the tutorial. i.e. if they stop playing the game immediately, get lost, whatever
The immediate popup to favorite the game and invite friends is also incredibly annoying.
When the UI is opened, there are also no arrows pointing to what to click. The player is spawned far away from the pad itself so its a far walk and they must turn around, the objects are not properly explained (add something pointing to it more)
The barriers on the bridge have FAR too much health. It is incredibly tedious to destroy them.
There are no players in the game, nor are there bots, so there is nobody to actually fight.
Analytics are how all the popular games have decided on the steps of their tutorials. This is not exclusive to big studios though. It can help even small devs figure out issues with their UX.
2 Likes
Your game is also very poorly optimized. Many getdescendants calls and FPS seems to drop over time.
Trust me, this will become an issue as your game grows. GetDescendants is not meant for this kind of thing.
While I am not able to view your code, I can assume it will crash mobile players due to high memory and CPU usage, and that also affects the algorithm so less players will see your game (which is honestly very well made imo)
As I wrote this, the getdescendants calls went from 6ms of frametime to 26ms of frametime. It will become unplayable over time. Reply to me if you want me to review your code or PM me and we can talk on a platform like discord (or via pms) about it.
I have decided to sit in the game for a long time and will update this with my framerate in about 15 minutes with a 2080ti 11gb and ryzen 7 5800x
Edit: it dropped to 11
3 Likes
Thank you for your reply ! I will apply your tips, here is my Highlight script if there is any way to optimise it and how are you getting all these information on the screen ?
The local script :
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local MAX_HIGHLIGHTS = 31
RunService.RenderStepped:Connect(function()
local targets = {}
for _, Item in pairs(workspace:GetDescendants()) do
if Item:IsA("Model") then
local TeamColor = Item:FindFirstChild("TeamColor")
if TeamColor and TeamColor.Value ~= Player.TeamColor then
local Primary = Item.PrimaryPart or Item:FindFirstChild("HumanoidRootPart") or Item:FindFirstChildWhichIsA("BasePart")
if Primary then
local distance = (Primary.Position - Player.Character.PrimaryPart.Position).Magnitude
table.insert(targets, {model = Item, dist = distance})
end
end
end
end
table.sort(targets, function(a, b)
return a.dist < b.dist
end)
for _, Item in pairs(workspace:GetDescendants()) do
if Item:IsA("Model") then
local Highlight = Item:FindFirstChild("Highlight")
if Highlight then
Highlight:Destroy()
end
end
end
for i = 1, math.min(MAX_HIGHLIGHTS, #targets) do
local target = targets[i].model
if not target:FindFirstChild("Highlight") then
local NewHighlight = Instance.new("Highlight")
NewHighlight.Parent = target
NewHighlight.FillTransparency = 0.7
NewHighlight.OutlineTransparency = 1
end
end
end)
Firstly: As for how I’m getting this info, do CTRL+SHIFT+F6 ingame to open this menu, then press ctrl+p to pause it at a frame and you can observe the usage from scripts
Put the objects to highlight in a folder rather than them being descendants of workspace. Then, use getchildren on that folder to grab all the items.
An even better way to do this is to use CollectionService and tag every object that should be highlighted. This way, you can even remove the getchildren call.
Combine the calls all into one
Also, you can make this run less frequently. Highlights do not require to be updated every frame. Thus, you could do something like the following (with most of these changes, aside from using collectionservice)
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local MAX_HIGHLIGHTS = 31
task.spawn(function()
while task.wait(1/15) do -- 15fps
local targets = {}
for _, Item in pairs(workspace:GetDescendants()) do
if Item:IsA("Model") then
local TeamColor = Item:FindFirstChild("TeamColor")
if TeamColor and TeamColor.Value ~= Player.TeamColor then
local Primary = Item.PrimaryPart or Item:FindFirstChild("HumanoidRootPart") or Item:FindFirstChildWhichIsA("BasePart")
if Primary then
local distance = (Primary.Position - Player.Character.PrimaryPart.Position).Magnitude
table.insert(targets, {model = Item, dist = distance})
end
end
if Item:FindFirstChild("Highlight") then
Item.Highlight:Destroy()
end
end
end
table.sort(targets, function(a, b)
return a.dist < b.dist
end)
for i = 1, math.min(MAX_HIGHLIGHTS, #targets) do
local target = targets[i].model
if not target:FindFirstChild("Highlight") then
local NewHighlight = Instance.new("Highlight")
NewHighlight.Parent = target
NewHighlight.FillTransparency = 0.7
NewHighlight.OutlineTransparency = 1
end
end
table.clear(targets) -- Instantly clears the table after it's done looping to micro-optimize memory and stuff yeash
end
end)
Your beams script is also very unoptimized, so I’d like to see it too
I’d also like to mention, there are probably instances being created repeatedly and not being destroyed after they’re done being used
Ok thank you again for your time !
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Teams = game:GetService("Teams")
local Player = Players.LocalPlayer
local SovietUnionTeam = Teams:WaitForChild("Soviet Union")
local USATeam = Teams:WaitForChild("USA")
local SovietUnionBase = workspace:WaitForChild("Map"):WaitForChild("Bases"):WaitForChild("SovietUnionBase"):WaitForChild("Base"):WaitForChild("Base")
local USABase = workspace:WaitForChild("Map"):WaitForChild("Bases"):WaitForChild("USABase"):WaitForChild("Base"):WaitForChild("Base")
RunService.Heartbeat:Connect(function()
local Spawners = {}
local Player = game.Players.LocalPlayer
if not Player or not Player.Character or not Player.Character:FindFirstChild("HumanoidRootPart") then return end
local HumanoidRootPart = Player.Character:WaitForChild("HumanoidRootPart")
local Humanoid = Player.Character:WaitForChild("Humanoid")
local Attachment0 = HumanoidRootPart:FindFirstChild("Attachement")
if Attachment0 == nil then
Attachment0 = Instance.new("Attachment", HumanoidRootPart)
end
local PlayerPos = Player.Character.HumanoidRootPart.Position
local Descendants = workspace:GetDescendants()
for _, Item in pairs(Descendants) do
if Item.Name == "Spawner" then
local TeamColor = Item.Parent.Parent:FindFirstChild("TeamColor")
if TeamColor and TeamColor.Value ~= Player.Team then
table.insert(Spawners, Item)
end
end
end
local ClosestSpawner = nil
local ClosestDistance = math.huge
for _, Spawner in pairs(Spawners) do
local Dist = (Spawner.Position - PlayerPos).Magnitude
if Dist < ClosestDistance then
ClosestDistance = Dist
ClosestSpawner = Spawner
end
end
if ClosestSpawner then
local Attachment1
if Humanoid.SeatPart == nil then
Attachment1 = ClosestSpawner:FindFirstChild("Attachement")
if Attachment1 == nil then
Attachment1 = Instance.new("Attachment", ClosestSpawner)
end
else
local Beam = Player.Character:FindFirstChild("Beam")
if Beam ~= nil then
Beam:Destroy()
end
end
local Beam = Player.Character:FindFirstChild("Beam")
if Beam == nil then
Beam = Instance.new("Beam")
Beam.Attachment0 = Attachment0
Beam.Attachment1 = Attachment1
Beam.Texture = "rbxassetid://16848361091"
Beam.TextureSpeed = 1
Beam.Width0 = 3
Beam.Width1 = 3
Beam.TextureLength = 3
Beam.TextureMode = Enum.TextureMode.Wrap
Beam.FaceCamera = true
Beam.Parent = Player.Character
else
if Beam.Attachment1.WorldPosition ~= Attachment1.WorldPosition then
Beam.Attachment0 = Attachment0
Beam.Attachment1 = Attachment1
end
end
end
end)
You made a typo, causing the attachment to repeatedly be created, leading to a memory leak and getdescendants taking FAR more of a CPU hit than it should.
Again, you can use instance tags rather than getdescendants to get all the spawns.
A very, very small optimization is using ipairs instead of pairs when you’re going through instances, because ipairs is very moderately faster (although, if a table includes nil, it will stop the loop immediately when it hits nil)
2 Likes
Thanks you so much ! I would never noticed that ! I will work on that, for now I added an arrow to indicate the purchase button and fixed the beams script with tags etc. I always do this typo because of the french language 
Getting back on track with the players getting confused thing, I sent you a list of analytics events that can help with your tutorial on discord
Also posting it here for clarity so people can see what things could be helpful
User joined game
User fully loaded in with no issues
User walks to spawn pad
User clicks the right arrow on the spawn menu, or clicks the spawn button
User loads in a tank with no issues
User drives the tank
User destroys an object
User passes by the bridge
User shoots gun
User fights another player
User dies to another player or kills the other player
User makes it to enemy spawn
1 Like
My FPS has been improved a LOT, but the highlights seem to be broken rn. FPS also helps with players as people tend to drop off when their FPS goes under 30 on mobile
I’m now getting a smooth 240fps constantly
1 Like