Replicating the Pvp Ghost from "The Vast Realm"

I’ve been trying to emulate the pvp ghost from a game called “The Vast Realm”
Here’s a short YouTube video about how it looks:
https://www.youtube.com/watch?v=-LKZ7hvk_Fo

It’s the little blue boxes that follow the player as they move around. Its supposed to show you where you are on the server

The problem with my script is that it keeps making the ghost look very choppy. I’ve tried to Tween the parts of the ghost to the position of the players character and I’ve also tried to set its cframe to the cframe of the character.

Here’s a link to what it currently looks like:
https://gyazo.com/9e98471b5de5080ab9e47f5aa7b7d1a0

!Everything here is ran by the server!

local tweenService = game:GetService("TweenService")
local runserver = game:GetService("RunService")

local Ghosts = {
	
}

runserver.Heartbeat:Connect(function(plr)
	for num1 , GhostTab in pairs(Ghosts) do
		for num2 , GhostPart in pairs(GhostTab.Ghost:GetChildren()) do
			local part = GhostTab.Char:FindFirstChild(GhostPart.Name)
			GhostPart.CFrame = part.CFrame
		end
	end
end)

game.Players.PlayerAdded:Connect(function(plr)
	local ghost = nil
	plr.CharacterAdded:Connect(function(char)
		
		ghost = game.ReplicatedStorage.Copy.Debug.GhostPvpChar:Clone()
		ghost.Parent = game.Workspace
		local gtab = {
			["Ghost"] = ghost;
			["Player"] = plr;
			["Char"] = char
		}
		
		table.insert(Ghosts,gtab)
	end)
end)

Thank you for helping

I recommend using ipairs in GetChildren() and use square brackets instead of :FindFirstChild(). I don’t know if anything changes but I guess it’s worth a try.

I don’t really see any change in performance nor does it make it look smoother. Thanks for trying to help! also what’s the difference between square brackets and :FindFirstChild()? from what I can see they do the same thing.

Why is this being handled on the server, I don’t really see the point in implementing a system like this other than visual effect… It would be better to handle this on the client, that’s me assuming you don’t require other players to see other player boxes…

I’m handling this on the server because, I want the player to see where they are on the server

I’m not going to question it then.
Are you tweening the “GhostPvpChar” Clone to the player?

No, I’m setting the “GhostPvpChar” cframe to the cframe of the players parts, and I’ve already tried tweening and it looked the same.

Right, can I ask you was this how you implemented the tweening?

local tweenService = game:GetService("TweenService")
local tweeninfo = TweenInfo.new(0.5,Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local runserver = game:GetService("RunService")

local Ghosts = {
	
}

runserver.Heartbeat:Connect(function(plr)
	for num1 , GhostTab in pairs(Ghosts) do
		for num2 , GhostPart in pairs(GhostTab.Ghost:GetChildren()) do
			local part = GhostTab.Char:FindFirstChild(GhostPart.Name)
            local goal = {CFrame = part.CFrame}
			local tweeninstance = tweenservice:Create(GhostPart,tweeninfo,goal)
            tweeninstance:Play()
		end
	end
end)

game.Players.PlayerAdded:Connect(function(plr)
	local ghost = nil
	plr.CharacterAdded:Connect(function(char)
		
		ghost = game.ReplicatedStorage.Copy.Debug.GhostPvpChar:Clone()
		ghost.Parent = game.Workspace
		local gtab = {
			["Ghost"] = ghost;
			["Player"] = plr;
			["Char"] = char
		}
		
		table.insert(Ghosts,gtab)
	end)
end)

Yes, that is how I implemented it.

Sorry, if this is a little too much but can I see a small clip of you tweening?
I’m just making sure because tweening is probably the smoothest transition in roblox your going to get…

Here’s the clip with tweening:
https://gyazo.com/2d4e3458d4bcaefdd72c85a6c3c41e5a

Hmmm, strange… This is really because this function here

We gonna have to rewrite this to be more efficient since your having to iterate through every single players Ghost which is very inefficient the way you have logically layed this out give me a second

Can we have a screenshot of the GhostChar you are cloning? More specifically a screenshot of the explorer

image

Instead of having this layed out like this wouldn’t it be best if we took a properly welded rig add the selection boxes, tween ghosts HRP to the plrs HRP then we just take the players animation track via

local animtracks = player.Character.Humanoid:GetPlayingAnimationTracks()

and play it to the ghosts humanoid, this will be less computationally heavy as opposed to what your currently doing

how could I do this without setting up multiple for loops inside of each other just to check if the animation track is already playing

so far I’ve gotten this

runserver.Heartbeat:Connect(function()
	for num1 , GhostTab in pairs(Ghosts) do
		local CharacterTracks = GhostTab.Char.Humanoid:GetPlayingAnimationTracks()
		local GhostTracks = GhostTab.Ghost.AniControl:GetplayingAnimationTracks()
		for i , CharTrack in pairs(CharacterTracks) do
			local animation = CharTrack.Animation
			for i , GhostTrack in pairs(GhostTracks) do
				-- check if Tracks are similar and play if one isn't playing
			end
		end
		
		local goal = {["CFrame"] = GhostTab.Char.HumanoidRootPart.CFrame}
		local tweeninstance = tweenService:Create(GhostTab.Ghost.HumanoidRootPart,TweenInfo.new(.1),goal)
		tweeninstance:Play()
		
	end
end)

Animation track is a table that contains the loaded and recently played animation.
In this case you wouldn’t need to get the ghosts animation track, since all we going to need do is get the players recent animation, in this case would be (example)

local plrtrack = plr.Character.Humanoid.Animator:GetPlayingAnimationTracks()
local recentanimation = plrtrack[1]
-- Now all you would need to do is play #recentanimation to ghosts character by loading it into the humanoid via its animator object

EDIT: recentanimation is an instance of the animation playing…

But the track would be playing every tick which causes lag and doesn’t make the ghost move also how would I stop the animations after the player stopped using them for example the walking animation

Oh, sorry for the late reply I was doing something else :sweat_smile:

Here is something I just whipped up together, sorry again

local tweenService = game:GetService("TweenService")
local tweeninfo = TweenInfo.new(.5, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 0)
local runserver = game:GetService("RunService")

local Ghosts = {}

runserver.Heartbeat:Connect(function(plr)
	for plrName, info in pairs(Ghosts) do
		local ghosthrp = info["Ghost"].HumanoidRootPart
		local goal = {}
		goal.CFrame = info["Char"].HumanoidRootPart.CFrame
		local tweeninstance = tweenService:Create(ghosthrp,tweeninfo,goal)
		tweeninstance:Play()
	end
end)

game.Players.PlayerAdded:Connect(function(plr)
	local ghost = nil
	plr.CharacterAdded:Connect(function(char)

		ghost = game.ReplicatedStorage.Copy.Debug.GhostPvpChar:Clone()
		ghost.Parent = game.Workspace
		Ghosts[plr.Name] = {
			["Ghost"] = ghost;
			["Player"] = plr;
			["Char"] = char
		}
		char.Humanoid.AnimationPlayed:Connect(function(recentanimation)
			for i, track in pairs (ghost.Humanoid:GetPlayingAnimationTracks()) do
				track:Stop()
			end
			local recentanimation = recentanimation.Animation
			local hum = ghost.Humanoid
			local anim = hum:LoadAnimation(recentanimation)
			anim:Play()
		end)
	end)
end)

If that still is laggy then we can use BodyPosition and BodyGyro which can eliminate the use of tweening entirely

EDIT:

Here is that as well since I already did it might as well complete it here it is fully working

local runserver = game:GetService("RunService")

local Ghosts = {}

runserver.Heartbeat:Connect(function(plr)
	for plrName, info in pairs(Ghosts) do
		if info["Char"]:FindFirstChild("HumanoidRootPart",false) then
			info["GhostBodyTrack"][1].Position = info["Char"].HumanoidRootPart.Position
			info["GhostBodyTrack"][2].CFrame = info["Char"].HumanoidRootPart.CFrame
		end
	end
end)

game.Players.PlayerAdded:Connect(function(plr)
	local ghost = nil
	plr.CharacterAdded:Connect(function(char)

		ghost = game.ReplicatedStorage.Copy.Debug.GhostPvpChar:Clone()
		ghost.Parent = game.Workspace
		
		local bodyposition = Instance.new("BodyPosition",ghost.HumanoidRootPart); bodyposition.MaxForce = Vector3.new(math.huge,math.huge,math.huge)
		local bodygyro = Instance.new("BodyGyro",ghost.HumanoidRootPart); bodygyro.MaxTorque = Vector3.new(10000,10000,10000)
		
		Ghosts[plr.Name] = {
			["Ghost"] = ghost;
			["Player"] = plr;
			["Char"] = char;
			["GhostBodyTrack"] = {bodyposition,bodygyro} 
		}
		char.Humanoid.AnimationPlayed:Connect(function(recentanimation)
			for i, track in pairs (ghost.Humanoid:GetPlayingAnimationTracks()) do
				track:Stop()
			end
			local recentanimation = recentanimation.Animation
			local hum = ghost.Humanoid
			local anim = hum:LoadAnimation(recentanimation)
			anim:Play()
		end)
	end)
end)