Hello, I recently released a third placement module that handles most things you need for a sandbox tycoon placement system (example: Miners Haven). How clean, easy to understand and easy to use is the module?
Here is a sample place for you to use:
placement.rbxl (39.1 KB)
I mainly want to improve things (if there are some) that may not be the best such as repeated code, inefficient code or anything else that could be wrong with the module.
Module Code
-- SETTINGS
-- Bools
local interpolation = true -- Toggles interpolation (smoothing)
local moveByGrid = true -- Toggles grid system
local collisions = true -- Toggles collisions
local buildModePlacement = false -- Toggles "build mode" placement
local displayGridTexture = true -- 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 = true -- Toggles if the raise and lower keys will be enabled
local transparentModel = true -- Toggles if the model itself will be transparent
-- 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
-- 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
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)
-- 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 userInputService = game:GetService("UserInputService")
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 abs = math.abs
local max = math.max
local pi = math.pi
local cframe = CFrame.new
local anglesXYZ = CFrame.fromEulerAnglesXYZ
-- Constructor variables
local grid
local itemLocation
local rotateKey
local terminateKey
local raiseKey
local lowerKey
-- Activation vatiables
local plot
local object
-- bools
local canPlace
local isColliding
local stackable
local smartRot
local canActivate = true
local currentRot = false
-- values used for calculations
local speed
local posX
local posY
local posZ
local rot
local x, z
local cx, cz
local lowerXBound
local upperXBound
local lowerZBound
local upperZBound
local initialY
-- collision variables
local collisionPoints
local collisionPoint
local collided
-- states
local states = {
"movement",
"placing",
"colliding",
"in-active"
}
local currentState = 4
local lastState = 4
-- other
local placedObjects
local loc
local primary
local lastPlacement = {}
local humanoid = character:WaitForChild("Humanoid")
local function setCurrentState(state)
currentState = clamp(state, 1, 4)
lastState = currentState
end
-- Changes the color of the hitbox depending on the current state
local function editHitboxColor()
if currentState == 3 then
object.PrimaryPart.Color = collisionColor
else
object.PrimaryPart.Color = hitboxColor
end
end
-- Checks for collisions on the hitbox
local function checkHitbox()
if object and collisions then
setCurrentState(1)
collisionPoint = object.PrimaryPart.Touched:Connect(function() end)
collisionPoints = object.PrimaryPart:GetTouchingParts()
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 collided
end
end
-- switches the floor depending on the value given
local function editFloor(f)
if enableFloors and not stackable then
if f == 1 then
posY = posY + floor(abs(floorStep))
else
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.Parent = plot
gridTex.Face = Enum.NormalId.Top
gridTex.StudsPerTileU = 2
gridTex.StudsPerTileV = 2
if smartDisplay then
gridTex.StudsPerTileU = grid
gridTex.StudsPerTileV = grid
end
end
end
local function rotate()
if smartRot then
if currentRot then
rot = rot + rotationStep
else
rot = rot - rotationStep
end
else
rot = rot + rotationStep
end
currentRot = not currentRot
end
local function round(n)
if (n - (n % 0.1)) - (n - (n % 1)) < 0.5 then
n = n - (n % 1)
else
n = (n - (n % 1)) + 1
end
return n
end
local function calculateYPos(tp, ts, o)
return (tp + ts / 2) + o / 2
end
local function bounds()
if currentRot then
lowerXBound = plot.Position.X - (plot.Size.X / 2)
upperXBound = plot.Position.X + (plot.Size.X / 2) - primary.Size.X
lowerZBound = plot.Position.Z - (plot.Size.Z / 2)
upperZBound = plot.Position.Z + (plot.Size.Z / 2) - primary.Size.Z
else
lowerXBound = plot.Position.X - (plot.Size.X / 2)
upperXBound = plot.Position.X + (plot.Size.X / 2) - primary.Size.Z
lowerZBound = plot.Position.Z - (plot.Size.Z / 2)
upperZBound = plot.Position.Z + (plot.Size.Z / 2) - primary.Size.X
end
posX = clamp(posX, lowerXBound, upperXBound)
posZ = clamp(posZ, lowerZBound, upperZBound)
end
-- Calculates the position of the object
local function calculateItemLocation()
x, z = mouse.Hit.X, mouse.Hit.Z
if moveByGrid then
if x % grid < grid / 2 then
posX = round(x - (x % grid))
else
posX = round(x + (grid - (x % grid)))
end
if z % grid < grid / 2 then
posZ = round(z - (z % grid))
else
posZ = round(z + (grid - (z % grid)))
end
if currentRot then
cx = primary.Size.X / 2
cz = primary.Size.Z / 2
else
cx = primary.Size.Z / 2
cz = primary.Size.X / 2
end
else
posX = x
posZ = z
end
if stackable and mouse.Target then
posY = calculateYPos(mouse.Target.Position.Y, mouse.Target.Size.Y, primary.Size.Y)
end
posY = clamp(posY, initialY, maxHeight + initialY)
bounds()
end
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()
object:SetPrimaryPartCFrame(primary.CFrame:Lerp(cframe(posX, posY, posZ) * cframe(cx, 0, cz) * anglesXYZ(0, rot * pi / 180, 0), speed))
end
end
-- handles user input
local function getInput(input, gpe)
if currentState ~= 4 then
if input.KeyCode == raiseKey then
editFloor(1)
elseif input.KeyCode == lowerKey then
editFloor(2)
elseif input.KeyCode == terminateKey then
placement:terminate()
elseif input.KeyCode == rotateKey then
rotate()
end
end
end
local function coolDown(plr, cd)
if lastPlacement[plr.UserId] == nil then
lastPlacement[plr.UserId] = os.time()
return true
else
if os.time() - lastPlacement[plr.UserId] >= cd then
lastPlacement[plr.UserId] = os.time()
return true
else
return false
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 == 0 and plot.Size.Z % grid == 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 > max(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
-- Sets up placement
function placement.new(g, objs, r, t, u, l)
local data = {}
local metaData = setmetatable(data, placement)
grid = abs(tonumber(g))
itemLocation = objs
rotateKey = r
terminateKey = t
raiseKey = u
lowerKey = l
data.gridsize = grid
data.items = objs
data.rotate = rotateKey
data.cancel = terminateKey
data.raise = raiseKey
data.lower = lowerKey
return data
end
function placement:getCurrentState()
return states[currentState]
end
function placement:pauseCurrentState()
lastState = currentState
if object then
currentState = states[4]
end
end
function placement:resume()
if object then
setCurrentState(lastState)
end
end
-- Terminates placement
function placement:terminate()
stackable = nil
canPlace = nil
smartRot = nil
object:Destroy()
object = nil
if displayGridTexture then
for i, v in next, plot:GetChildren() do
if v then
if v.Name == "GridTexture" and v:IsA("Texture") then
v:Destroy()
end
end
end
end
setCurrentState(4)
canActivate = true
return
end
-- Requests to place down the object
function placement:requestPlacement(func)
if currentState ~= 4 or currentState ~= 3 and object then
local cf
calculateItemLocation()
if coolDown(player, placementCooldown) then
if buildModePlacement then
cf = getFinalCFrame()
checkHitbox()
setCurrentState(2)
if currentState == 2 then
func:InvokeServer(object.Name, placedObjects, loc, cf, collisions)
setCurrentState(1)
end
else
cf = getFinalCFrame()
checkHitbox()
setCurrentState(2)
if currentState == 2 then
if func:InvokeServer(object.Name, placedObjects, loc, cf, collisions) then
placement:terminate()
end
end
end
end
end
end
-- Activates placement
function placement:activate(id, pobj, plt, stk, r)
if object then
object:Destroy()
object = nil
end
plot = plt
object = itemLocation:FindFirstChild(tostring(id))
placedObjects = pobj
loc = itemLocation
approveActivation()
object = itemLocation:FindFirstChild(id):Clone()
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
if transparentModel then
o.Transparency = o.Transparency + transparencyDelta
end
end
end
end
object.PrimaryPart.Transparency = hitboxTransparency
stackable = stk
smartRot = r
if not stk then
mouse.TargetFilter = placedObjects
else
mouse.TargetFilter = object
end
if buildModePlacement then
canActivate = true
else
canActivate = false
end
initialY = calculateYPos(plt.Position.Y, plt.Size.Y, object.PrimaryPart.Size.Y)
posY = initialY
speed = 0
rot = 0
currentRot = true
translateObj()
displayGrid()
editHitboxColor()
if interpolation then
speed = clamp(abs(tonumber(1 - lerpSpeed)), 0, 0.9)
else
speed = 1
end
primary = object.PrimaryPart
object.Parent = pobj
setCurrentState(1)
end
runService:BindToRenderStep("Input", Enum.RenderPriority.Input.Value, translateObj)
userInputService.InputBegan:Connect(getInput)
return placement
Thanks!