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.
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.
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
After the CFrame AddOns
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.
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.
--|| 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