Is there any way I can improve my grid placement system?

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
1 Like

bumping because of no replies. I really want to know what y’all think of my code

In module.DeinitializeMap(player), table.remove(map, table.find(map, player.UserId)) is incorrect because map is a dictionary, not an array. You should instead remove the player’s entry with map[player.UserId] = nil. local touchingParts = game.Workspace:GetPartsInPart(part) if #touchingParts > 0 then for _, i in ipairs(touchingParts) do print(i) end part:Destroy() return end

As player maps grow in size, map[tostring(player.UserId)] could become big. Depending on game requirements u should optimize this by chunking or compressing data if needed.

1 Like

Consolidate repeated logic, use fewer remote calls, clean unused variables, simplify context action binding, improve event disconnects, try using CollectionService for grouping parts, reduce redundant code on server side placement, and try OOP Blocks

Try to encapsulate each block’s behavior inside a class/module
For example, you could create a Block class with methods like :Place(), :Remove(), :Highlight(), etc., instead of managing all this in the main script.
Quick example how to do that:

local Block = {}
Block.__index = Block

function Block.new(uid, position, rotation)
    local self = setmetatable({}, Block)
    self.uid = uid
    self.position = position
    self.rotation = rotation
    self.instance = ReplicatedStorage.Blocks:FindFirstChild(uid):Clone()
    return self
end

function Block:Place()
    -- Placement logic
end

function Block:Remove()
    -- Removal logic
end

return Block

If you want me to go more in-depth with any of my suggestions let me know.