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.