Why the players are so confused in my game?

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 :sweat_smile:

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

I added bots

3 Likes