Creating a Rust Building System with Attachments and Raycasting!

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.
Step2_Image


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!

35 Likes

Why not just use Player:GetMouse() instead of calling a module to get the player’s mouse

1 Like

I don’t know I decided to add it.

can you put a showcase video at the end?

Player:GetMouse() is deprecated.

1 Like

Yes and so is the method he is using with the module he is still using the same Player:GetMouse() and returning that.

1 Like

Should I the use the UserInputService MouseLocation Function?

https://gyazo.com/a26c6e25bf3e42909a376a478ebb6197

Ik this is old but could u maybe send the code or file for the one u recorded cuz i cant get the position of the attachment right

image

THis is how i done it, but it not working right

Also @NotCasry a Video: robloxapp-20220910-0126402

In the script make sure you have worldposition not position

I don’t know how you obtained my assets, but cool tutorial nonetheless

No it’s not, nobody ever said that. It was only superseded by UserInputService, it was never deprecated.

It doesn’t mean it doesn’t work anymore.

Having the problems.
Not sure if it will ever be fixed

Couldn’t you just make pivot points?

I’m not that experienced of a programmer.

Compare your work to the sources to find any issues.

It does. Some people just have minor bugs that need to be fixed.