Creating A Fortnite Building - Tutorial for Intermediate Developers

Fortnite building is what sets it apart from other shooters in the battle royale arena, as being able to throw up walls, ramps, and other structures on the fly can completely change the dynamic of a fight.

I will teach you how to easily recreate this Mechanic on Roblox Studio.

Step 1:
We will first create our Local and Module Scripts which would be used on the client to calculate the position and rotation of the build.
image
Scripts Definitions:
Main - This will be used to change the Rotation and Position of the build.
BuildManagerComponent - This would be use for functions and variables which would be stored inside.
Step 2:
Inside the BuildManagerComponent Module we want to start creating the module. First we will declare our settings and variables inside.

local BuildManagerComponent = {} -- Module table
--|| MODULE SETTINGS ||--
BuildManagerComponent.GridSize = 16 -- The GridSize which would be used for GridSnapping the build location and rotation.
BuildManagerComponent.BuildDistance = 2 -- How far we can build
--|| MODULE VARIABLES ||--
BuildManagerComponent.isBuilding = false
BuildManagerComponent.SelectedBuild = "Wall" -- We set this to wall so when build mode is toggled the first build is Wall. You can change this to Wall Ramp or Floor depending on what you want it to be.
--|| TABLES & DICTIONARIES ||--
local BuildingKeybinds = {
	[Enum.KeyCode.Q] = "Wall", -- Building Keybind for a Wall
	[Enum.KeyCode.C] = "Floor", -- Building Keybind for a Floor
	[Enum.KeyCode.V] = "Ramp" -- Building Keybind for a Ramp
}

Now we create the GridSnapping function

--|| PRIVATE FUNCTIONS ||--
local function GridSnap(Value, Size)
	return (math.floor(Value/Size + 0.5) * Size) --  Rounding down the Quotient of Value and Size + 0.5 then multiplying it by the size.
end

Now its time to create the functions which calculate the next build position and next build rotation.

--|| MODULE FUNCTIONS ||--
function BuildManagerComponent.GetNextBuildPosition(HumanoidRootPartPosition, MouseLookVector3)
	local DirectionVector3 = MouseLookVector3 * BuildManagerComponent.BuildDistance
	DirectionVector3 += HumanoidRootPartPosition
	return Vector3.new(
		GridSnap(DirectionVector3.X, BuildManagerComponent.GridSize),
		GridSnap(DirectionVector3.Y, BuildManagerComponent.GridSize)  + BuildManagerComponent.GridSize/2,
		GridSnap(DirectionVector3.Z, BuildManagerComponent.GridSize)
	)
end
function BuildManagerComponent.GetNextBuildRotation(Vector)
	if(typeof(Vector) == "Vector3") then -- Check if the given Parameter is a Vector3
		local Y = math.atan2(Vector.X, Vector.Z) -- Getting the Arc Tangent in Radians of the Vector which would be Mouse.Hit.LookVector
		return Vector3.new(0,GridSnap(Y, math.rad(-90)), 0) -- Grid Snap the Arc Tangent to -90 in Radians.
	end
end

After we are done with the main functions, Its time for the Keybind functions.

--|| TOGGLE BUILDING ||--
function BuildManagerComponent.ToggleBuildMode(ActionName, InputState, InputObject)
	if ActionName == "ToggleBuild" then
		if InputState == Enum.UserInputState.Begin then
			BuildManagerComponent.isBuilding = not BuildManagerComponent.isBuilding -- Setting isBuilding to not isBuilding to get a toggle effect, true - false, false - true
			warn("Toggled Build Mode: ", BuildManagerComponent.isBuilding)
		end
	end
end
--|| SWITCH TO A DIFFERENT BUILD( Ramp, Wall, Floor, Etc) ||--
function BuildManagerComponent.SwitchBuild(ActionName, InputState, InputObject)
	if(ActionName == "SwitchBuild") then
		if(InputState == Enum.UserInputState.Begin) then
			BuildManagerComponent.isBuilding = true
			if(BuildManagerComponent.isBuilding and BuildingKeybinds[InputObject.KeyCode]) then
				BuildManagerComponent.SelectedBuild = BuildingKeybinds[InputObject.KeyCode] -- Setting the Selected build based on the Keybind Clicked
			end
		end
	end
end

Step 3:
We also need to make sure we have our builds ready. The model to the builds I used are here.
Wall - https://www.roblox.com/library/6646973363/Walls
Floor - https://www.roblox.com/library/6646979337/Floors
Ramps/Stairs - https://www.roblox.com/library/6646985030/Ramps
For the previews I set the transparency to 0.7 then set the texture id to - rbxassetid://6206958104

If you are making your own models(In studio) Make sure that the ramp has a box collision. You can do this by making a part. Rotation the Orientation to Vector3.new(45,0,0) then Exporting the object then importing it back to roblox.

Step 4
Now that we are done with that we want to start working on the Local Script. First we start off by adding a new folder inside the local script called BuildMeshes then another folder inside of the Folder we just created called Preview. Now we Insert the preview meshes to the Preview Folder. Make sure each mesh has CanCollide turned off and Massless and Anchored turned on.
image

Inside the local script we define our game services, modules, variables and folders.

--|| SERVICES ||--
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
--|| MODULES ||--
local BuildManagerComponentModule = require(script:WaitForChild("BuildManagerComponent"))
--|| VARIABLES ||--
local Player = Players.LocalPlayer
local Mouse = Player:GetMouse()
local CharacterModel = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = CharacterModel:WaitForChild("HumanoidRootPart")
--|| FOLDERS ||--
local BuildMeshesFolder = script:WaitForChild("BuildMeshes")
local PreviewFolder = BuildMeshesFolder:WaitForChild("Preview")

Now we define our settings our data that we are preserving

--|| SETTINGS ||--
local BuildingMeshes = {
	["Wall"] = PreviewFolder:WaitForChild("Wall"),
	["Floor"] = PreviewFolder:WaitForChild("Floor"),
	["Ramp"] = PreviewFolder:WaitForChild("Ramp"),
}
local CFrameAddOns = {
	["Wall"] = CFrame.new(0,0,(BuildManagerComponentModule.GridSize/2)),
	["Ramp"] = CFrame.new(0,0,0),
	["Floor"] = CFrame.new(Vector3.new(0,-BuildManagerComponentModule.GridSize/2,0))
}

For the CFrameAddOns since we are snapping it to the grid we never actually offsetted the Position to the outside.

Before the CFrameAddOns
image

After the CFrame AddOns
image

Now for the Private Functions and Binding Actions

--|| PRIVATE FUNCTIONS ||--
local function ResetPreviewParents(Mesh)
	for _,BuildMesh in pairs(BuildingMeshes) do
		if BuildMesh ~= Mesh and BuildMesh.Parent ~= PreviewFolder then
			BuildMesh.Parent = PreviewFolder
		end
	end
end
--|| ACTIONS ||--
ContextActionService:BindAction("SwitchBuild", BuildManagerComponentModule.SwitchBuild, true, Enum.KeyCode.Q, Enum.KeyCode.C, Enum.KeyCode.V) -- The KeyCodes for the keybinds
ContextActionService:BindAction("ToggleBuild", BuildManagerComponentModule.ToggleBuildMode, true, Enum.KeyCode.H)

Now we add a RenderStepped Event. Inside of it we want to check if the player is building.

--|| EVENTS ||--
RunService.RenderStepped:Connect(function()
	if(BuildManagerComponentModule.isBuilding) then
		
	else
		
	end
end)

If they are building we want to get the NextBuildPosition and NextBuildRotation, if they are not building we want to Call the ResetPreviewParents Function.

RunService.RenderStepped:Connect(function()
	if(BuildManagerComponentModule.isBuilding) then
		local BuildComponentPosition = BuildManagerComponentModule.GetNextBuildPosition(HumanoidRootPart.Position, Mouse.Hit.LookVector)
		local BuildComponentRotation = BuildManagerComponentModule.GetNextBuildRotation(Mouse.Hit.LookVector)
	else
		ResetPreviewParents()
	end
end)

Now we get the Build Mesh and Set the CFrame and Parent.

RunService.RenderStepped:Connect(function()
	if(BuildManagerComponentModule.isBuilding) then
		local BuildComponentPosition = BuildManagerComponentModule.GetNextBuildPosition(HumanoidRootPart.Position, Mouse.Hit.LookVector)
		local BuildComponentRotation = BuildManagerComponentModule.GetNextBuildRotation(Mouse.Hit.LookVector)
		
		local BuildComponent = BuildingMeshes[BuildManagerComponentModule.SelectedBuild]
		ResetPreviewParents(BuildComponent)
		BuildComponent.Parent = workspace:FindFirstChild("Builds") or workspace
		BuildComponent.CFrame = CFrame.new(BuildComponentPosition) * CFrame.Angles(BuildComponentRotation.X,BuildComponentRotation.Y,BuildComponentRotation.Z) * CFrameAddOns[BuildManagerComponentModule.SelectedBuild]
	else
		ResetPreviewParents()
	end
end)

If we where to run the game now the grid snapping would be working.
https://gyazo.com/9b8d32e6edf80a024c7e36d42ac66df9
Now its time to handle the placement of it.

Step 5
Now that we are with client side lets move on to server side. Lets start off by making a Folders and Remote Events in ReplicatedStorage.
image

Now we go back to BuildManagerComponent Module script and add a few variables and functions.

--|| SERVICES ||--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--|| FOLDERS ||--
local BuildingEventsFolder = ReplicatedStorage:WaitForChild("BuildingEvents")
--|| REMOTE EVENTS ||--
local PlaceBuildEvent = BuildingEventsFolder:WaitForChild("PlaceBuild")

-- A function which we will use to check collisions. 
local function GetTouchingParts(Part)
   local Connection = Part.Touched:Connect(function() end)
   local Results = Part:GetTouchingParts()
   Connection:Disconnect()
   return Results
end

function BuildManagerComponent.PlaceBuild(BuildMesh, BuildName)
	if(BuildManagerComponent.isBuilding) then
        -- Check for colliding builds.
		local Results = GetTouchingParts(BuildMesh)
		local Placeable = true -- Placeable is set to true since we assume they can place till we find something that says otherwise.
		for _, Build in pairs(Results) do
			if Build.Name == BuildMesh.Name then
				if Build.Position == BuildMesh.Position then
					Placeable = false
				end
			end
		end
		if(Placeable) then
			PlaceBuildEvent:FireServer(BuildName, BuildMesh.Position, BuildMesh.Orientation)
		end
	end
end

Inside the local script instead of using ContextActionService to Bind MouseButton1 to the function we are instead going to use UserInputService.InputBegan and inside of that we check the UserInputType of the InputObject and call the PlaceBuild function we created in BuildManagerComponent.

UserInputService.InputBegan:Connect(function(InputObject, GameProcessed)
	if GameProcessed then return end
	if(InputObject.UserInputType == Enum.UserInputType.MouseButton1) then
		BuildManagerComponentModule.PlaceBuild(BuildingMeshes[BuildManagerComponentModule.SelectedBuild], BuildManagerComponentModule.SelectedBuild)
	end
end)

Step 6(Finale):
We create a Server Script Inside Server Script Service then define a few variables. Make sure you also Parent the actual builds(non preview builds) to the Server Script.
image

--|| SERVICES ||--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--|| FOLDERS ||--
local BuildingEventsFolder = ReplicatedStorage:WaitForChild("BuildingEvents")
--|| REMOTE EVENTS ||--
local PlaceBuildEvent = BuildingEventsFolder:WaitForChild("PlaceBuild")

Now for the Remote Event

PlaceBuildEvent.OnServerEvent:Connect(function(Player, BuildName, BuildPosition, BuildRotation)
	local BuildComponent = script:FindFirstChild(BuildName)
	if BuildComponent then
		BuildComponent = BuildComponent:Clone()
		BuildComponent.Parent = workspace:FindFirstChild("Builds") or workspace
		BuildComponent.Position = BuildPosition
		BuildComponent.Orientation = BuildRotation
		BuildComponent.Anchored = true
		BuildComponent.CanCollide = true
	end
end)

After you have finished. All your scripts should look like something similar to this.

Local Script:

--|| SERVICES ||--
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
--|| MODULES ||--
local BuildManagerComponentModule = require(script:WaitForChild("BuildManagerComponent"))
--|| VARIABLES ||--
local Player = Players.LocalPlayer
local Mouse = Player:GetMouse()
local CharacterModel = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = CharacterModel:WaitForChild("HumanoidRootPart")
--|| FOLDERS ||--
local BuildMeshesFolder = script:WaitForChild("BuildMeshes")
local PreviewFolder = BuildMeshesFolder:WaitForChild("Preview")
--|| SETTINGS ||--
local BuildingMeshes = {
	["Wall"] = PreviewFolder:WaitForChild("Wall"),
	["Floor"] = PreviewFolder:WaitForChild("Floor"),
	["Ramp"] = PreviewFolder:WaitForChild("Ramp"),
}
local CFrameAddOns = {
	["Wall"] = CFrame.new(0,0,(BuildManagerComponentModule.GridSize/2)),
	["Ramp"] = CFrame.new(0,0,0),
	["Floor"] = CFrame.new(Vector3.new(0,-BuildManagerComponentModule.GridSize/2,0))
}
--|| PRIVATE FUNCTIONS ||--
local function ResetPreviewParents(Mesh)
	for _,BuildMesh in pairs(BuildingMeshes) do
		if BuildMesh ~= Mesh and BuildMesh.Parent ~= PreviewFolder then
			BuildMesh.Parent = PreviewFolder
		end
	end
end
--|| ACTIONS ||--
ContextActionService:BindAction("SwitchBuild", BuildManagerComponentModule.SwitchBuild, true, Enum.KeyCode.Q, Enum.KeyCode.C, Enum.KeyCode.V) -- The KeyCodes for the keybinds
ContextActionService:BindAction("ToggleBuild", BuildManagerComponentModule.ToggleBuildMode, true, Enum.KeyCode.H)
--|| EVENTS ||--
UserInputService.InputBegan:Connect(function(InputObject, GameProcessed)
	if GameProcessed then return end
	if(InputObject.UserInputType == Enum.UserInputType.MouseButton1) then
		BuildManagerComponentModule.PlaceBuild(BuildingMeshes[BuildManagerComponentModule.SelectedBuild], BuildManagerComponentModule.SelectedBuild)
	end
end)
RunService.RenderStepped:Connect(function()
	if(BuildManagerComponentModule.isBuilding) then
		local BuildComponentPosition = BuildManagerComponentModule.GetNextBuildPosition(HumanoidRootPart.Position, Mouse.Hit.LookVector)
		local BuildComponentRotation = BuildManagerComponentModule.GetNextBuildRotation(Mouse.Hit.LookVector)
		
		local BuildComponent = BuildingMeshes[BuildManagerComponentModule.SelectedBuild]
		ResetPreviewParents(BuildComponent)
		BuildComponent.Parent = workspace:FindFirstChild("Builds") or workspace
		BuildComponent.CFrame = CFrame.new(BuildComponentPosition) * CFrame.Angles(BuildComponentRotation.X,BuildComponentRotation.Y,BuildComponentRotation.Z) * CFrameAddOns[BuildManagerComponentModule.SelectedBuild]
		
	else
		ResetPreviewParents()
	end
end)

BuildManagerComponent Module:

local BuildManagerComponent = {}
--|| MODULE SETTINGS ||--
BuildManagerComponent.GridSize = 16
BuildManagerComponent.BuildDistance = 2
--|| MODULE VARIABLES ||--
BuildManagerComponent.isBuilding = false
BuildManagerComponent.SelectedBuild = "Wall"
--|| TABLES & DICTIONARIES ||--
local BuildingKeybinds = {
	[Enum.KeyCode.Q] = "Wall",
	[Enum.KeyCode.C] = "Floor",
	[Enum.KeyCode.V] = "Ramp"
}
--|| SERVICES ||--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--|| FOLDERS ||--
local BuildingEventsFolder = ReplicatedStorage:WaitForChild("BuildingEvents")
--|| REMOTE EVENTS ||--
local PlaceBuildEvent = BuildingEventsFolder:WaitForChild("PlaceBuild")
--|| PRIVATE FUNCTIONS ||--
local function GridSnap(Value, Size)
	return (math.floor(Value/Size + 0.5) * Size)
end
local function GetTouchingParts(Part)
	local Connection = Part.Touched:Connect(function() end)
	local Results = Part:GetTouchingParts()
	Connection:Disconnect()
	return Results
end
--|| MODULE FUNCTIONS ||--
function BuildManagerComponent.GetNextBuildPosition(HumanoidRootPartPosition, MouseLookVector3)
	local DirectionVector3 = MouseLookVector3 * BuildManagerComponent.BuildDistance
	DirectionVector3 += HumanoidRootPartPosition
	return Vector3.new(
		GridSnap(DirectionVector3.X, BuildManagerComponent.GridSize),
		GridSnap(DirectionVector3.Y, BuildManagerComponent.GridSize) + BuildManagerComponent.GridSize/2,
		GridSnap(DirectionVector3.Z, BuildManagerComponent.GridSize)
	)
end
function BuildManagerComponent.GetNextBuildRotation(Vector)
	if(typeof(Vector) == "Vector3") then
		local Y = math.atan2(Vector.X, Vector.Z)
		return Vector3.new(0,GridSnap(Y, math.rad(-90)), 0)
	end
end
--|| TOGGLE BUILDING ||--
function BuildManagerComponent.ToggleBuildMode(ActionName, InputState, InputObject)
	if ActionName == "ToggleBuild" then
		if InputState == Enum.UserInputState.Begin then
			BuildManagerComponent.isBuilding = not BuildManagerComponent.isBuilding
			warn("Toggled Build Mode: ", BuildManagerComponent.isBuilding)
		end
	end
end
--|| SWITCH TO A DIFFERENT BUILD( Ramp, Wall, Floor, Etc) ||--
function BuildManagerComponent.SwitchBuild(ActionName, InputState, InputObject)
	if(ActionName == "SwitchBuild") then
		if(InputState == Enum.UserInputState.Begin) then
			BuildManagerComponent.isBuilding = true
			if(BuildManagerComponent.isBuilding and BuildingKeybinds[InputObject.KeyCode]) then
				BuildManagerComponent.SelectedBuild = BuildingKeybinds[InputObject.KeyCode]
			end
		end
	end
end
function BuildManagerComponent.PlaceBuild(BuildMesh, BuildName)
	if(BuildManagerComponent.isBuilding) then
		local Results = GetTouchingParts(BuildMesh)
		local Placeable = true
		for _, Build in pairs(Results) do
			if Build.Name == BuildMesh.Name then
				if Build.Position == BuildMesh.Position then
					Placeable = false
				end
			end
		end
		if(Placeable) then
			PlaceBuildEvent:FireServer(BuildName, BuildMesh.Position, BuildMesh.Orientation)
		end
	end
end
return BuildManagerComponent

Server Script:

--|| SERVICES ||--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--|| FOLDERS ||--
local BuildingEventsFolder = ReplicatedStorage:WaitForChild("BuildingEvents")
--|| REMOTE EVENTS ||--
local PlaceBuildEvent = BuildingEventsFolder:WaitForChild("PlaceBuild")
--|| EVENTS ||--
PlaceBuildEvent.OnServerEvent:Connect(function(Player, BuildName, BuildPosition, BuildRotation)
	local BuildComponent = script:FindFirstChild(BuildName)
	if BuildComponent then
		BuildComponent = BuildComponent:Clone()
		BuildComponent.Parent = workspace:FindFirstChild("Builds") or workspace
		BuildComponent.Position = BuildPosition
		BuildComponent.Orientation = BuildRotation
		BuildComponent.Anchored = true
		BuildComponent.CanCollide = true
	end
end)

Here is the rbxl of the place if you need it! - Fortnite Building System Tutorial.rbxl (116.0 KB)

I figured it should be best to make a tutorial on this since I see a lot of people wanting this but most of them have no idea. Hope this can help or give knowledge for the people seeking it.

This concludes the tutorial on how to create a fortnite building system on roblox studio.If you need any further help be sure to join my discord as I am most active on discord - Development Help

54 Likes

wow, that is a lot of code, thank you for making this tutorial, i don’t need it, but I can see it would be useful for other things

4 Likes

Good stuff, I know twelve year old me would be excited as I tried making a Fortnite game back then, glad you’re bringing forward these mechanics though as I haven’t seen much talk about this before.

6 Likes

Do you know how to do a edit system?

4 Likes

Sorry for the late reply, I have been really busy, Yes it is possible to make a edit system with this, ofc you have to do a lot of editing though

1 Like

do you mind giving to us when your done

1 Like

For anyone wondering, here is an updated version of what I have.
Updated Version

3 Likes

are you going to provide the code for the updated version?

2 Likes

bro will you drop the updated version

2 Likes

How exactly does math.floor(Value/Size + 0.5) * Size work? I would just like to know how this makes it snap to a grid, as I personally find it interesting.

1 Like

Sorry for the late reply but it aligns a value to the nearest grid line by scaling the value down to grid units, rounding to the nearest grid unit using math.floor after adding 0.5 for proper rounding, and then scaling it back up. This is so that it snaps to the closet grid position, rather than being rounded down or up to the nearest grid cell edge.

1 Like

Would be a better learning experience if you were to continue it by yourself ngl you would learn a lot.

2 Likes

this is sick bro are you still working on it

1 Like

sorry didnt see this messaage, im happy to assist you in your development my discord is decompi, you can add me through there

2 Likes