Weld cframe issue

I have two unanchored welded parts. One is the normal main part. The other is the coin. I want the coin to be welded to the main part. Which it does. However animating it going slightly up and down and fully rotating is a new issue. I just want the coin to follow its animation like it’s “anchored” but still move and everything with its main part which gets moved by physical forces.

--//============================ ANIMATION SYSTEM ============================//
do
	local angle = 0
	RunService.Heartbeat:Connect(function(dt)
		angle = angle + ROTATION_SPEED * dt/10
		local bobOffset = math.sin(dt) * BOB_AMPLITUDE * 0
		for _, coin in ipairs(CollectionService:GetTagged("GreenStar")) do
			coin.CFrame = coin.CFrame * CFrame.new(0, bobOffset, 0) * CFrame.Angles(0, angle, 0)
			coin.Weld.C0 = coin.Parent.PrimaryPart.CFrame:ToObjectSpace(coin.CFrame) * coin.Weld.C0:Inverse()

		end
	end)
end
Full module
local Collectables = {}

--//============================ SERVICES & CONFIGURATION ============================//

local CollectionService = game:GetService("CollectionService")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local COLLECTABLES_FOLDER = workspace:FindFirstChild("Collectables")

local COIN_TEMPLATE = ServerStorage.Collectables:WaitForChild("GreenStar")

-- Maze dimensions (must match the generator script)
local CELL_SIZE = 16
local PLANK_LENGTH_IN_CELLS = 3

-- Animation Parameters
local BOB_AMPLITUDE = 0.5
local BOB_SPEED = 1
local ROTATION_SPEED = math.rad(1)

--//============================ PRIVATE FUNCTIONS ============================//

local function onCoinTouched(otherPart, coin)
	local character = otherPart.Parent
	local player = character and Players:GetPlayerFromCharacter(character)
	if player then
		Collectables.collect(coin, player)
	end
end

--//============================ PUBLIC MODULE FUNCTIONS ============================//

--- [PART 1] Calculates the center position for EVERY cell within a given block model.
-- This method is robust and works even if cells have holes instead of solid floors.
-- @param block The Model of the block/plank to find positions in.
-- @return table A table of Vector3 positions, one for each cell's center.
function Collectables.getSpawnPositionsInBlock(block)

	local primaryPart = block.PrimaryPart
	local plankCenter = primaryPart.Position
	local plankSize = primaryPart.Size

	local positions = {}

	-- Determine the plank's orientation by checking its longest dimension.
	local mainAxisVector
	if plankSize.X > plankSize.Z then
		mainAxisVector = Vector3.new(1, 0, 0)
	else
		mainAxisVector = Vector3.new(0, 0, 1)
	end

	-- The offset from the plank's absolute center to the center of the first cell in the sequence.
	-- For 3 cells, this is `(3-1)/2 * 16 = 16`.
	local startOffset = (PLANK_LENGTH_IN_CELLS - 1) / 2 * CELL_SIZE

	-- Calculate the world-space position of the very first cell's center.
	local firstCellCenter = plankCenter - mainAxisVector * startOffset

	-- Loop through each of the cells in the plank.
	for i = 0, PLANK_LENGTH_IN_CELLS - 1 do
		-- Calculate this cell's center by moving along the main axis.
		local cellCenter = firstCellCenter + mainAxisVector * (i * CELL_SIZE)
		table.insert(positions, cellCenter)
	end

	return positions
end


--- [PART 2] Spawns a single coin at a specific world position and welds it to a block.
-- @param position The world-space Vector3 where the coin should be centered.
-- @param block The block model whose PrimaryPart the coin will be welded to.
-- @return The newly created coin instance, or nil if spawn failed.
--- [PART 2] Spawns a single coin at a specific world position and welds it to a block.
-- @param position The world-space Vector3 where the coin should be centered.
-- @param block The block model whose PrimaryPart the coin will be welded to.
-- @return The newly created coin instance, or nil if spawn failed.
function Collectables.spawnAt(position, block)
	if not position or not block or not block.PrimaryPart then
		return nil
	end

	local newCoin = COIN_TEMPLATE:Clone()

	-- CRITICAL FIX 1: The coin must be unanchored to be welded to an unanchored block.
	newCoin.Anchored = false
	newCoin.CanCollide = false -- Also good practice
	newCoin.Parent = block

	-- We do not set the coin's CFrame directly, the weld will do this for us.
	local weld = Instance.new("Weld")
	weld.Part0 = newCoin             -- The part that moves (the coin)
	weld.Part1 = block.PrimaryPart   -- The static reference part (the block)
	weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(CFrame.new(position))
	weld.Parent = newCoin
	
	CollectionService:AddTag(newCoin, "GreenStar")

	newCoin.Touched:Connect(function(otherPart)
		onCoinTouched(otherPart, newCoin)
	end)

	return newCoin
end

--- A wrapper function that spawns a coin in every cell of a given block.
-- @param block The block model to spawn coins in.
function Collectables.spawnInBlock(block)
	local spawnPositions = Collectables.getSpawnPositionsInBlock(block)

	for _, pos in ipairs(spawnPositions) do
		Collectables.spawnAt(pos, block)
	end
end

--- Handles the server-side logic for collecting a coin.
function Collectables.collect(coin, player)
	if not coin or not CollectionService:HasTag(coin, "GreenStar") then return end
	CollectionService:RemoveTag(coin, "GreenStar")
	local FadeTween = game:GetService("TweenService"):Create(coin, TweenInfo.new(.8, Enum.EasingStyle.Sine), {Transparency = 1})
	
	local Sound = game.ReplicatedStorage.Sounds.CollectGreen:Clone()
	Sound.Parent = coin
	Sound:Play()
	
	local Sound = game.ReplicatedStorage.Sounds.Background:Clone()
	Sound.Parent = coin
	Sound:Play()
	
	FadeTween:Play()
	FadeTween.Completed:Connect(function() coin:Destroy() end)
end

--//============================ ANIMATION SYSTEM ============================//
do
	local angle = 0
	RunService.Heartbeat:Connect(function(dt)
		angle = angle + ROTATION_SPEED * dt/10
		local bobOffset = math.sin(dt) * BOB_AMPLITUDE * 0
		for _, coin in ipairs(CollectionService:GetTagged("GreenStar")) do
			coin.CFrame = coin.CFrame * CFrame.new(0, bobOffset, 0) * CFrame.Angles(0, angle, 0)
			coin.Weld.C0 = coin.Parent.PrimaryPart.CFrame:ToObjectSpace(coin.CFrame) * coin.Weld.C0:Inverse()

		end
	end)
end
--]]


return Collectables

How do i currently position the weld with the coin as to not fling everything?

1 Like

bump

Modifying the coin.CFrame directly even though it’s welded isn’t really a good idea. As you’ve stated, it might result in chaotic spinning and whatever.

So, we should never touch CFrame of a welded part. Instead:

  • Animate Weld.C0 relative to the initial offset,
  • Store the initial C0 once during setup,
  • Apply bob + rotation each frame relative to that base.

(This might work if we aren’t supposed to desync per coin, also it probably won’t work if you’re using WeldConstraints :slightly_smiling_face: )

Try replacing the --//== ANIMATION SYSTEM ==//-- with this :

-- Table to store each coin's weld and its original C0 (relative position to the parent block)
local originalOffsets = {}

-- On startup, go through all coins tagged as "GreenStar"
for _, coin in ipairs(CollectionService:GetTagged("GreenStar")) do
	local weld = coin:FindFirstChildOfClass("Weld") -- Find the Weld attached to the coin

	-- Make sure the weld exists, the coin has a parent model, and the model has a PrimaryPart
	if weld and coin.Parent and coin.Parent:IsA("Model") and coin.Parent.PrimaryPart then
		-- Save the weld and its original offset (C0) relative to the parent
		originalOffsets[coin] = {
			weld = weld,
			baseC0 = weld.C0 -- This is the starting CFrame offset from the parent (before any animation)
		}
	end
end

-- t will accumulate elapsed time for smooth animation
local t = 0

-- This function runs every frame (on the Heartbeat event)
RunService.Heartbeat:Connect(function(dt)
	t += dt -- Accumulate time to drive sine wave + rotation

	-- Calculate how much to rotate and bob this frame
	local angle = t * ROTATION_SPEED -- Rotation in radians over time
	local bobOffset = math.sin(t * BOB_SPEED * math.pi * 2) * BOB_AMPLITUDE -- Vertical bobbing using sine wave

	-- Loop through all registered coins
	for coin, data in pairs(originalOffsets) do
		-- Make sure the coin and weld still exist
		if coin and coin.Parent and data.weld then
			-- Calculate a CFrame for rotation around Y-axis
			local rotation = CFrame.Angles(0, angle, 0)

			-- Calculate a CFrame for vertical bobbing (up and down)
			local bob = CFrame.new(0, bobOffset, 0)

			-- Apply both bobbing and rotation relative to the original position
			-- New C0 = bobbing * rotation * original offset
			data.weld.C0 = bob * rotation * data.baseC0
		end
	end
end)

With the new block of code,

  • originalOffsets[coin] stores the original Weld.C0 at spawn time,
  • The animation loop just adds bobbing & rotation to that base,
  • This avoids CFrame stacking and keeps coins visually stable.

I hope this works!

1 Like

well it defiantly came with its own issues and the for loop doesnt account for newly added tagged coins but other then that the welds dont fling so thanks a LOT!

--//============================ ANIMATION SYSTEM ============================//
do
	-- Services
	local CollectionService = game:GetService("CollectionService")
	local RunService = game:GetService("RunService")

	-- Constants (assuming these are defined elsewhere in your script)
	local ROTATION_SPEED = 1
	local BOB_SPEED = 1

	-- Table to store each coin's weld and its original C0 (relative position to the parent block)
	local originalOffsets = {}

--[[
-- vvvvvv THIS IS THE MOVED LOOP vvvvvv
-- On startup, go through all coins tagged as "GreenStar" ONE TIME to set them up.
--]]
	local function SetUpAllStars(tag)
		if tag ~= "GreenStar" then return end
		for _, coin in ipairs(CollectionService:GetTagged("GreenStar")) do
			local weld = coin:FindFirstChildOfClass("Weld") -- Find the Weld attached to the coin

			-- Make sure the weld exists, the coin has a parent model, and the model has a PrimaryPart
			if weld and not originalOffsets[coin] then
				-- Save the weld and its original offset (C0) relative to the parent
				originalOffsets[coin] = {
					weld = weld,
					baseC0 = weld.C0 -- This is the starting CFrame offset from the parent (before any animation)
				}
			end
		end
	end
	
	task.spawn(function()
		CollectionService.TagAdded:Connect(SetUpAllStars)
	end)

	-- t will accumulate elapsed time for smooth animation
	local t = 0

	-- This function runs every frame (on the Heartbeat event)
	RunService.Heartbeat:Connect(function(dt)
		-- The expensive setup loop has been removed from here.

		t += dt -- Accumulate time to drive sine wave + rotation

		-- Calculate how much to rotate and bob this frame
		local angle = t * ROTATION_SPEED -- Rotation in radians over time
		local bobOffset = math.sin(t * BOB_SPEED * math.pi * 2) * 0.5 -- Vertical bobbing using sine wave

		-- Loop through all registered coins that we found at the start
		for coin, data in pairs(originalOffsets) do
			-- Make sure the coin and weld still exist
			-- Calculate a CFrame for rotation around Y-axis
			local rotation = CFrame.Angles(0, angle, 0)

			-- Calculate a CFrame for vertical bobbing (up and down)
			local bob = CFrame.new(0, bobOffset, 0)
			data.weld.C0 = (bob * rotation)*(CFrame.new(data.baseC0.Position))
			
		end
	end)
end

Well amazingly with some tweaks and such it works perfectly!

However there is one issue… it only wokrs 80% of the time. Sometimes its y axis is visably wrong. instead rotating down 90 degrees in wtv direction bobbing and roating along that axis. which is very non ignorable. Why could this be? ive even retweaked my position getting for the stars to spawn greatly.

NewSpawnFunc

function getCellPositionsFromPlank(plankModel)
	local primaryPart = plankModel.PrimaryPart
	if not primaryPart then
		warn("Plank model does not have a PrimaryPart: ".. plankModel.Name)
		return {}
	end

	local positions = {}
	local partCFrame = primaryPart.CFrame
	local partSize = primaryPart.Size


	local numCells
	local stepDirection

	if partSize.X > partSize.Z then
		-- This is an X-Oriented plank (e.g., Size is 48x1x16)
		numCells = math.round(partSize.X / CELL_SIZE) -- FIX: Removed .X
		stepDirection = partCFrame.RightVector * CELL_SIZE -- FIX: Removed .X
	else
		-- This is a Z-Oriented plank (e.g., Size is 16x1x48)
		numCells = math.round(partSize.Z / CELL_SIZE) -- FIX: Removed .Z (This was the line causing the error)
		stepDirection = partCFrame.LookVector * CELL_SIZE -- FIX: Removed .Z
	end

	local totalSpan = (numCells - 1) * CELL_SIZE
	local firstCellCenter = partCFrame.Position - (stepDirection.Unit * (totalSpan / 2))

	for i = 0, numCells - 1 do
		local cellCenter = firstCellCenter + (stepDirection * i)
		table.insert(positions, cellCenter)
	end

	return positions
end

Why could this be? why does it only work most of the time and other times completely shift its y-axis?

Well,

  1. Using the server script to rotate and update parts is a common bad practice. My advice
    is to fire a remote event to the client and updating your coins’ rotation and whatnot there.

an example:


RunService.Heartbeat:Connect(function(dt)
	t += dt
	task.defer(function()
		for i = #coins, 1, -1 do
			local coin = coins[i]
			if coin and coin.model and coin.model.Parent then
				coinEvent:FireAllClients(coin.model, t, coin.originalC0) -- originalc0 is basically original offsets, but stored in a constructor.
			else
				return -- Cleanup dead/invalid coins
			end
		end
	end)
end)

The main problem probably is that you’re completely losing all rotation + orientation by using

This just does this:

  • Creates a new CFrame with just the position from the original.
  • Then applies rotation and bob to that.
  • Loses all original rotation/orientation of the coin.
    :red_triangle_pointed_down: So if the coin wasn’t upright at spawn, it’s now being rotated as if it were, which breaks the effect.

With this in mind, using the baseC0’s CFrame is just better practice overall.

data.weld.C0 = data.baseC0 * rotation * bob

This probably working is due to using the CFrame’s local axis, not the world’s. (Although you probably know this, i just added it anyway ^^)

Just for reference, here’s a coin server script I’ve developed for you to also help me understand welding better :slight_smile: :

**Server Script**
--//===================         SERVICE REFERENCES         ==============================\\--

local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local DataStoreService = game:GetService("DataStoreService")

local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

--//===================         REMOTE EVENTS & MODULES         ==============================\\--

local event = ReplicatedStorage.BetterTween.TweenClients
local soundEvent = ReplicatedStorage.CoinService.Sounds.CoinSound.SendSound
local coinEvent = ReplicatedStorage.CoinService.CoinAngles -- Sends rotation data to clients

local BetterTween = require(ReplicatedStorage.BetterTween) -- Custom tween module
local TweenService = game:GetService("TweenService")
local tweenInfo = TweenInfo.new(0.85) -- Default tween duration

--//===================         INITIAL COIN SETUP         ==============================\\--

local coinPart = CollectionService:GetTagged("CoinPart") -- Get all parts tagged as "CoinPart"
local allCoinParts = {table.unpack(coinPart)} -- Copy to a mutable table

local coins = {} -- All spawned coins are stored here

--//===================         COIN CLASS DEFINITION         ==============================\\--

local Coin = {}
Coin.__index = Coin

-- Constructor for a new coin
function Coin.new()
	local coinTable = {
		model = nil,
		hasBeenTouched = false,
		originalC0 = nil, -- Stores the original weld offset for animation
	}
	setmetatable(coinTable, Coin)

	local templateCoin = ServerStorage.Coin:WaitForChild("YellowCoin")
	coinTable.model = templateCoin:Clone()
	
	-- Prep coin properties
	coinTable.model.Anchored = false
	coinTable.model.CanCollide = false
	coinTable.model.Parent = workspace
	
	return coinTable
end

-- Triggered when a player touches a coin
function Coin:whenCoinTouched(touchedPart, baseCoin)
	if not self.hasBeenTouched then
		self.hasBeenTouched = true -- Prevent re-triggering this coin

		-- Remove touched coin from `allCoinParts`
		for i, part in ipairs(allCoinParts) do
			if part == baseCoin then
				table.remove(allCoinParts, i)
			end
		end

		-- Remove this coin from the `coins` table
		for i, coin in ipairs(coins) do
			if coin == self then
				table.remove(coins, i)
			end
		end

		-- Tween coin transparency for disappearance effect
		local timeSpent = BetterTween:Tween(self.model, tweenInfo, {Transparency = 1})

		-- Award player health and coins
		local character = touchedPart:FindFirstAncestorWhichIsA("Model")
		local humanoid = character:WaitForChild("Humanoid")
		humanoid.Health += 50

		local playerFromCharacter = Players:GetPlayerFromCharacter(character)
		playerFromCharacter.leaderstats.Coins.Value += 1

		-- Play collection sound on client
		soundEvent:FireClient(playerFromCharacter, self)

		-- Destroy coin after animation completes
		task.delay(timeSpent, function()
			self.model:Destroy()
			self = nil -- optional: allow GC
		end)
	else
		return -- Coin already touched, ignore
	end
end

-- Welds the coin to a CoinPart and sets up touch detection
function Coin:partWelding(baseCoin)
	local weld = Instance.new("Weld")

	-- Position coin slightly above the base part
	self.model.CFrame = baseCoin.CFrame * CFrame.new(0, 3.5, 0)

	-- Set up weld from coin to baseCoin
	weld.Part1 = baseCoin
	weld.Part0 = self.model
	weld.C0 = baseCoin.CFrame:ToObjectSpace(self.model.CFrame)
	weld.Parent = self.model

	-- Save original offset for animation
	self.originalC0 = weld.C0

	-- Set up touch detection
	self.model.Touched:Connect(function(otherpart)
		self:whenCoinTouched(otherpart, baseCoin)
	end)

	return self
end

-- Spawns and welds coins to all tagged parts in the scene
function getAllCoins()
	for _, part in ipairs(allCoinParts) do
		if part:IsA("BasePart") then
			local newCoin = Coin.new():partWelding(part)
			table.insert(coins, newCoin)
		end
	end
end

-- When a new CoinPart is added during gameplay, spawn a coin on it
CollectionService:GetInstanceAddedSignal("CoinPart"):Connect(function(addedObject)
	table.insert(allCoinParts, addedObject)
	getAllCoins()
end)

-- Initial coin generation
getAllCoins()

--//===================         ANIMATION LOOP (ROTATE + BOB)         ==============================\\--

local t = 0 -- Time accumulator for smooth animation

RunService.Heartbeat:Connect(function(dt)
	t += dt

	-- Defer to ensure safe iteration even if coins get removed during loop
	task.defer(function()
		for i = #coins, 1, -1 do
			local coin = coins[i]
			if coin and coin.model and coin.model.Parent then
				-- Tell client to rotate and bob this coin
				coinEvent:FireAllClients(coin.model, t, coin.originalC0)
			else
				return -- Skip invalid/dead coins
			end
		end
	end)
end)

**Local Script**
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local soundEvent = ReplicatedStorage.CoinService.Sounds.CoinSound.SendSound :: RemoteEvent
local coinEvent = ReplicatedStorage.CoinService.CoinAngles

coinEvent.OnClientEvent:Connect(function(model , t, originalC0)
	
	local success, result = pcall(function()
		
		local weld = model and model:FindFirstChild("Weld") -- Finds the weld on the model
		weld.C0 = originalC0 * CFrame.Angles(0, t * 0.5, 0) * CFrame.new(0, math.sin(t * 2), 0) -- assigns the new C0 to the weld, rotating it 

	end)
end)

	
	

soundEvent.OnClientEvent:Connect(function(self)

	local coinSound = ReplicatedStorage.CoinService.Sounds.CoinSound
	local cloneSound = coinSound:Clone()
	cloneSound.Parent = self.model -- the sounds will only be played per client to avoid unnecessary confusion ig
	cloneSound:Play()

end)
**Better Tween Module**
local BetterTween = {}

local mainTask

local TweenService = game:GetService("TweenService")
local event = script.TweenClients

local activeInstance = {}

local function instanceCheck(instance, goal)
	for instanceName, val in pairs(goal) do
		instance[instanceName] = val
	end
	activeInstance[instance] = nil
	end

function BetterTween:Tween(instance, tweenInfo, goal)
	
	if activeInstance[instance] then
		task.cancel(mainTask)
	end
	
	local complete = false
	
	local tweenData = {
		
		tweenInfo.Time,
		tweenInfo.EasingStyle,
		tweenInfo.EasingDirection,
		tweenInfo.RepeatCount,
		tweenInfo.Reverses,
		tweenInfo.DelayTime
		
	} 
	
	
	local totalTime = (tweenInfo.Time + tweenInfo.DelayTime) * (tweenInfo.RepeatCount + 1)
	
	table.insert(activeInstance, instance)
	mainTask = task.delay(totalTime, instanceCheck, instance, goal)
	
	

	event:FireAllClients(instance, tweenData, goal)
	
	return totalTime
	
end

return BetterTween

Alright, I’m not telling you to just copy my code (you can btw).

But you SHOULD use it as reference or whatever and further improve your code if you can understand what’s written.

Keep it up!!! :sunglasses:

1 Like

unfortunetly that line makes things worse. Im pretty sure it was the original code u gave me before i tweaked it. Its worse because instead now of each star rotating on its own inependent y axis they rotate all around the plankmodle/block 's center. And still have some stars having an incorrect y axis.

maybe the axis thing is due to the fact i spawn by using to object space. I am not sure

data.weld.C0 = (bob * rotation)*(CFrame.new(data.baseC0.Position))

also i was gonna do the local script coin animation after how bad my recieve was from just a animate code block lol. went from a 100kb of recieve to nearly 0.

1 Like

I’m going to be quick and informal now but the thing you’re experiencing is actually because of the mesh. Like %80. I’ve had that experience as well while trying to script the coin.

It rotates but not around itself like a globe, but around another object like a planet.

I’m almost sure that’s a problem with the mesh’s origin/pivot point. Because when I had that
issue when developing, it bothered me to a point where i had to change meshes and when I did, the problem was just gone.

So, just try changing the mesh or just adjust the pivot point. Here’s the one I used for mine :slight_smile: :

rbxassetid://12726636742

Just go back to your original code (the one they were spinning normally) and try using a different mesh. If I can help with anything else or if it just doesn’t work, let me know.

Btw, here are my coins. They use BetterTween and client-sided animations, static on the server.

The sounds are a bit offset due to the compression but you get the idea.

1 Like

Unfortunately changing meshes didnt change the issue.

by the way this is my new module

local Collectables = {}

--//============================ SERVICES & CONFIGURATION ============================//

local CollectionService = game:GetService("CollectionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local COLLECTABLES_FOLDER = workspace:FindFirstChild("Collectables")

local COIN_TEMPLATE = ReplicatedStorage.CollectableTemplates:WaitForChild("GreenStar")

-- Maze dimensions (must match the generator script)
local CELL_SIZE = 16
local PLANK_LENGTH_IN_CELLS = 3


--//============================ PRIVATE FUNCTIONS ============================//

local function onCoinTouched(otherPart, coin)
	local character = otherPart.Parent
	local player = character and Players:GetPlayerFromCharacter(character)
	
	
	if not CollectionService:HasTag(coin, "GreenStar") or not player then 
		return false
	end

	CollectionService:RemoveTag(coin, "GreenStar")
	ReplicatedStorage.Remotes.CoinTouch:FireClient(player, coin)
	
	coin.CanTouch = false
	
	
	local FadeTween = game:GetService("TweenService"):Create(coin, TweenInfo.new(.25, Enum.EasingStyle.Sine), {Transparency = 1})
	FadeTween:Play()
	FadeTween.Completed:Wait()
	coin.Parent = nil
	coin:Destroy()

	
	return true
end

--//============================ PUBLIC MODULE FUNCTIONS ============================//

--- [PART 1] Calculates the center position for EVERY cell within a given block model.
-- This method is robust and works even if cells have holes instead of solid floors.
-- @param block The Model of the block/plank to find positions in.
-- @return table A table of Vector3 positions, one for each cell's center.

function getCellPositionsFromPlank(plankModel)
	local primaryPart = plankModel.PrimaryPart
	if not primaryPart then
		warn("Plank model does not have a PrimaryPart: ".. plankModel.Name)
		return {}
	end

	-- Assume CELL_SIZE is a single number (e.g., 16) defined somewhere above this function.
	-- local CELL_SIZE = 16 

	local positions = {}
	local partCFrame = primaryPart.CFrame
	local partSize = primaryPart.Size

	-- 1. Determine the orientation and length of the plank.
	-- We can cleverly check which axis is the longest.
	local numCells
	local stepDirection

	if partSize.X > partSize.Z then
		-- This is an X-Oriented plank (e.g., Size is 48x1x16)
		numCells = math.round(partSize.X / CELL_SIZE) -- FIX: Removed .X
		stepDirection = partCFrame.RightVector * CELL_SIZE -- FIX: Removed .X
	else
		-- This is a Z-Oriented plank (e.g., Size is 16x1x48)
		numCells = math.round(partSize.Z / CELL_SIZE) -- FIX: Removed .Z (This was the line causing the error)
		stepDirection = partCFrame.LookVector * CELL_SIZE -- FIX: Removed .Z
	end

	-- 2. Calculate the center of the VERY FIRST cell.
	-- We start at the center of the whole PrimaryPart and move backwards
	-- by half the total span of the cell centers to get to the start point.
	local totalSpan = (numCells - 1) * CELL_SIZE
	local firstCellCenter = partCFrame.Position - (stepDirection.Unit * (totalSpan / 2))

	-- 3. Loop and calculate the center of each subsequent cell.
	for i = 0, numCells - 1 do
		local cellCenter = firstCellCenter + (stepDirection * i)
		table.insert(positions, cellCenter)
	end

	return positions
end

function Collectables.spawnAt(position, block)
	if not position or not block or not block.PrimaryPart then
		return nil
	end

	local newCoin = COIN_TEMPLATE:Clone()

	-- CRITICAL FIX 1: The coin must be unanchored to be welded to an unanchored block.
	newCoin.Anchored = false
	newCoin.CanCollide = false -- Also good practice
	newCoin.Parent = block

	-- We do not set the coin's CFrame directly, the weld will do this for us.
	local weld = Instance.new("Weld")
	weld.Part0 = newCoin             -- The part that moves (the coin)
	weld.Part1 = block.PrimaryPart   -- The static reference part (the block)
	weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(CFrame.new(position))
	weld.Parent = newCoin
	
	CollectionService:AddTag(newCoin, "GreenStar")
	
	local Bool
	local Connection = newCoin.Hitbox.Touched:Connect(function(otherPart)
		print(otherPart)
		Bool = onCoinTouched(otherPart, newCoin)
	end)
	if Bool == true then Connection:Disconnect() end
	return newCoin
end

--- A wrapper function that spawns a coin in every cell of a given block.
-- @param block The block model to spawn coins in.
function Collectables.spawnInBlock(block)
	local spawnPositions = getCellPositionsFromPlank(block)

	for _, pos in ipairs(spawnPositions) do
		Collectables.spawnAt(pos, block)
	end
end

--- Handles the server-side logic for collecting a coin.
function Collectables.collect(coin, player)
	if not coin then return end
	
	warn("		Warn(hey)")

	game.ReplicatedStorage.Sounds.CollectGreen:Play()	
	game.ReplicatedStorage.Sounds.Background:Play()

	local Emitter = coin.Attachment.ParticleEmitter
	Emitter.Enabled = false
	Emitter.LockedToPart = false
	Emitter.Speed = NumberRange.new(5,7)
	Emitter.Acceleration = Vector3.new(0, -2, 0)
	Emitter:Emit(25)
end

--- Deletes all "GreenStar" coins currently in the game.
function Collectables.deleteAllCoins()
	for _, coin in ipairs(CollectionService:GetTagged("GreenStar")) do
		coin:Destroy()
	end
end

--//============================ ANIMATION SYSTEM ============================//
function Collectables.Animate()
	-- Constants (assuming these are defined elsewhere in your script)
	local ROTATION_SPEED = 1
	local BOB_SPEED = 1

	-- Table to store each coin's weld and its original C0 (relative position to the parent block)
	local originalOffsets = {}

--[[
This function is called when a "GreenStar" tag is ADDED to an instance.
It adds the star's weld information to our tracking table.
]]
	local function AddStar(star)
		-- Don't add if it's already being tracked or if it's not a valid Part/Model
		if originalOffsets[star] or not star:IsA("BasePart") then return end

		local weld = star:FindFirstChildOfClass("Weld")

		-- Make sure the weld exists before trying to save it
		if weld then
			-- Save the weld and its original offset (C0) relative to the parent
			originalOffsets[star] = {
				weld = weld,
				baseC0 = weld.C0 -- This is the starting CFrame offset from the parent
			}
			print("Tracking new GreenStar:", star:GetFullName())
		end
	end

	local function RemoveStar(star)
		-- Check if we are actually tracking this star before trying to remove it
		if originalOffsets[star] then
			originalOffsets[star] = nil -- This removes the entry from the table
		end
	end
	
	task.spawn(function()
		CollectionService:GetInstanceAddedSignal("GreenStar"):Connect(AddStar)
		CollectionService:GetInstanceRemovedSignal("GreenStar"):Connect(RemoveStar)
	end)
	-- 2. Dynamic Updates: Connect to events to handle future changes.
	
--]]
	-- t will accumulate elapsed time for smooth animation
	local t = 0

	-- This function runs every frame (on the Heartbeat event)
	RunService.Heartbeat:Connect(function(dt)
		-- The expensive setup loop has been removed from here.

		t += dt -- Accumulate time to drive sine wave + rotation

		-- Calculate how much to rotate and bob this frame
		local angle = t * ROTATION_SPEED -- Rotation in radians over time
		local bobOffset = math.sin(t * BOB_SPEED * math.pi * 2) * 0.5 -- Vertical bobbing using sine wave

		-- Loop through all registered coins that we found at the start
		for coin, data in pairs(originalOffsets) do
			-- Make sure the coin and weld still exist
			-- Calculate a CFrame for rotation around Y-axis
			local rotation = CFrame.Angles(0, angle, 0)

			-- Calculate a CFrame for vertical bobbing (up and down)
			local bob = CFrame.new(0, bobOffset, 0)
			data.weld.C0 = (bob * rotation)*(CFrame.new(data.baseC0.Position))
			
		end
	end)
end


return Collectables

animate is called locally and so is collect. rest is by server.

Well, if it’s not that try replacing :

weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(CFrame.new(position))

:white_check_mark: With:

weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(CFrame.new(position) * CFrame.Angles(0, math.rad(0), 0))

Or better: capture full rotation, e.g.:

-- Create coin with a default rotation upright
local uprightCF = CFrame.new(position) * CFrame.Angles(0, 0, 0) -- You can tweak this angle
weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(uprightCF)

Then in animation:

data.weld.C0 = data.baseC0 * bob * rotation

Now it’ll probably work — because baseC0 starts with a known upright rotation, and you apply animation on top.

TL;DR

  • The problem isn’t .C0 = baseC0 * bob * rotation
  • The problem is that baseC0 had no proper rotation to begin with
  • So rotation gets applied to a part facing some random direction (based on spawn orientation)
  • Fix it at spawn time by rotating coins upright when setting the initial weld.C0

I really hope this works because otherwise, I honestly cannot think of another way right now.

1 Like

well i sorta found the problem which i guess sounds obvious but here it is. The weld cframing is fully correct. rotating and bobbing on its y axis.

the stars are the ones with there orientation showing off having 90 or - 90 in there z axis rotation.

Ohh, wait I’m cooking something up then.

1 Like

the issue unfortunately still persists. I even tried

weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(CFrame.new(position)* newCoin.CFrame.Rotation) 

but no hope

nevermind you cooked. forgot to change a value. Thanks a lot lol. I wouldnt have been able to figure it my self

Edit :slight_smile: :

Oh well, I’m just posting this anyways then and make sure to try and implement these to
further solidify your code!!! I gotchu anytime bro :?

Using newCoin.CFrame.Rotation actually kind of messes up everything because:

  • CFrame.Rotation returns a rotation matrix (CFrame) without translation, but when you multiply by CFrame.new(position) * newCoin.CFrame.Rotation, you’re combining position with the coin’s current rotation which might already be misaligned from the template.

Why this causes problems:

  • The template coin (COIN_TEMPLATE) may have a weird rotation from modeling, which gets copied every time you clone it.
  • So newCoin.CFrame.Rotation just duplicates the same “tilted” rotation every time.
  • weld.C0 ends up including that tilt, so your bobbing/rotating animation around Y acts on a tilted axis.


When you clone a star, it might be saved in Studio with an initial Orientation like:

Orientation = Vector3.new(0, 0, 90) -- tilted sideways

So now:

  • Its local Y-axis is no longer “up”
  • When you do:
C0 = baseC0 * CFrame.new(0, bobOffset, 0) * CFrame.Angles(0, t, 0)

you’re rotating around a tilted axis.


:white_check_mark: Hopeful Solution: Reset the rotation of the cloned star when spawning

Right after cloning the coin in spawnAt, insert this:

newCoin.CFrame = CFrame.new(newCoin.Position) -- strips rotation

Or, if you want to force it to face up properly:

newCoin.CFrame = CFrame.new(newCoin.Position) * CFrame.Angles(0, 0, 0)

If you’re using a hitbox inside the coin (like newCoin.Hitbox), reset that instead:

newCoin:SetPrimaryPartCFrame(CFrame.new(position)) -- if it's a model

Or if it’s a single part:

newCoin.CFrame = CFrame.new(position) -- makes it upright

Then try to apply the weld. Also, try using this :

weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(newCoin.CFrame)

Basic fix inside spawnAt:

Here’s how your spawn should go (example, obviously) :

function Collectables.spawnAt(position, block)
	if not position or not block or not block.PrimaryPart then
		return nil
	end

	local newCoin = COIN_TEMPLATE:Clone()

	newCoin.Anchored = false
	newCoin.CanCollide = false
	newCoin.Parent = block

	-- STRIP rotation, force upright:
	newCoin.CFrame = CFrame.new(position)

	local weld = Instance.new("Weld")
	weld.Part0 = newCoin
	weld.Part1 = block.PrimaryPart
	weld.C0 = block.PrimaryPart.CFrame:ToObjectSpace(newCoin.CFrame)
	weld.Parent = newCoin

	CollectionService:AddTag(newCoin, "GreenStar")

	-- ...
end

  • Now all coins start upright
  • Your animation around Y looks correct 100% of the time
  • No sideways or broken bobbing

Yet again, I hope this helps to further solidify your code.

1 Like

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