I’ve made a working grid placement system, which I am happy of how it turned out to be, but since I’m still learning Lua, there may be a lot of things I am missing on and could improve my code. I’m open to all criticism and plan on improving!
Here’s my code:
-- ClientPlacementScript
local module = {}
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ContextActionService = game:GetService("ContextActionService")
local Blocks = ReplicatedStorage:WaitForChild("Blocks")
local BlockPlaceEvent = ReplicatedStorage.RemoteEvents.Server:WaitForChild("BlockPlaceEvent")
local BlockRemoveEvent = ReplicatedStorage.RemoteEvents.Server:WaitForChild("BlockRemoveEvent")
local RemoteFunctions = workspace.RemoteFunctions;
local GetBlock = RemoteFunctions:WaitForChild("GetBlockByUIDRemoteFunction")
local GetItemCount = RemoteFunctions:WaitForChild("GetItemCountRemoteFunction")
local StartPlacingEvent: BindableEvent = ReplicatedStorage.BindableEvents:WaitForChild("PlayerStartPlacingEvent")
local StopPlacingEvent: BindableEvent = ReplicatedStorage.BindableEvents:WaitForChild("PlayerStopPlacingEvent")
local PlaceEvent: BindableEvent = ReplicatedStorage.BindableEvents:WaitForChild("PlayerPlaceBlockEvent")
local StartRemovingEvent: BindableEvent = ReplicatedStorage.BindableEvents:WaitForChild("PlayerStartRemovingEvent")
local StopRemovingEvent: BindableEvent = ReplicatedStorage.BindableEvents:WaitForChild("PlayerStopRemovingEvent")
local camera = workspace.CurrentCamera
local CELL_SIZE = workspace:WaitForChild("CellSize").Value
local previewConnect = nil
local partData = nil
local point = nil
local rotation = nil
local info = TweenInfo.new(.33, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out, 0, false, 0)
local playerId = game.Players.LocalPlayer.UserId
-----------------------------------
-- LOCAL FUNCTIONS ----------------
-----------------------------------
module.isPlacing = false;
module.isRemoving = false;
local function buildPreview(deltaTime: number)
if not partData then return end
local part = partData.Instance;
local params = RaycastParams.new()
params.FilterDescendantsInstances = {part, unpack(workspace.Parts:GetChildren()), unpack(workspace.Wind:GetChildren()), game.Players.LocalPlayer.Character}
params.FilterType = Enum.RaycastFilterType.Exclude
local mouse = game.Players.LocalPlayer:GetMouse()
local unitRay = camera:ScreenPointToRay(mouse.X, mouse.Y)
local ray = workspace:Raycast(unitRay.Origin, unitRay.Direction * 50, params);
if ray then
local pos = ray.Position
local nX = math.round(pos.X / CELL_SIZE)
local nZ = math.round(pos.Z / CELL_SIZE)
local nPoint = Vector3.new(nX * (CELL_SIZE), 4, nZ * (CELL_SIZE))
if point ~= nPoint then
local tween = TweenService:Create(part, info, {["Position"] = nPoint})
tween:Play()
end
point = nPoint;
else
point = nil
end
end
local removePart: BasePart? = nil;
local oldSelectedPart: BasePart? = nil;
local removeConnect = nil
local function removePreview(deltaTime: number)
if not removePart then return end
local params = RaycastParams.new()
params.FilterDescendantsInstances = {removePart, unpack(workspace.Parts:GetChildren()), unpack(workspace.Wind:GetChildren()), game.Players.LocalPlayer.Character}
params.FilterType = Enum.RaycastFilterType.Exclude
local mouse = game.Players.LocalPlayer:GetMouse()
local unitRay = camera:ScreenPointToRay(mouse.X, mouse.Y)
local ray = workspace:Raycast(unitRay.Origin, unitRay.Direction * 50, params);
if ray then
local pos = ray.Position
local nX = math.round(pos.X / CELL_SIZE)
local nZ = math.round(pos.Z / CELL_SIZE)
local nPoint = Vector3.new(nX * (CELL_SIZE), 6, nZ * (CELL_SIZE))
if point ~= nPoint and point then
removePart.Position = nPoint
if oldSelectedPart then
local h = oldSelectedPart:FindFirstChildOfClass("Highlight")
if h then
h:Destroy()
end
oldSelectedPart = nil
end
for _, part in ipairs(workspace:GetPartsInPart(removePart)) do
if part:GetAttribute("OwnerId") == playerId and part:GetAttribute("UniqueName") then
local highlight = Instance.new("Highlight")
highlight.OutlineColor = Color3.new(1, .15, .15)
oldSelectedPart = part;
highlight.Parent = oldSelectedPart
break
end
end
end
point = nPoint
else
point = nil
end
end
local function remove(action, state)
if state == Enum.UserInputState.Begin then
if point then
BlockRemoveEvent:FireServer(point)
end
end
end
local function place(action, state)
if state == Enum.UserInputState.Begin then
if point and rotation then
PlaceEvent:Fire(partData.UniqueName)
BlockPlaceEvent:FireServer(partData.UniqueName, point, rotation)
if GetItemCount:InvokeServer(partData.UniqueName) - 1 <= 0 then
module.StopPlacing()
end
end
end
end
local function rotate(action, state)
if state == Enum.UserInputState.Begin then
local y = rotation.y;
rotation = Vector3.new(0, (y + 90) % 360, 0);
local part = partData.Instance
local tween = TweenService:Create(part, info, {["Orientation"] = rotation})
tween:Play()
end
end
local function cleanupPlacing()
if previewConnect then
previewConnect:Disconnect()
previewConnect = nil
end
partData.Instance:Destroy()
partData = nil
ContextActionService:UnbindAction("Place")
ContextActionService:UnbindAction("Rotate")
end
local function cleanupRemoving()
if removeConnect then
removeConnect:Disconnect()
removeConnect = nil
end
if oldSelectedPart then
local h = oldSelectedPart:FindFirstChildOfClass("Highlight")
if h then
h:Destroy()
end
end
oldSelectedPart = nil
if removePart then
removePart:Destroy()
end
ContextActionService:UnbindAction("Remove")
end
--------------------------------
-- MODULE FUNCTIONS-------------
--------------------------------
function module.StartRemoving()
if module.isRemoving then return end
module.isRemoving = true
StartRemovingEvent:Fire()
if module.isPlacing then
cleanupPlacing()
StopPlacingEvent:Fire()
module.isPlacing = false
end
removePart = Instance.new("Part")
removePart.Size = Vector3.new(CELL_SIZE, CELL_SIZE, CELL_SIZE)
removePart.Parent = workspace
removePart.Anchored = true
removePart.CanCollide = false
removePart.Transparency = 1
removeConnect = RunService.RenderStepped:Connect(removePreview)
ContextActionService:BindAction("Remove", remove, false, Enum.UserInputType.MouseButton1)
ContextActionService:BindAction("StopRemoving", (function(_, state) if state == Enum.UserInputState.Begin then module.StopRemoving() end end), false, Enum.KeyCode.Q)
end
function module.StopRemoving()
module.isRemoving = false
if removeConnect then
removeConnect:Disconnect()
removeConnect = nil
end
if oldSelectedPart then
local highlight = oldSelectedPart:FindFirstChildOfClass("Highlight")
if highlight then
oldSelectedPart = nil
local tween = TweenService:Create(highlight, info, {FillTransparency = 1, OutlineTransparency = 1})
tween.Completed:Connect(function(playbackState: Enum.PlaybackState)
highlight:Destroy()
end)
tween:Play()
ContextActionService:UnbindAction("Remove")
ContextActionService:UnbindAction("StopRemoving")
end
else
cleanupRemoving()
end
StopRemovingEvent:Fire()
end
function module.StartPlacing(blockUid: string)
if module.isPlacing then return end
module.isPlacing = true;
if module.isRemoving then
cleanupRemoving()
module.isRemoving = false
end
if partData then
partData.Instance:Destroy()
end
partData = GetBlock:InvokeServer(blockUid)
partData.Instance = ReplicatedStorage.Blocks:FindFirstChild(partData.InstanceName, true):Clone()
point = Vector3.new(0, 0, 0)
rotation = Vector3.new()
local part = partData.Instance;
part.CanCollide = false;
part.Parent = workspace;
part.Position = point;
if not previewConnect then
previewConnect = RunService.RenderStepped:Connect(buildPreview)
end
StartPlacingEvent:Fire(partData.UniqueName)
ContextActionService:BindAction("Place", place, false, Enum.UserInputType.MouseButton1, Enum.UserInputType.Touch)
ContextActionService:BindAction("Rotate", rotate, true, Enum.KeyCode.R)
ContextActionService:BindAction("StopPlacing", (function(_, state) if state == Enum.UserInputState.Begin then module.StopPlacing() end end), false, Enum.KeyCode.Q)
end
function module.StopPlacing()
local InventoryBehaviour = require(game.Players.LocalPlayer.PlayerGui.ScreenGui.Inventory.InventoryBehaviour)
InventoryBehaviour.show()
module.isPlacing = false;
local part: Part = partData.Instance;
part.CastShadow = false
local tween = TweenService:Create(part, info, {["Transparency"] = 1})
tween.Completed:Connect(function()
part:Destroy()
end)
tween:Play()
ContextActionService:UnbindAction("Place")
ContextActionService:UnbindAction("Rotate")
ContextActionService:UnbindAction("StopPlacing")
partData = nil
previewConnect:Disconnect()
previewConnect = nil
StopPlacingEvent:Fire()
end
return module
-- ServerMapModule
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Inventory = require(script.Parent:WaitForChild("PlayerInventory"))
local BlockRegistry = require(script.Parent:WaitForChild("BlockRegistry"))
local BlockPlaceEvent = ReplicatedStorage.RemoteEvents.Client.PlayerPlaceEvent;
local module = {}
local map = {}
local function fixVector(v: Vector3)
local x = (v.X == -0 and 0 or v.X)
local y = (v.Y == -0 and 0 or v.Y)
local z = (v.Z == -0 and 0 or v.Z)
return Vector3.new(x, y, z)
end
function module.PlaceBlock(player: Player, blockUid: string, pos: Vector3, rot: Vector3)
pos = fixVector(pos)
local block = BlockRegistry.getDataByUID(blockUid)
local part: Part = block.Instance
if part then
part.Parent = workspace.Parts
part.Position = pos
part.Rotation = rot
if #game.Workspace:GetPartsInPart(part) > 0 then
part:Destroy()
for _, i in ipairs(game.Workspace:GetPartsInPart(part)) do
print(i)
end
return
end
local playerMap = module.GetMap(player)
local x = playerMap[tostring(pos.X)]
if not x or not x[tostring(pos.Z)] then
part:SetAttribute("OwnerId", player.UserId)
map[tostring(player.UserId)] = map[tostring(player.UserId)] or {}
map[tostring(player.UserId)][tostring(pos.X)] = map[tostring(player.UserId)][tostring(pos.X)] or {}
map[tostring(player.UserId)][tostring(pos.X)][tostring(pos.Z)] = {
UniqueName = blockUid,
Part = part,
PositionX = pos.X,
PositionY = pos.Y,
PositionZ = pos.Z,
RotationY = rot.Y
}
Inventory.TakeItem(player, blockUid)
BlockPlaceEvent:FireClient(player)
end
end
end
function module.RemoveBlock(player: Player, pos: Vector3)
pos = fixVector(pos)
local x = module.GetMap(player)[tostring(pos.X)]
if not x or not x[tostring(pos.Z)] then -- position empty
return
end
local part = x[tostring(pos.Z)].Part
if part:GetAttribute("UniqueName") then
Inventory.AddItem(player, part:GetAttribute("UniqueName"), 1)
end
part.CastShadow = false
local info = TweenInfo.new(.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local tween = TweenService:Create(part, info, {Transparency = 1})
tween.Completed:Connect(function(playbackState: Enum.PlaybackState)
part:Destroy()
end)
tween:Play()
map[tostring(player.UserId)][tostring(pos.X)][tostring(pos.Z)] = nil
end
function module.GetMap(player: Player)
return map[tostring(player.UserId)]
end
function module.InitializeMap(player: Player)
map[tostring(player.UserId)] = {}
end
function module.DeinitializeMap(player: Player)
table.remove(map, table.find(map, player.UserId))
end
return module