Should I handle controls on the client or server?

Edit: I might just be dumb and it’s fine to do all of this on the client, but I just wanna make sure

So I know controls can only be done on the client using UserInputService although for my game I’ve coded a slide, wall run and wall climbing and it’s all handled on the client and I’m gonna assume that I should probably break it up a bit and handle some things on the server although I’m not exactly sure what so my question is like what should be on the client and what should go on the server?

Heres the code which I kind of mashed together today so sorry if it’s wack.

--| Services
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UIS = game:GetService("UserInputService")
local CAS = game:GetService("ContextActionService")

--| Variables
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:wait()
local HRP = Character:WaitForChild("HumanoidRootPart")
local Humanoid = Character:WaitForChild("Humanoid")

local Animations = game:GetService("ReplicatedStorage"):WaitForChild("Animations")
local Anims = {}
if Animations then
	for _,anim in pairs(Animations:GetChildren()) do
		if anim:IsA("Animation") then
			Anims[anim.Name] = Humanoid:LoadAnimation(anim)
		end
	end
end

local Climbing = false
local Face = nil
local Sliding = false
local Actions = {0;0;0;0;}
local Buttons = {"A";"D";"S";"W"}
local Left = false
local Right = false
local LeftPlaying = false
local RightPlaying = false

local Controls = {
	["Q"] = function()
		if (not Sliding) and (not Climbing) and (Humanoid.FloorMaterial ~= Enum.Material.Air) then
			Sliding = true
			Humanoid.JumpHeight = 0
			Anims["Slide"]:Play()

			local Slide = Instance.new("BodyVelocity")
			Slide.MaxForce = Vector3.new(1,0,1)*30000
			Slide.Velocity = HRP.CFrame.lookVector*100
			Slide.Parent = HRP

			for count = 1,8 do
				wait(0.1)
				Slide.Velocity *= 0.7
			end

			Anims["Slide"]:Stop()
			Humanoid.JumpHeight = 7.2
			Slide:Destroy()
			Sliding = false		
		end
	end,
	["F"] = function()
		if not Sliding then
			if (not Climbing) then
				OnClimb()
			else
				OffClimb()
			end			
		end
	end,
}

local Params = RaycastParams.new()
Params.CollisionGroup = ("Climbable")
Params.FilterDescendantsInstances = {Character}
Params.FilterType = Enum.RaycastFilterType.Blacklist
Params.IgnoreWater = true

local ClimbMove = Instance.new("BodyVelocity")
ClimbMove.MaxForce = Vector3.new(1,1,1)*math.huge
ClimbMove.P = math.huge
ClimbMove.Velocity = Vector3.new()
ClimbMove.Name = ("Climb Speed")

local ClimbGyro = Instance.new("BodyGyro")
ClimbGyro.CFrame = CFrame.new()
ClimbGyro.D = 100
ClimbGyro.MaxTorque = Vector3.new(1,1,1)*math.huge
ClimbGyro.P = 3000

function Down(Key) 
	return UIS:IsKeyDown(Enum.KeyCode[Key]) 
end

function MovePlayer(Part1,Part2,Cross1,Cross2)
	if not HRP:FindFirstChild("WRBV") then
		BodyVelocity = Instance.new("BodyVelocity",HRP)
		BodyVelocity.Name = "WRBV"		
	end
	if not HRP:FindFirstChild("WRBG") then
		BodyGyro = Instance.new("BodyGyro",HRP)		
		BodyGyro.Name = "WRBG"
	end

	BodyGyro.D = 100
	BodyGyro.P = 10000
	BodyGyro.MaxTorque = Vector3.new(0,0,0)
	BodyVelocity.MaxForce = Vector3.new(0,0,0)
	BodyGyro.CFrame = HRP.CFrame
	BodyGyro.MaxTorque = Vector3.new(1,1,1) * math.huge
	BodyVelocity.MaxForce = Vector3.new(1,1,1) * math.huge
	if Part1 then
		BodyVelocity.Velocity = Cross1 * 50
		Left = true
	elseif Part2 then
		BodyVelocity.Velocity = -Cross2 * 50
		Right = true
	end	
end

function StopMovingPlayer()
	if HRP:FindFirstChild("WRBV") and HRP:FindFirstChild("WRBG") then
		HRP:FindFirstChild("WRBV"):Destroy()
		HRP:FindFirstChild("WRBG"):Destroy()
		Left = false
		Right = false
		Anims["Left"]:Stop()
		Anims["Right"]:Stop()		
	end
end

function OffClimb()
	Climbing=false
	ClimbGyro.Parent=nil
	ClimbMove.Parent=nil
	Face=nil
end

function OnClimb()
	local origin = HRP.Position
	local direction = HRP.CFrame.LookVector

	local result = workspace:Raycast(origin, direction, Params)
	if result then
		Climbing=true
		HRP.CFrame = CFrame.new(HRP.CFrame.p, Vector3.new(HRP.Position.X - result.Normal.X, HRP.Position.Y, HRP.Position.Z - result.Normal.Z))
		Face = CFrame.new(result.Position+result.Normal, result.Position)

		Humanoid.AutoRotate=false
		Humanoid.PlatformStand=true

		ClimbMove.Parent=HRP
		ClimbGyro.Parent=HRP

		repeat
			RunService.RenderStepped:Wait()
			ClimbGyro.CFrame=Face or CFrame.new()

			if HRP.Position.Y > result.Instance.Size.Y+3 then
				Climbing=false
				OffClimb()
			end

			local sideOrigin = HRP.CFrame*CFrame.new(0, 0, -1).Position
			local sideDirection = HRP.CFrame.RightVector*(Down("D") and -2 or 2)

			local Hit = workspace:Raycast(sideOrigin, sideDirection, Params)
			if Hit and (Down("D") or Down("A")) then
				Face=CFrame.new(Hit.Position+Hit.Normal, Hit.Position)
			end
		until (not Climbing)

		Humanoid.AutoRotate=true
		Humanoid.PlatformStand=false
	end
end

function WallRun()
	local Ray1 = Ray.new(HRP.CFrame.p,HRP.CFrame.rightVector * -3)
	local Ray2 = Ray.new(HRP.CFrame.p,HRP.CFrame.rightVector * 3)
	local Part1,Position2,Normal1 = workspace:FindPartOnRayWithIgnoreList(Ray1,{Character})
	local Part2,Position2,Normal2 = workspace:FindPartOnRayWithIgnoreList(Ray2,{Character})
	local Cross1 = Vector3.new(0,1,0):Cross(Normal1)
	local Cross2 = Vector3.new(0,1,0):Cross(Normal2)
	
	if (Part1) or (Part2) then -- Player is touching a part
		--if Normal1.y == 0 or Normal2.y == 0 then --Not exactly sure what any of this does
		if (Part1 and Part1.Name == "WallRun") or (Part2 and Part2.Name == "WallRun") then
			if Part1 then
				HRP.CFrame = CFrame.new(HRP.CFrame.p,Vector3.new(HRP.Position.X + Cross1.x,HRP.Position.Y,HRP.Position.Z + Cross1.z))
			elseif Part2 then
				HRP.CFrame = CFrame.new(HRP.CFrame.p,Vector3.new(HRP.Position.X - Cross2.x,HRP.Position.Y,HRP.Position.Z - Cross2.z))
			end

			if Down("W") and not Climbing then
				MovePlayer(Part1,Part2,Cross1,Cross2)
			else
				StopMovingPlayer()
			end
		end

	elseif (not Part1 and not Part2) then --Player is no longer touching a part
		StopMovingPlayer()
	end

	if Left and not LeftPlaying then
		--print("LEFT")
		Anims["Left"]:Play()
		LeftPlaying = true
	elseif not Left then
		LeftPlaying = false
	end

	if Right and not RightPlaying then
		--print("RIGHT")
		Anims["Right"]:Play()
		RightPlaying = true
	elseif not Right then
		RightPlaying = false
	end
end

UIS.InputBegan:Connect(function(Input,IsTyping)
	if IsTyping then return end
	
	local Key = UIS:GetStringForKeyCode(Input.KeyCode)
	local Test = table.find(Buttons, Key)

	if Test then
		Actions[Test]=1
	elseif Controls[Key] then
		Controls[Key]()
	end
end)

UIS.InputEnded:Connect(function(Input,IsTyping)
	if IsTyping then return end
	
	local Key = UIS:GetStringForKeyCode(Input.KeyCode)
	local Test = table.find(Buttons, Key)

	if Test then
		Actions[Test]=0
	end
end)

RunService.RenderStepped:Connect(function()
	local Strafe = Actions[2] - Actions[1]
	local Surge = Actions[3] - Actions[4]

	ClimbMove.Velocity=HRP.CFrame.RightVector*(Strafe*8)+Vector3.new(0, Surge*-8, 0)
	
	WallRun()
end)
2 Likes

I think these should be handled by the client. Clients can respond to input immediately and can update the character.

Well controls have to be on the client, then it’s up to you to decide what you want to do on the client and the server.

Remember all game logic should be handled on the server, the client can change anything they want

If you only animate things on the client, other players will not be able to see them. But if you only do it on the server it might be laggy for the LocalPlayer.

Roblox’s default character physics are handled by the client then replicated to the server, that way it’s not laggy for the LocalPlayer. But this allows for players to do hacks like super speed.

Definitely the client.

The client’s hardware is what runs a LocalScript. It will be instant feedback to the player, thus immersing them in the experience.

If you run the code on the server, the logic will all be network dependant. Imagine having 20 players in the game. That will multiply the server load by 20. Which will create a poor gaming experience to all players. A laggy server is no fun for anyone. Especially players with poor connections.

Handle things such as hit events, action events and anything that is light to process on the server. Any core gameplay and such should be done on the client. It’s okay to have some delay when awarding loot to the player for example. But if the player were to climb a wall, we want the player to receive feedback instantly instead of relying on their connection to the server.

Remember to also fit in some sanity checks on the server to minimize exploiting and vulnerability in your game.

3 Likes