Hello fellow people! Today, I created a tutorial for people on how to create a building system similar to the one in Rust.
Here is the video of what the final product will look like.
https://gyazo.com/a26c6e25bf3e42909a376a478ebb6197
What are trying to recreate in this tutorial?
We want to recreate a building system from the one in Rust using Attachments and Values.
Why are we using Attachments?
Well if we use Attachments we can easily snap parts together without doing addition math, and positioning
Why are we also using Values?
If we use Values it would be easier to determine on what we are doing, so we don’t have to use additional if statements in our script.
Why are we creating this type of building system?
Because, you can use this in your sandbox games, survival games, and maybe even a similar building system like Rust and BloxBurg.
Credits!
NotCasry - For creating this Devforum.
SmileyBoi - For the foundation and wall.
Lampy - For the idea of using attachments to create a build system.
RiverCryptz - For the idea of creating a module script named “Mouse”
Sources
BuildOpenSourceModel - Roblox
BuildOpenSourcePlace.rbxl (78.2 KB)
Intro and Mouse Module
Let’s start by importing the module script named “Mouse” created by me and the idea from RiverCryptz.
local module = {}
function module.GetMouse()
local Player = Players.LocalPlayer
local Mouse = Player:GetMouse()
return Mouse
end
function module.GetMouseRay(Mouse : Mouse, FilterType : Enum.RaycastFilterType, Filters : {Instance}, DirectionLength : number)
local UnitRay = Mouse.UnitRay
local Origin = UnitRay.Origin
local Direction = UnitRay.Direction * DirectionLength
local RaycastParams = RaycastParams.new()
RaycastParams.FilterType = FilterType
RaycastParams.FilterDescendantsInstances = Filters
local Result = workspace:Raycast(Origin, Direction, RaycastParams)
if Result and not Result.Instance then
return {
RaycastOrigin = UnitRay.Origin;
RaycastDirection = UnitRay.Direction * DirectionLength;
RaycastPosition = Result.Position;
RaycastNormal = Result.Normal;
RaycastDistance = Result.Distance;
}
elseif Result and Result.Instance then
return {
RaycastOrigin = UnitRay.Origin;
RaycastDirection = UnitRay.Direction * DirectionLength;
RaycastInstance = Result.Instance;
RaycastPosition = Result.Position;
RaycastNormal = Result.Normal;
RaycastDistance = Result.Distance;
RaycastMaterial = Result.Material;
}
elseif not Result then
return {
RaycastOrigin = nil;
RaycastDirection = nil;
RaycastInstance = nil;
RaycastPosition = nil;
RaycastNormal = nil;
RaycastDistance = nil;
RaycastMaterial = nil;
}
end
end
return module
Mouse Module: Mouse - Roblox
Important Note: Add this module into ReplicatedStorage!!
Getting the Values and Attachments Ready
Start of by adding 4 Attachments named “Wall” in the foundation, and name them all “Wall” because that’s the Attachment type we will be using for the snapping.
These orientations are for rotating the wall.
Left: 0, 0, 0
Right: 0, 0, 0
Top: 0, 90, 0
Bottom: 0, 90, 0
We also add a Bool Attribute in each Attachment named “Allowed” to determine if there is already a part placed their or not.
Next we get a wall and add 2 Values into it. One Object Value and One String Value.
The Object Value is the Attachment that has been selected, the String Value is to determine what Attachment we are going to snap too.
Next, we will add 2 Folders. One named “ClientObjects” and the other named “ServerObjects”. We will also have a RemoteFunction named “CallPlacement” to fire whenever we click the Left Mouse Button.
Lastly, we add a LocalScript in StarterGui named “ClientHandler”.
Now the Coding Part
To start of get all the services. Players, ReplicatedStorage, RunService, UserInputService.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
Next, we require the module.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local Mouse = require(ReplicatedStorage:WaitForChild("Mouse"))
Then, we get the player itself, the player character, and it’s mouse by calling the GetMouse function in the Module Script.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
After that, we get the part we want to place by defining it, cloning it, and setting it’s properties.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
Then, we create 2 variables, one named “GoodToPlace” set to false and “Attachment” set to nil.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
Next, we run the RenderStepped function, then we get the Raycast and the Raycast Instance
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
RunService.RenderStepped:Connect(function()
local Raycast = MouseModule.GetMouseRay(Mouse, Enum.RaycastFilterType.Blacklist, {ClonedSample, Character}, 10000)
if Raycast and Raycast.RaycastInstance then
end
end)
Then, we get the RaycastInstance, RaycastPosition, and create a variable name NearestAttachment that will equal to nil.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
RunService.RenderStepped:Connect(function()
local Raycast = MouseModule.GetMouseRay(Mouse, Enum.RaycastFilterType.Blacklist, {ClonedSample, Character}, 10000)
if Raycast and Raycast.RaycastInstance then
local RaycastInstance = Raycast.RaycastInstance
local RaycastPosition = Raycast.RaycastPosition
end
end)
After, getting both of those, we loop through every Attachment in the part, checking if it’s a Attachment and getting it’s position and orientation.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
RunService.RenderStepped:Connect(function()
local Raycast = MouseModule.GetMouseRay(Mouse, Enum.RaycastFilterType.Blacklist, {ClonedSample, Character}, 10000)
if Raycast and Raycast.RaycastInstance then
local RaycastInstance = Raycast.RaycastInstance
local RaycastPosition = Raycast.RaycastPosition
local NearestAttachment = nil
for _, attachment in pairs(RaycastInstance:GetChildren()) do
if attachment:IsA("Attachment") then
local AttachmentWorldPosition = attachment.WorldPosition
local AttachmentOrientation = attachment.Orientation
end
end
end
end)
Then, we check if the ClonedSample AttachmentType Value is equal to the Attachment Name. And, if it’s a yes then we get the nearest Attachment from the Raycast Position to the Attachment World Position using magnitude but also checking if the distance is below 5
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
RunService.RenderStepped:Connect(function()
local Raycast = MouseModule.GetMouseRay(Mouse, Enum.RaycastFilterType.Blacklist, {ClonedSample, Character}, 10000)
if Raycast and Raycast.RaycastInstance then
local RaycastInstance = Raycast.RaycastInstance
local RaycastPosition = Raycast.RaycastPosition
local NearestAttachment = nil
for _, attachment in pairs(RaycastInstance:GetChildren()) do
if attachment:IsA("Attachment") then
local AttachmentWorldPosition = attachment.WorldPosition
local AttachmentOrientation = attachment.Orientation
if (ClonedSample.AttachmentType.Value == attachment.Name) then
if (ClonedSample.Position - AttachmentWorldPosition).Magnitude < 5 then
end
end
end
end
end
end)
After, getting the closet attachment we check if the Attribute Allowed is set to false, and GoodToPlace is not equal to nil, if both of those return the values we wanted then we NearestAttachment equal to attachment and Attachment equal to attachment, and set GoodToPlace equal to true, the ClonedSample CFrame to the Attachment Position and it’s color to green.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
RunService.RenderStepped:Connect(function()
local Raycast = MouseModule.GetMouseRay(Mouse, Enum.RaycastFilterType.Blacklist, {ClonedSample, Character}, 10000)
if Raycast and Raycast.RaycastInstance then
local RaycastInstance = Raycast.RaycastInstance
local RaycastPosition = Raycast.RaycastPosition
local NearestAttachment = nil
for _, attachment in pairs(RaycastInstance:GetChildren()) do
if attachment:IsA("Attachment") then
local AttachmentWorldPosition = attachment.WorldPosition
local AttachmentOrientation = attachment.Orientation
if (ClonedSample.AttachmentType.Value == attachment.Name) then
if (ClonedSample.Position - AttachmentWorldPosition).Magnitude < 5 then
if (attachment:GetAttribute("Allowed") == false and GoodToPlace ~= nil) then
NearestAttachment = attachment
Attachment = attachment
ClonedSample.CFrame = CFrame.new(AttachmentWorldPosition) * CFrame.Angles(0, math.rad(AttachmentOrientation.Y), 0)
ClonedSample.Color = Color3.fromRGB(0, 255, 0)
GoodToPlace = true
end
end
end
end
end
end
end)
After, we did all of those steps, under the loop we check if NearestAttachment is equal to nil and Attachment is equal to nil and GoodToPlace is not equal to nil then we set GoodToPlace equal to false, change it’s cframe to the mouse cframe, and it’s color to red to show the player they can’t place it there.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
RunService.RenderStepped:Connect(function()
local Raycast = MouseModule.GetMouseRay(Mouse, Enum.RaycastFilterType.Blacklist, {ClonedSample, Character}, 10000)
if Raycast and Raycast.RaycastInstance then
local RaycastInstance = Raycast.RaycastInstance
local RaycastPosition = Raycast.RaycastPosition
local NearestAttachment = nil
for _, attachment in pairs(RaycastInstance:GetChildren()) do
if attachment:IsA("Attachment") then
local AttachmentWorldPosition = attachment.WorldPosition
local AttachmentOrientation = attachment.Orientation
if (ClonedSample.AttachmentType.Value == attachment.Name) then
if (ClonedSample.Position - AttachmentWorldPosition).Magnitude < 5 then
if (attachment:GetAttribute("Allowed") == false and GoodToPlace ~= nil) then
NearestAttachment = attachment
Attachment = attachment
ClonedSample.CFrame = CFrame.new(AttachmentWorldPosition) * CFrame.Angles(0, math.rad(AttachmentOrientation.Y), 0)
ClonedSample.Color = Color3.fromRGB(0, 255, 0)
GoodToPlace = true
end
end
end
end
end
if NearestAttachment == nil and GoodToPlace ~= nil then
ClonedSample.CFrame = CFrame.new(RaycastPosition.X, RaycastPosition.Y + ClonedSample.Size.Y/2, RaycastPosition.Z)
ClonedSample.Color = Color3.fromRGB(255, 0, 0)
GoodToPlace = false
end
end
end)
Last, we run a InputBegan function checking if gameProcessedEvent is equal to false, but also checking if the player pressed the Left Mouse Button. Next, we check if GoodToPlace is not equal to nil and GoodToPlace is equal to true, and we also check if Attachment is not equal to nil, and lastly we Invoke the Remote Function with the arguments of ClonedSample.Name, ClonedSample.CFrame, and the Attachment.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local MouseModule = require(ReplicatedStorage:WaitForChild("Mouse"))
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Mouse = MouseModule.GetMouse()
local Object = ReplicatedStorage:WaitForChild("ClientObjects"):WaitForChild("Wall")
local ClonedSample = Object:Clone()
ClonedSample.Parent = workspace
ClonedSample.Material = Enum.Material.Neon
ClonedSample.CanCollide = false
ClonedSample.Anchored = true
ClonedSample.Color = Color3.fromRGB(255, 0 ,0)
local GoodToPlace = false
local Attachment = nil
RunService.RenderStepped:Connect(function()
local Raycast = MouseModule.GetMouseRay(Mouse, Enum.RaycastFilterType.Blacklist, {ClonedSample, Character}, 10000)
if Raycast and Raycast.RaycastInstance then
local RaycastInstance = Raycast.RaycastInstance
local RaycastPosition = Raycast.RaycastPosition
local NearestAttachment = nil
for _, attachment in pairs(RaycastInstance:GetChildren()) do
if attachment:IsA("Attachment") then
local AttachmentWorldPosition = attachment.WorldPosition
local AttachmentOrientation = attachment.Orientation
if (ClonedSample.AttachmentType.Value == attachment.Name) then
if (ClonedSample.Position - AttachmentWorldPosition).Magnitude < 5 then
if (attachment:GetAttribute("Allowed") == false and GoodToPlace ~= nil) then
NearestAttachment = attachment
Attachment = attachment
ClonedSample.CFrame = CFrame.new(AttachmentWorldPosition) * CFrame.Angles(0, math.rad(AttachmentOrientation.Y), 0)
ClonedSample.Color = Color3.fromRGB(0, 255, 0)
GoodToPlace = true
end
end
end
end
end
if NearestAttachment == nil and GoodToPlace ~= nil then
ClonedSample.CFrame = CFrame.new(RaycastPosition.X, RaycastPosition.Y + ClonedSample.Size.Y/2, RaycastPosition.Z)
ClonedSample.Color = Color3.fromRGB(255, 0, 0)
GoodToPlace = false
end
end
end)
UserInputService.InputBegan:Connect(function(Input, gameProcessedEvent)
if gameProcessedEvent then return end
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
if GoodToPlace ~= nil and GoodToPlace then
if Attachment ~= nil then
ReplicatedStorage:WaitForChild("CallPlace"):InvokeServer(ClonedSample.Name, ClonedSample.CFrame, Attachment)
end
end
end
end)
Now, moving on to the Server Script which is really simple. We get ReplicatedStorage, then check if the RemoteFunction has been called and if it’s called then we check if the part exists under ReplicatedStorage ServerObjects and it’s not equal to nil. Then, we clone it, set it’s parent to workspace, set it’s cframe equal to ClientPartCFrame, set it’s AttachmentObject Value equal to ClientAttachment, and then set the Attachment Attribute Allowed equal to true.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
ReplicatedStorage:WaitForChild("CallPlace").OnServerInvoke = function(Player, ClientPartName, ClientPartCFrame, ClientAttachment)
if ReplicatedStorage:FindFirstChild("ServerObjects"):FindFirstChild(ClientPartName) ~= nil then
local clientPart = ReplicatedStorage:FindFirstChild("ServerObjects"):FindFirstChild(ClientPartName):Clone()
clientPart.Parent = workspace
clientPart.CFrame = ClientPartCFrame
clientPart.AttachmentObject.Value = ClientAttachment
ClientAttachment:SetAttribute("Allowed", true)
end
end
If you have any issues make sure to comment down below, and have the great rest of your day!