How would I make builds not placeable on air?

I’m making a fortnite building system and I cannot figure out how to make a “gravity” system

I followed this tutorial: Creating A Fortnite Building - Tutorial for Intermediate Developers

Server Script:

local RS = game:GetService("ReplicatedStorage")
local RemoteEvents = RS.RemoteEvents
local PlaceBuildEvent = RemoteEvents:WaitForChild("PlaceBuild")

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

Module Script:

local BuildManager = {}

local ContextActionService = game:GetService("ContextActionService")
local RS = game:GetService("ReplicatedStorage")
local RemoteEvents = RS.RemoteEvents
local PlaceBuildEvent = RemoteEvents:WaitForChild("PlaceBuild")

BuildManager.GridSize = 16
BuildManager.BuildDistance = 10

BuildManager.isBuilding = false
BuildManager.SelectedBuild = "Wall"

local BuildingKeybinds = {
	[Enum.KeyCode.Z] = "Wall", 
	[Enum.KeyCode.X] = "Floor", 
	[Enum.KeyCode.C] = "Ramp"
}

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

function BuildManager.GetNextBuildPosition(HumanoidRootPartPosition, MouseLookVector3)
	local DirectionVector3 = MouseLookVector3 * BuildManager.BuildDistance
	DirectionVector3 += HumanoidRootPartPosition
	return Vector3.new(
		GridSnap(DirectionVector3.X, BuildManager.GridSize),
		GridSnap(DirectionVector3.Y, BuildManager.GridSize)  + BuildManager.GridSize/2,
		GridSnap(DirectionVector3.Z, BuildManager.GridSize)
	)
end

function BuildManager.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

function BuildManager.ToggleBuildMode(ActionName, InputState, InputObject)
	if ActionName == "ToggleBuild" then
		if InputState == Enum.UserInputState.Begin then
			BuildManager.isBuilding = not BuildManager.isBuilding
			warn("Toggled Build Mode: ", BuildManager.isBuilding)
		end
	end
end

function BuildManager.SwitchBuild(ActionName, InputState, InputObject)
	if(ActionName == "SwitchBuild") then
		if(InputState == Enum.UserInputState.Begin) then
			BuildManager.isBuilding = true
			if(BuildManager.isBuilding and BuildingKeybinds[InputObject.KeyCode]) then
				BuildManager.SelectedBuild = BuildingKeybinds[InputObject.KeyCode]
			end
		end
	end
end

local function GetTouchingParts(Part)
	local Connection = Part.Touched:Connect(function() end)
	local Results = Part:GetTouchingParts()
	Connection:Disconnect()
	return Results
end


function BuildManager.PlaceBuild(BuildMesh, BuildName)
	if(BuildManager.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 BuildManager

Local Script:

-- // SERVICES // --

local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")
local UIS = game:GetService("UserInputService")

-- // MODULES // --

local BuildManager = require(script:WaitForChild("BuildManager"))

-- // FOLDERS // --
local MeshesFolder = script.BuildMeshes
local PreviewFolder = MeshesFolder.Preview

-- // PLAYER // --

local plr = game.Players.LocalPlayer
local char = plr.Character or plr.CharacterAdded:Wait()
local HumanoidRootPart = char:WaitForChild("HumanoidRootPart")

local Mouse = plr:GetMouse()

-- // TABLES // --

local BuildingMeshes = {
	["Wall"] = PreviewFolder:WaitForChild("Wall");
	["Floor"] = PreviewFolder:WaitForChild("Floor");
	["Ramp"] = PreviewFolder:WaitForChild("Ramp")
}

local CFrameAddOns = {
	["Wall"] = CFrame.new(0,0,(BuildManager.GridSize/2));
	["Floor"] = CFrame.new(Vector3.new(0,-BuildManager.GridSize/2,0));
	["Ramp"] = CFrame.new(0,0,0);
}

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", BuildManager.SwitchBuild, true, Enum.KeyCode.Z, Enum.KeyCode.X, Enum.KeyCode.C) -- The KeyCodes for the keybinds
ContextActionService:BindAction("ToggleBuild", BuildManager.ToggleBuildMode, true, Enum.KeyCode.H)
--|| EVENTS ||--
UIS.InputBegan:Connect(function(InputObject, GameProcessed)
	if GameProcessed then return end
	if(InputObject.UserInputType == Enum.UserInputType.MouseButton1) then
		BuildManager.PlaceBuild(BuildingMeshes[BuildManager.SelectedBuild], BuildManager.SelectedBuild)
	end
end)

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

		local BuildComponent = BuildingMeshes[BuildManager.SelectedBuild]
		ResetPreviewParents(BuildComponent)
		BuildComponent.Parent = workspace:FindFirstChild("Builds") or workspace
		BuildComponent.CFrame = CFrame.new(BuildComponentPosition) * CFrame.Angles(BuildComponentRotation.X,BuildComponentRotation.Y,BuildComponentRotation.Z) * CFrameAddOns[BuildManager.SelectedBuild]

	else
		ResetPreviewParents()
	end
end)

Please help me I have been trying to do this for like 3 days.

(And yes before anyone asks I have tried learning from roblox’s battle royale game)

2 Likes

I have made it using raycasts but I’m still wondering if there’s a better solution to this

1 Like

This is what my updated system looks like; updated system

I implemented a method to ensure that construction behaves realistically and efficiently. The process involves two primary steps:

1. Mathematical Calculations: Initially, I perform a series of mathematical calculations. These calculations are crucial for determining the positions where the pre-build components (indicated by the blue outlines) can be placed without requiring movement. This preemptive step simplifies the subsequent processes by minimizing the need for adjustments and ensuring that the placement of components is both precise and logical. For instance, it prevents a wall from unrealistically ascending if such a movement would result in an invalid configuration or placement that cannot be supported by the underlying structure which intern puts less stress on me changing the raycast and using it much less than I have to.

2. Collision Detection: After calculating the optimal positions, I proceed to check for collisions between the pre-build components and any existing parts of the structure. Specifically, I look for parts that are touching and share the same name as the outline or the pre-build component. This step is vital for determining whether a new component can be added to the structure at the intended location without causing unrealistic movements or placements.

local function IsConnectedGrid(OutlineCFrame, rawString)
	local AutomaticValidation = 0
	local Blacklist = false
	local OverlapParam = OverlapParams.new()

	OverlapParam.FilterDescendantsInstances = {
		workspace.Builds,
		workspace["Game Workspace"].Misc.Baseplate
	}
	OverlapParam.FilterType = Enum.RaycastFilterType.Whitelist

	for index, value in pairs(EnumList.OVERLAPS[rawString]) do
		if(#game.Workspace:GetPartBoundsInBox(OutlineCFrame * value.CFrame, value.Size, OverlapParam) > 0) then
			AutomaticValidation += 1
			if(value.Blacklist == true) then
				Blacklist = true
			end
		end
	end
	return AutomaticValidation, Blacklist
end

So here is a little insight on how I did it, you probably won’t understand most of it but hopefully it helps you in someway.

local rawInt = BaseController.SelectedBuild;
local rawString = EnumList.KEYS[rawInt];
local outline = BaseController.Assets[rawString];
if(outline == nil) then
	return
end
outline.Name = rawString;
outline.Parent = workspace.Temp.Outlines;
local RaycastResult = Quadratics:Cast(CameraController.HumanoidRootPart.Position, CameraController.HumanoidRootPart.CFrame.LookVector, 15, {workspace.Builds}, Enum.RaycastFilterType.Whitelist)

local Arg = ((RaycastResult and RaycastResult.Instance.Parent == workspace.Builds)) and 2 or nil
if(RaycastResult and Arg ~= nil) then
	if(rawString == "Ramp") then
		local Boolean1 = string.find(RaycastResult.Instance.Name, "Wall")
		local Boolean2 = string.find(RaycastResult.Instance.Name, "Floor")
		if(Boolean1) then
			-- CHECK IF CROUCH IS ENABLED IF IT IS THEN DONT CHANGE THE ARG - for RAMP PLACE THROUGH WALL WHEN CROUCHED
			Arg = 9
		elseif(Boolean2) then
			Arg = 9
		end
	elseif(rawString == "Wall") then
		local Boolean1 = string.find(RaycastResult.Instance.Name, "Ramp")
		local Boolean2 = string.find(RaycastResult.Instance.Name, "Wall")
		if(Boolean1) then
			Arg = 2
		elseif(Boolean2) then
			Arg = 8
		end
	end
end
BaseController:ResetSpeculations(rawString);
local Location, AddOn = BaseController:RelativeLongitudeLatitude(LocalCharacter.HumanoidRootPart,rawInt, Arg) -- (rawString == "Ramp" and CameraEnumerationDirection == "Down" or CameraEnumerationDirection == "Top") and 2 or nil);
local Rotation = BaseController:RelativeRotationalMatrix();

if(rawInt == 2) then
	local PredictedCFrame = CFrame.new(Location) * AddOn
	if(RaycastResult and string.find(RaycastResult.Instance.Name, "Wall") and IsBehind(RaycastResult.Instance.Position, PredictedCFrame.Position)) then

	else
		outline.CFrame = PredictedCFrame;
	end
elseif  (rawInt == 3) then
	local PredictedCFrame = CFrame.new(Location) * CFrame.Angles(Rotation.X, Rotation.Y, Rotation.Z) * AddOn;
	local Boolean1 = RaycastResult and string.find(RaycastResult.Instance.Name, "Floor")
	local Boolean2 = RaycastResult and string.find(RaycastResult.Instance.Name, "Wall")
	if(Boolean1 or Boolean2 and IsBehind(RaycastResult.Instance.Position, PredictedCFrame.Position)) then
		if(Boolean1) then
			if(PredictedCFrame.Position.Y >= RaycastResult.Instance.CFrame.Position.Y) then
				outline.CFrame = PredictedCFrame * CFrame.Angles(math.rad(-36.87), 0, 0)
			end
		end
	else
		outline.CFrame = PredictedCFrame * CFrame.Angles(math.rad(-36.87), 0, 0)
	end
elseif (rawInt == 1) then
	local PredictedCFrame = CFrame.new(Location) * CFrame.Angles(Rotation.X, Rotation.Y, Rotation.Z) * AddOn;
	if(RaycastResult and string.find(RaycastResult.Instance.Name, "Floor") and IsBehind(RaycastResult.Instance.Position, PredictedCFrame.Position)) then

	else
		local AutomaticValidation, Blacklist = IsConnectedGrid(PredictedCFrame, rawString)
		if(AutomaticValidation <= 0 and BaseController.Placeable == true) then

		else
			outline.CFrame = PredictedCFrame
		end
	end
end


if(BaseController.Positions[roundVector3(outline.Position)] == true) then
	BaseController.Placeable = false;
	outline.Parent = nil;
	return
end
local AutomaticValidation, Blacklist = IsConnectedGrid(outline.CFrame, rawString)

if(Blacklist == true) then
	BaseController.Placeable = false;
	outline.Parent = nil
	return
end
if (AutomaticValidation == 0) then
	BaseController.Placeable = false;
	outline.BrickColor = BrickColor.new("Persimmon");
elseif(AutomaticValidation >= 1) then
	outline.BrickColor = BrickColor.new("Electric blue");
	BaseController.Placeable = true;
end
1 Like

What does the EnumList stand for?

Or could you just show me how to implement it in my script?

If that’s possible of course.

Its better to understand what you are doing than just copy the code, EnumList is nothing really its just a list of constants i have.

EnumController.KEYS = {
	[1] = "Wall",
	[2] = "Floor",
	[3] = "Ramp"
}

EnumController.OVERLAPS = {
	Wall = {
		[1] = {
			["CFrame"] = CFrame.new( -0.000381469727, 6.5759201, 8.01086426e-05, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(10, 1, 1)
		},
		[2] = {
			["CFrame"] = CFrame.new(-0.000381469727, -7.2045517, 8.01086426e-05, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(10, 1.5, 1)
		},
		[3] = {
			["CFrame"] = CFrame.new(-8.62721252, 0, 8.01086426e-05, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(1, 10, 1)
		},
		[4] = {
			["CFrame"] = CFrame.new(8.82350159, 0, 8.01086426e-05, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(1, 10, 1)
		},
		[5] = {
			["CFrame"] = CFrame.new(-0.000381469727, 0.352737427, 8.01086426e-05, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(10, 1, 1)
		}
	},
	Floor = {
		[1] = {
			["CFrame"] = CFrame.new(-0.000122070312, 0.000457763672, -8.83127975, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] =  Vector3.new(10, 1, 1),
		},
		[2] = {
			["CFrame"] = CFrame.new(8.98901367, -0.0357818604, -0.000423431396, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] =  Vector3.new(1, 1, 10),
		},
		[3] = {
			["CFrame"] = CFrame.new(-9.01773071, 0.000457763672, -0.000423431396, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] =  Vector3.new(1, 1, 10),
		},
		[4] = {
			["CFrame"] = CFrame.new(-0.000122070312, 0.000457763672, 8.6138382, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] =  Vector3.new(10, 1, 1),
		},
	},
	Ramp = {
		[1] = {
			["CFrame"] = CFrame.new(9.27209473, 0.000213623047, -0.000427246094, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(1, 1, 10)
		},
		[2] = {
			["CFrame"] = CFrame.new(-9.07286072, 0.000213623047, -0.000427246094, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(1, 1, 10)
		},
		[3] = {
			["CFrame"] = CFrame.new(-0.00032043457, 0.000213623047, 11.3549385, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(10, 1, 1)
		},
		[4] = {
			["CFrame"] = CFrame.new(-0.00032043457, 0.000213623047, -11.4423714, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Size"] = Vector3.new(10, 1, 1)
		},
		[5] = {
			["CFrame"] = CFrame.new(0.272094727, 0.000213623047, -0.000427246094, 1, 0, 0, 0, 1, 0, 0, 0, 1),
			["Blacklist"] = true,
			["Size"] = Vector3.new(3, 1, 6)
		}
	}
}
1 Like

Okay , thank you, I might be able to do something with this.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.