Car placement system clone broken

I am using this system for a car game which involves spawning and cloning cars the player can place anywhere. The placement module I am basing this of is here: How to use Placement Service I will provide the code below, there is also a video of the issue linked below as well. We are also using the: A-Chassis 6C by Novena

Video: Placement system - YouTube
Image of services:

Module:

-- SETTINGS

-- Bools
local interpolation = false -- Toggles interpolation (smoothing)
local moveByGrid = false-- Toggles grid system
local collisions = false -- Toggles collisions
local buildModePlacement = false -- Toggles "build mode" placement
local displayGridTexture = false -- Toggles the grid texture to be shown when placing
local smartDisplay = false -- Toggles smart display for the grid. If true, it will rescale the grid texture to match your gridsize
local enableFloors = false -- Toggles if the raise and lower keys will be enabled
local transparentModel = false -- Toggles if the model itself will be transparent
local instantActivation = true -- Toggles if the model will appear at the mouse position immediately when activating placement
local includeSelectionBox = true -- Toggles if a selection box will be shown while placing
local gridFadeIn = false -- If you want the grid to fade in when activating placement
local gridFadeOut = false -- If you want the grid to fade out when ending placement

-- Color3
local collisionColor = Color3.fromRGB(255, 75, 75) -- Color of the hitbox when colliding
local hitboxColor = Color3.fromRGB(75, 255, 75) -- Color of the hitbox while not colliding
local selectionColor = Color3.fromRGB(0, 255, 0) -- Color of the selectionBox lines (includeSelectionBox much be set to "true")
local selectionCollisionColor = Color3.fromRGB(255, 0, 0) -- Color of the selectionBox lines when colliding (includeSelectionBox much be set to "true")

-- Integers
local maxHeight = 100 -- Max height you can place objects (in studs)
local floorStep = 10 -- The step (in studs) that the object will be raised or lowered
local rotationStep = 90 -- Rotation step

-- Numbers/Floats
local hitboxTransparency = 0.8 -- Hitbox transparency when placing
local transparencyDelta = 0.6 -- Transparency of the model itself (transparentModel must equal true)
local lerpSpeed = 0.7 -- speed of interpolation. 0 = no interpolation, 0.9 = major interpolation
local placementCooldown = 0.5 -- How quickly the user can place down objects (in seconds)
local maxRange = 80 -- Max range for the model (in studs)
local lineThickness = 0.05 -- How thick the line of the selection box is (includeSelectionBox much be set to "true")
local lineTransparency = 0.8 -- How transparent the line of the selection box is (includeSelectionBox must be set to "true")

-- Other
local gridTexture = "rbxassetid://2415319308"

-- DO NOT EDIT PAST THIS POINT UNLESS YOU KNOW WHAT YOUR DOING.

local placement = {}

placement.__index = placement

-- Essentials
local runService = game:GetService("RunService")
local contextActionService = game:GetService("ContextActionService")

local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local mouse = player:GetMouse()	

-- math/cframe functions
local clamp = math.clamp
local floor = math.floor
local ceil = math.ceil
local abs = math.abs
local min = math.min
local pi = math.pi

local cframe = CFrame.new
local anglesXYZ = CFrame.fromEulerAnglesXYZ

-- states
local states = {
	"movement",
	"placing",
	"colliding",
	"in-active",
	"out-of-range"
}

local currentState = 4
local lastState = 4

-- Constructor variables
local GRID_UNIT
local itemLocation
local rotateKey
local terminateKey
local raiseKey
local lowerKey
local autoPlace

-- Activation variables
local plot
local object

-- bools
local canActivate = true
local currentRot = false
local running = false
local canPlace
local stackable
local smartRot
local range

-- values used for calculations
local speed = 1
local preSpeed = 1

local posX
local posY
local posZ
local rot
local x, z
local cx, cz

local LOWER_X_BOUND
local UPPER_X_BOUND

local LOWER_Z_BOUND
local UPPER_Z_BOUND

local initialY

-- collision variables
local collisionPoints
local collisionPoint

-- other
local placedObjects
local loc
local primary
local selection
local lastPlacement = {}
local humanoid = character:WaitForChild("Humanoid")

-- Sets the current state depending on input of function
local function setCurrentState(state)
	currentState = clamp(state, 1, 5)
	lastState = currentState
end

-- Changes the color of the hitbox depending on the current state
local function editHitboxColor()
	if primary then
		if currentState >= 3 then
			primary.Color = collisionColor
			selection.Color3 = selectionCollisionColor
		else
			primary.Color = hitboxColor
			selection.Color3 = selectionColor
		end
	end
end

-- Checks to see if the model is in range of the maxRange
local function getRange()
	return (primary.Position - character.PrimaryPart.Position).Magnitude
end

-- Checks for collisions on the hitbox (credit EgoMoose)
local function checkHitbox()
	if object and collisions then
		if range then
			setCurrentState(5)
		else
			setCurrentState(1)
		end

		collisionPoint = object.PrimaryPart.Touched:Connect(function() end)
		collisionPoints = object.PrimaryPart:GetTouchingParts()

		-- Checks if there is collision on any object that is not a child of the object and is not a child of the player
		for i = 1, #collisionPoints do
			if not collisionPoints[i]:IsDescendantOf(object) and not collisionPoints[i]:IsDescendantOf(character) then
				setCurrentState(3)

				break
			end
		end

		collisionPoint:Disconnect()

		return
	end
end

local function raiseFloor(actionName, inputState, inputObj)
	if currentState ~= 4 and inputState == Enum.UserInputState.Begin then
		if enableFloors and not stackable then
			posY = posY + floor(abs(floorStep))
		end
	end
end

local function lowerFloor(actionName, inputState, inputObj)
	if currentState ~= 4 and inputState == Enum.UserInputState.Begin then
		if enableFloors and not stackable then
			posY = posY - floor(abs(floorStep))
		end
	end
end

-- handles the grid texture
local function displayGrid()
	if displayGridTexture then
		local gridTex = Instance.new("Texture")

		gridTex.Name = "GridTexture"
		gridTex.Texture = gridTexture
		gridTex.Face = Enum.NormalId.Top
		gridTex.Transparency = 1

		gridTex.StudsPerTileU = 2
		gridTex.StudsPerTileV = 2

		if smartDisplay then
			gridTex.StudsPerTileU = GRID_UNIT
			gridTex.StudsPerTileV = GRID_UNIT	
		end

		if gridFadeIn then
			spawn(function()
				for i = 1, 0, -0.1 do
					if currentState ~= 4 then
						gridTex.Transparency = i

						wait()
					end
				end
			end)
		else
			gridTex.Transparency = 0
		end

		gridTex.Parent = plot
	end
end

local function rotate(actionName, inputState, inputObj)
	if currentState ~= 4 and inputState == Enum.UserInputState.Begin then
		if smartRot then
			-- Rotates the model depending on if currentRot is true/false
			if currentRot then
				rot = rot + rotationStep
			else 
				rot = rot - rotationStep
			end
		else
			rot = rot + rotationStep
		end

		-- Toggles currentRot
		currentRot = not currentRot
	end
end

-- Rounds any number to the nearest integer (credit iGottic)
local function round(number)
	local decimal_placement = 1 

	return (number % (1/decimal_placement) > 1/decimal_placement*0.5) and ceil(number*decimal_placement)/decimal_placement or floor(number*decimal_placement)/decimal_placement
end

-- Calculates the Y position to be ontop of the plot (all objects) and any object (when stacking)
local function calculateYPos(tp, ts, o)
	return (tp + ts*0.5) + o*0.5
end

-- Clamps the x and z positions so they cannot leave the plot
local function bounds()
	-- currentRot is here because if we rotate the model the offset is changed
	if currentRot then
		LOWER_X_BOUND = plot.Position.X - (plot.Size.X*0.5) 
		UPPER_X_BOUND = plot.Position.X + (plot.Size.X*0.5) - primary.Size.X

		LOWER_Z_BOUND = plot.Position.Z - (plot.Size.Z*0.5)	
		UPPER_Z_BOUND = plot.Position.Z + (plot.Size.Z*0.5) - primary.Size.Z
	else
		LOWER_X_BOUND = plot.Position.X - (plot.Size.X*0.5) 
		UPPER_X_BOUND = plot.Position.X + (plot.Size.X*0.5) - primary.Size.Z

		LOWER_Z_BOUND = plot.Position.Z - (plot.Size.Z*0.5)	
		UPPER_Z_BOUND = plot.Position.Z + (plot.Size.Z*0.5) - primary.Size.X
	end

	posX = clamp(posX, LOWER_X_BOUND, UPPER_X_BOUND)
	posZ = clamp(posZ, LOWER_Z_BOUND, UPPER_Z_BOUND)
end

-- Calculates the position of the object
local function calculateItemLocation()
	if currentRot then
		x, z = mouse.Hit.X - primary.Size.X*0.5, mouse.Hit.Z - primary.Size.Z*0.5

		cx = primary.Size.X*0.5
		cz = primary.Size.Z*0.5
	else
		x, z = mouse.Hit.X - primary.Size.Z*0.5, mouse.Hit.Z - primary.Size.X*0.5

		cx = primary.Size.Z*0.5
		cz = primary.Size.X*0.5
	end

	if moveByGrid then
		-- Snaps models to grid
		if x % GRID_UNIT < GRID_UNIT*0.5 then
			posX = round(x - (x % GRID_UNIT))
		else
			posX = round(x + (GRID_UNIT - (x % GRID_UNIT)))
		end

		if z % GRID_UNIT < GRID_UNIT*0.5 then
			posZ = round(z - (z % GRID_UNIT))
		else
			posZ = round(z + (GRID_UNIT - (z % GRID_UNIT)))
		end
	else
		posX = x
		posZ = z
	end

	-- Changes posY depending on mouse target
	if stackable and mouse.Target then
		posY = calculateYPos(mouse.Target.Position.Y, mouse.Target.Size.Y, primary.Size.Y)
	end

	-- Clamps posY to a max height above the plot position
	posY = clamp(posY, initialY, maxHeight + initialY)

	bounds()
end

--[[
	Used for sending a final CFrame to the server when using interpolation.
	When interpolating the position is changing. This is the position the object will
	end up after the lerp is finished.
]]
local function getFinalCFrame()
	return cframe(posX, posY, posZ)*cframe(cx, 0, cz)*anglesXYZ(0, rot*pi/180, 0)
end

-- Sets the position of the object
local function translateObj()
	if currentState ~= 4 then
		calculateItemLocation()
		checkHitbox()
		editHitboxColor()

		if getRange() > maxRange then
			setCurrentState(5)

			range = true
		else
			range = false
		end

		object:SetPrimaryPartCFrame(primary.CFrame:Lerp(cframe(posX, posY, posZ)*cframe(cx, 0, cz)*anglesXYZ(0, rot*pi/180, 0), speed))
	end
end

local function unbindInputs()
	contextActionService:UnbindAction("Rotate")
	contextActionService:UnbindAction("Raise")
	contextActionService:UnbindAction("Lower")
	contextActionService:UnbindAction("Terminate")
end

local function bindInputs()
	contextActionService:BindAction("Rotate", rotate, false, rotateKey)
	contextActionService:BindAction("Raise", raiseFloor, false, raiseKey)
	contextActionService:BindAction("Lower", lowerFloor, false, lowerKey)
	contextActionService:BindAction("Terminate", TERMINATE_PLACEMENT, false, terminateKey)
end

function TERMINATE_PLACEMENT()
	if object then
		if selection then
			selection:Destroy()
			selection = nil
		end

		stackable = nil
		canPlace = nil
		smartRot = nil

		object:Destroy()
		object = nil

		setCurrentState(4)

		-- removes grid texture from plot
		if displayGridTexture then
			for i, v in next, plot:GetChildren() do
				if v then
					if v.Name == "GridTexture" and v:IsA("Texture") then
						if gridFadeOut then
							for i = v.Transparency, 1, 0.1 do
								v.Transparency = i

								wait()
							end

							v:Destroy()
						else
							v:Destroy()
						end	
					end
				end
			end
		end

		canActivate = true

		unbindInputs()

		mouse.TargetFilter = nil

		return
	end
end

-- Makes sure that you cannot place objects too fast.
local function coolDown(plr, cd)
	if lastPlacement[plr.UserId] == nil then
		lastPlacement[plr.UserId] = tick()

		return true
	else
		if tick() - lastPlacement[plr.UserId] >= cd then
			lastPlacement[plr.UserId] = tick()

			return true
		else
			return false
		end
	end
end

local function PLACEMENT(func)
	if currentState ~= 3 and currentState ~= 4 and currentState ~= 5 and object then
		local cf

		calculateItemLocation()

		-- Makes sure you have waited the cooldown period before placing
		if coolDown(player, placementCooldown) then
			-- Buildmode placement is when you can place multiple objects in one session
			if buildModePlacement then
				cf = getFinalCFrame()

				checkHitbox()
				setCurrentState(2)

				-- Sends information to the server, so the object can be placed
				if currentState == 2 then
					func:InvokeServer(object.Name, placedObjects, loc, cf, collisions, plot)

					setCurrentState(1)
				end
			else
				cf = getFinalCFrame()

				checkHitbox()
				setCurrentState(2)

				if currentState == 2 then
					-- Same as above (line 509)
					if func:InvokeServer(object.Name, placedObjects, loc, cf, collisions, plot) then
						TERMINATE_PLACEMENT()
					end
				end
			end
		end
	end
end

-- Verifys that the plane which the object is going to be placed upon is the correct size
local function verifyPlane()	
	if plot.Size.X%GRID_UNIT == 0 and plot.Size.Z%GRID_UNIT == 0 then
		return true
	else
		return false
	end
end

-- Checks if there are any problems with the users setup
local function approveActivation()
	if not verifyPlane() then
		warn("The object that the model is moving on is not scaled correctly. Consider changing it.")
	end

	if GRID_UNIT > min(plot.Size.X, plot.Size.Z) then 
		error("Grid size is larger than the plot size. To fix this, try lowering the grid size.")
	end
end

-- Constructor function
function placement.new(g, objs, r, t, u, l)
	local data = {}
	local metaData = setmetatable(data, placement)

	-- Sets variables needed
	GRID_UNIT = abs(tonumber(g))
	itemLocation = objs
	rotateKey = r
	terminateKey = t
	raiseKey = u
	lowerKey = l

	data.gridsize = GRID_UNIT
	data.items = objs
	data.rotate = rotateKey
	data.cancel = terminateKey
	data.raise = raiseKey
	data.lower = lowerKey

	return data 
end

-- returns the current state when called
function placement:getCurrentState()
	return states[currentState]
end

-- Pauses the current state
function placement:pauseCurrentState()
	lastState = currentState

	if object then
		currentState = states[4]
	end
end

-- Resumes the current state if paused
function placement:resume()
	if object then
		setCurrentState(lastState)
	end
end

-- Terminates placement
function placement:terminate()
	TERMINATE_PLACEMENT()
end

function placement:haltPlacement()
	if autoPlace then
		if running then
			running = false
		end
	end
end

-- Requests to place down the object
function placement:requestPlacement(func)
	if autoPlace then
		running = true

		repeat
			PLACEMENT(func)

			wait(placementCooldown)
		until not running
	else
		PLACEMENT(func)
	end
end

-- Activates placement
function placement:activate(id, pobj, plt, stk, r, a)
	TERMINATE_PLACEMENT()

	-- Sets necessary variables for placement 
	plot = plt
	object = itemLocation:FindFirstChild(tostring(id)):Clone()
	placedObjects = pobj
	loc = itemLocation

	approveActivation()

	-- Sets properties of the model (CanCollide, Transparency)
	for i, o in next, object:GetDescendants() do
		if o then
			if o:IsA("Part") or o:IsA("UnionOperation") or o:IsA("MeshPart") then
				o.CanCollide = false
				o.Anchored = true

				if transparentModel then
					o.Transparency = o.Transparency + transparencyDelta
				end
			end
		end
	end

	if includeSelectionBox then	
		selection = Instance.new("SelectionBox")
		selection.Name = "outline"
		selection.LineThickness = lineThickness
		selection.Color3 = selectionColor
		selection.Transparency = lineTransparency
		selection.Parent = player.PlayerGui
		selection.Adornee = object.PrimaryPart
	end

	object.PrimaryPart.Transparency = hitboxTransparency

	stackable = stk
	smartRot = r

	-- Allows stackable objects depending on stk variable given by the user
	if not stk then
		mouse.TargetFilter = placedObjects
	else
		mouse.TargetFilter = object
	end

	-- Toggles buildmode placement (infinite placement) depending on if set true by the user
	if buildModePlacement then
		canActivate = true
	else
		canActivate = false
	end

	-- Gets the initial y pos and gives it to posY
	initialY = calculateYPos(plt.Position.Y, plt.Size.Y, object.PrimaryPart.Size.Y)
	posY = initialY

	speed = 0
	rot = 0
	currentRot = true
	autoPlace = a

	translateObj()
	displayGrid()
	editHitboxColor()
	bindInputs()

	-- Sets up interpolation speed
	speed = 1

	if interpolation then
		preSpeed = clamp(abs(tonumber(1 - lerpSpeed)), 0, 0.9)

		if instantActivation then
			speed = 1
		else
			speed = preSpeed
		end
	end

	-- Parents the object to the location given
	if object then
		primary = object.PrimaryPart
		setCurrentState(1)
		object.Parent = pobj

		wait()

		speed = preSpeed
	else
		TERMINATE_PLACEMENT()

		warn("Your trying to activate placement too fast! Please slow down")
	end
end

runService:BindToRenderStep("Input", Enum.RenderPriority.Input.Value, translateObj)

return placement

Server handler:

local replicatedStorage = game:GetService("ReplicatedStorage")

local function checkHitbox(character, object)
	if object then
		local collided = false

		local collisionPoint = object.PrimaryPart.Touched:Connect(function() end)
		local collisionPoints = object.PrimaryPart:GetTouchingParts()

		for i = 1, #collisionPoints do
			if not collisionPoints[i]:IsDescendantOf(object) and not collisionPoints[i]:IsDescendantOf(character) then
				collided = true

				break
			end
		end

		collisionPoint:Disconnect()

		return collided
	end
end

local function checkBoundaries(plot, primary)
	local lowerXBound
	local upperXBound

	local lowerZBound
	local upperZBound

	local currentPos = primary.Position

	lowerXBound = plot.Position.X - (plot.Size.X*0.5) 
	upperXBound = plot.Position.X + (plot.Size.X*0.5)

	lowerZBound = plot.Position.Z - (plot.Size.Z*0.5)	
	upperZBound = plot.Position.Z + (plot.Size.Z*0.5)

	return currentPos.X > upperXBound or currentPos.X < lowerXBound or currentPos.Z > upperZBound or currentPos.Z < lowerZBound
end

local function ChangeTransparency(item, c)
	for i, o in next, item:GetDescendants() do
		if o then
			if o:IsA("Part") or o:IsA("UnionOperation") or o:IsA("MeshPart") then
				o.Transparency = c
			end
		end
	end
end


local function place(plr, name, location, prefabs, cframe, c, plot)
	local item = prefabs:FindFirstChild(name):Clone()
	item.PrimaryPart.CanCollide = false
	item:SetPrimaryPartCFrame(cframe)

	ChangeTransparency(item, 1)

	if checkBoundaries(plot, item.PrimaryPart) then
		return
	end

	item.Parent = location

	if c then
		if not checkHitbox(plr.Character, item) then
			ChangeTransparency(item, 0)

			item.PrimaryPart.Transparency = 1

			return true
		else
			item:Destroy()

			return false
		end
	else
		ChangeTransparency(item, 0)

		item.PrimaryPart.Transparency = 1

		return true
	end
end

replicatedStorage.Remotes.requestPlacement.OnServerInvoke = place

Client handler:

local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")

local player = players.LocalPlayer
local mouse = player:GetMouse()

local remote = replicatedStorage.Remotes:WaitForChild("requestPlacement")
local button = script.Parent

local placementModule = require(replicatedStorage.Modules:WaitForChild("PlacementModule"))

local placement = placementModule.new(
	2,
	replicatedStorage.Models,
	Enum.KeyCode.R, Enum.KeyCode.X, Enum.KeyCode.U, Enum.KeyCode.L
)

button.MouseButton1Click:Connect(function()
	placement:activate("1970 Edge Dividend", workspace.base.itemHolder, workspace.base, false, false)
end)

mouse.Button1Down:Connect(function()
	placement:requestPlacement(remote)
end)

Disclaimer right and the car is named the 1970 Edge Dividend but instead

Any help is appreciated! Thanks.

I am not sure what the issue exactly is, but if you’re talking about the parts that are invisible and become visible after placement, this is because I forgot to change the server code to not do that. This code should work:

local replicatedStorage = game:GetService("ReplicatedStorage")

local function checkHitbox(character, object)
	if object then
		local collided = false

		local collisionPoint = object.PrimaryPart.Touched:Connect(function() end)
		local collisionPoints = object.PrimaryPart:GetTouchingParts()

		for i = 1, #collisionPoints do
			if not collisionPoints[i]:IsDescendantOf(object) and not collisionPoints[i]:IsDescendantOf(character) then
				collided = true

				break
			end
		end

		collisionPoint:Disconnect()

		return collided
	end
end

local function checkBoundaries(plot, primary)
	local lowerXBound
	local upperXBound

	local lowerZBound
	local upperZBound

	local currentPos = primary.Position

	lowerXBound = plot.Position.X - (plot.Size.X*0.5) 
	upperXBound = plot.Position.X + (plot.Size.X*0.5)

	lowerZBound = plot.Position.Z - (plot.Size.Z*0.5)	
	upperZBound = plot.Position.Z + (plot.Size.Z*0.5)

	return currentPos.X > upperXBound or currentPos.X < lowerXBound or currentPos.Z > upperZBound or currentPos.Z < lowerZBound
end


local function place(plr, name, location, prefabs, cframe, c, plot)
	local item = prefabs:FindFirstChild(name):Clone()
	item.PrimaryPart.CanCollide = false
	item:SetPrimaryPartCFrame(cframe)

	if checkBoundaries(plot, item.PrimaryPart) then
		return
	end

	item.Parent = location

	if c then
		if not checkHitbox(plr.Character, item) then
			item.PrimaryPart.Transparency = 1

			return true
		else
			item:Destroy()

			return false
		end
	else
		item.PrimaryPart.Transparency = 1

		return true
	end
end

replicatedStorage.Remotes.requestPlacement.OnServerInvoke = place

Sorry about this, I realized this and forgot to update it in the module code and tutorials most likely as well. The next version of the module will fix this though! Hope this helped!

2 Likes

Thanks I will try it tomorrow and if it works I will mark it as solution! Thank you and nice work with your module!

1 Like