Help with CFrame manipulation

I’m making a system where, while the player holds down the E key, parts start forming a circle around the player. The problem is, I can’t figure out the math to calculate the angle of the new part based on the previous one — right now, each part is being created without referencing the last one. I basically need to fix the part that handles the angles. If anyone knows how to help, I’d really appreciate it!

--\\VARIABLES
local ratio = 7
local angle = 45
local firstPart : Part

spawnRemote.OnServerEvent:Connect(function(plr, timer, isFirst)
	local part = Instance.new('Part')
	part.Name = 'Part' .. timer
	part.Anchored = true
	part.Parent = partFolder	
	
	local char = plr.Character
	local hrp : Part = char and char:FindFirstChild('HumanoidRootPart')
	
	if isFirst then
		part.CFrame = hrp.CFrame + hrp.CFrame.LookVector * ratio
		part.CFrame = CFrame.lookAt(part.Position, part.Position + Vector3.new(0, 1, 0), (hrp.Position - part.Position).Unit)
		
		firstPart = part
	else
		local vetorRelativo = firstPart.Position - hrp.Position
		angle += -math.rad(20)

		
		local x = ratio * math.cos(angle)
		local z = ratio * math.sin(angle) 
		
		part.CFrame = hrp.CFrame * CFrame.new(x, hrp.CFrame.LookVector.Y, z)
		part.CFrame = CFrame.lookAt(part.Position, part.Position + Vector3.new(0, 1, 0), (hrp.Position - part.Position).Unit)

	end
	--angle = (math.rad(45) * timer)
end)

1 Like

I’m not sure exactly what you want here. You showed a picture of the error, I take it that’s not it.
You had the right idea until you started mixing degrees and radians.

LocalScript
--LocalScript in StarterPlayerScripts
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local spawnRemote = ReplicatedStorage:FindFirstChild("spawnRemote") or Instance.new("RemoteEvent", ReplicatedStorage)
spawnRemote.Name = "spawnRemote"

local partFolder = Workspace:FindFirstChild("partFolder") or Instance.new("Folder", Workspace)
partFolder.Name = "partFolder"

local timer = 0
local holding = false

UserInputService.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.E then
		holding = true
		timer = 0
		spawnRemote:FireServer(timer, true)
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.E then
		holding = false
	end
end)

RunService.RenderStepped:Connect(function()
	if holding then
		timer += 1
		spawnRemote:FireServer(timer, false)
	end
end)
ServerScript
--ServerScript in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")

local spawnRemote = ReplicatedStorage:FindFirstChild("spawnRemote") or Instance.new("RemoteEvent", ReplicatedStorage)
spawnRemote.Name = "spawnRemote"

local partFolder = Workspace:FindFirstChild("partFolder") or Instance.new("Folder", Workspace)
partFolder.Name = "partFolder"

local ratio = 7
local angleStep = math.rad(20)
local firstPart
local currentAngle = 0

spawnRemote.OnServerEvent:Connect(function(plr, timer, isFirst)
	local part = Instance.new('Part')
	part.Name = 'Part' .. timer
	part.Anchored = true
	part.Size = Vector3.new(1,1,1)
	part.Parent = partFolder	

	local hrp = plr.Character and plr.Character:FindFirstChild('HumanoidRootPart')
	if not hrp then return end

	if isFirst then
		part.CFrame = hrp.CFrame * CFrame.new(ratio, 0, 0)
		firstPart = part
		currentAngle = 0
	else
		currentAngle += angleStep
		local x = ratio * math.cos(currentAngle)
		local z = ratio * math.sin(currentAngle)
		part.CFrame = hrp.CFrame * CFrame.new(x, 0, z)
	end
end)

You have to be standing totally still when you press the E.. I’m thinking that will be the next issue.


Well, that was certainly more of a challenge.

LocalScript
--LocalScript in StarterPlayerScripts
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local spawnRemote = ReplicatedStorage:FindFirstChild("spawnRemote") or Instance.new("RemoteEvent", ReplicatedStorage)
spawnRemote.Name = "spawnRemote"

local partFolder = Workspace:FindFirstChild("partFolder") or Instance.new("Folder", Workspace)
partFolder.Name = "partFolder"

local timer = 0
local holding = false
local originCFrame

UserInputService.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.E then
		local hrp = game.Players.LocalPlayer.Character and game.Players.LocalPlayer.Character:FindFirstChild("HumanoidRootPart")
		if not hrp then return end
		originCFrame = hrp.CFrame
		holding = true
		timer = 0
		spawnRemote:FireServer(timer, true, originCFrame)
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.E then
		holding = false
	end
end)

RunService.RenderStepped:Connect(function()
	if holding then
		timer += 1
		spawnRemote:FireServer(timer, false, originCFrame)
	end
end)
ServerScript
--ServerScript in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")

local spawnRemote = ReplicatedStorage:FindFirstChild("spawnRemote") or Instance.new("RemoteEvent", ReplicatedStorage)
spawnRemote.Name = "spawnRemote"

local partFolder = Workspace:FindFirstChild("partFolder") or Instance.new("Folder", Workspace)
partFolder.Name = "partFolder"

local ratio = 7
local angleStep = math.rad(20)
local sequences = {}

spawnRemote.OnServerEvent:Connect(function(plr, timer, isFirst, originCFrame)
	if isFirst then
		sequences[plr] = {angle = 0, origin = originCFrame}
	end

	local seq = sequences[plr]
	if not seq then return end

	local part = Instance.new("Part")
	part.Name = "Part" .. timer
	part.Anchored = true
	part.Size = Vector3.new(1,1,1)
	part.Parent = partFolder

	local x = ratio * math.cos(seq.angle)
	local z = ratio * math.sin(seq.angle)
	part.CFrame = seq.origin * CFrame.new(x, 0, z)

	seq.angle += angleStep
end)
1 Like

The problem is, the parts should spawn close to each other, since I want them to form a circle around the player while they’re holding E.
The first part spawns in the player’s LookVector, and the next parts should be generated side by side to form a circle. However, as you can see in the previous image, the second part spawned far away from the first one.

obs: The script is working, but not the way I want.

1 Like

You need to calculate angleStep instead of using constant one.
To do that I added: partSize variable.
Also you need to orient parts toward player position.

--ServerScript in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")

local spawnRemote = ReplicatedStorage:FindFirstChild("spawnRemote") or Instance.new("RemoteEvent", ReplicatedStorage)
spawnRemote.Name = "spawnRemote"

local partFolder = Workspace:FindFirstChild("partFolder") or Instance.new("Folder", Workspace)
partFolder.Name = "partFolder"

local ratio = 20
local partSize = Vector3.new(4, 3, 1)
-- calculate angle step based on part size and ratio
local angleStep = partSize.X / ratio
local sequences = {}

spawnRemote.OnServerEvent:Connect(function(plr, timer, isFirst, originCFrame)
	if isFirst then
		sequences[plr] = {angle = 0, origin = originCFrame}
	end

	local seq = sequences[plr]
	if not seq then return end

	local part = Instance.new("Part")
	part.Name = "Part" .. timer
	part.Anchored = true
	part.Size = partSize
	part.Parent = partFolder

	local x = ratio * math.cos(seq.angle)
	local z = ratio * math.sin(seq.angle)
	part.CFrame = CFrame.lookAt((seq.origin * CFrame.new(x, 0, z)).Position, seq.origin.Position)

	seq.angle += angleStep
	
	-- stop on full circle	
	if seq.angle > math.pi * 2 then
		sequences[plr] = nil
	end
end)

6 Likes

First of all: thanks for the scripts!
But you should not do it on client side because of possible race condition between client and server that might result in two created events with the same name.
Client should wait for the event only:
local spawnRemote = ReplicatedStorage:WaitForChild("spawnRemote")
Thanks!

1 Like

I do that expecting it to be removed and set up manually. It’s not like anything like that would ever end up in a finished script I’d use. It’s both a script and an explanation of what needs to be set up at the same time. Those scripts are rough and just smashed together to reach concept validation. After that, you go back and refine them with proper scripting.

That should look like this in the end..

local remote = rep:WaitForChild("spawnRemote")

Come on now, this part is even more odd.

RunService.RenderStepped:Connect(function()
	if holding then
		timer += 1
		spawnRemote:FireServer(timer, false, originCFrame)
	end
end)

This silly stuff is only for quick testing. I just want to get here:

spawnRemote.OnServerEvent:Connect 

So I can test that approach and get something rolling to work with. There is a method to the madness, after refining you end up with a sweet script like yours.

2 Likes

I agree.
This has one flaw that build speed depends of clients performance: faster clients with more fps build the wall faster, that obviously makes the game not balanced.
Sorry for not pointing into all odds.
I’ve got your point about concepts.
I am just a fan of race conditions and creation of spawnEvent has caught my eye.
Thank you for your scripts again, sir - much appreciated that!

2 Likes

Hey, how can I spawn the first part in front of the player, based on their look vector?
I tried this:

part.CFrame = CFrame.lookAt((seq.origin * CFrame.new(x, 0, z)).Position , seq.origin.Position)  * CFrame.new(seq.origin.LookVector)

but it didn’t work.

You need to rotate additionally using model’s current look vector.
I’ve changed local script to use model’s pivot instead of humanoid root part’s one.
I’ve changed server script to use max parts count, this makes wall width variable but avoid walls overlapping.

local script
--LocalScript in StarterPlayerScripts
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local spawnRemote = ReplicatedStorage:WaitForChild("spawnRemote")

local partFolder = Workspace:FindFirstChild("partFolder") or Instance.new("Folder", Workspace)
partFolder.Name = "partFolder"

local timer = 0
local holding = false
local originCFrame

UserInputService.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.E then
		originCFrame = game.Players.LocalPlayer.Character:GetPivot()
		holding = true
		timer = 0
		spawnRemote:FireServer(timer, true, originCFrame)
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.E then
		holding = false
	end
end)

RunService.RenderStepped:Connect(function()
	if holding then
		timer += 1
		spawnRemote:FireServer(timer, false, originCFrame)
	end
end)
server script
--ServerScript in ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")

local spawnRemote = ReplicatedStorage:FindFirstChild("spawnRemote") or Instance.new("RemoteEvent", ReplicatedStorage)
spawnRemote.Name = "spawnRemote"

local partFolder = Workspace:FindFirstChild("partFolder") or Instance.new("Folder", Workspace)
partFolder.Name = "partFolder"

local ratio = 8
local total_parts = 16 
local partSize = Vector3.new(ratio * math.pi * 2 / total_parts, 3, 1)
-- calculate angle step based on part size and ratio
local angleStep = partSize.X / ratio
local sequences = {}

spawnRemote.OnServerEvent:Connect(function(plr, timer, isFirst, originCFrame : CFrame)
	if isFirst then
		sequences[plr] = {angle = 0, origin = originCFrame}
	end

	local seq = sequences[plr]
	if not seq then return end

	local part = Instance.new("Part")
	part.Name = "Part" .. timer
	part.Anchored = true
	part.Size = partSize
	part.Parent = partFolder

	local x = ratio * math.cos(seq.angle)
	local z = ratio * math.sin(seq.angle)
	local angle = CFrame.new(seq.origin.LookVector) * CFrame.Angles(0, math.pi * .5, 0)
	part.CFrame = CFrame.lookAt(seq.origin * angle:VectorToWorldSpace(Vector3.new(x, 0, z)), seq.origin.Position)

	seq.angle += angleStep

	-- stop on full circle	
	if seq.angle > math.pi * 2 then
		sequences[plr] = nil
	end
end)

thank you so much bro, math and cframe manipulation is so hard to understand

I just changed one part of the script

total_parts = ratio*2

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.