I made a bad movement/state system and I want to rewrite it but I need help

Context: I made a movement system last week that was utterly terrible, so I rewrote it and it actually turned out way better, but still just terrible

My question is what is the best way to make a movement/state system? My current one has two different types of checks, one for when the player is on the ground and another one for when the player is in the air, and then it does logic stuff and sets the player’s state accordingly (if they’re on the ground, state can only be running, sprinting or idle. if they’re in the air, it can only be jumping or climbing. dashing overrides both of these because the player can dash mid air and on the ground)

It sucks. I can slowly feel it getting more unorganized as I add more things to it (want to add a wall climb and crouching)

So here’s my question again, what’s the best way to go about making a movement system and state machine? Should you be doing state logic every frame or only when there’s input? (For the record I’m doing this all on the client right now because it’s already enough of a headache worrying about making it work I don’t wanna have to worry about server validation rn)

I’m going to be pasting my code at the bottom of this post, don’t read it if you don’t want to I’m just putting the code there for those who want to read it and see what I mean, I just want to be pointed in the right direction

local MovementController = {}

local StateControllerModule = require(script.Parent.StateController)
local AnimationControllerModule = require(script.Parent.AnimationController)

local UIS = game:GetService("UserInputService")
local Debris = game:GetService("Debris")
local RS = game:GetService("RunService")

MovementController.__index = MovementController

function MovementController.new(player : Player)
	local self = setmetatable({}, MovementController)
	
	self.Character = player.Character or player.CharacterAdded:Wait()
	self.Humanoid = self.Character:WaitForChild("Humanoid")
	self.Player = player
	self.Animations = AnimationControllerModule.new(self.Humanoid.Animator)
	self.HRP = self.Character:WaitForChild("HumanoidRootPart")

	self.StateController = StateControllerModule.new(player)
	self.shiftLocked = false
	self.facingCamera = false

	self.WalkSpeed = 16
	self.SprintSpeed = 30
	self.Camera = workspace.CurrentCamera

	self.climbing = false
	self.sprintToggled = false
	self.isJumping = false
	self.canJump = true
	self.jumpDelay = 1
	self.dashing = false
	self.canDash = true
	self.dashTime = 0.267
	self.dashSpeed = 47
	self.dashCD = 2
	self.lastState = nil
	self.canWallClimb = true

	self.movementKeys = {
		[Enum.KeyCode.LeftAlt] = function() self:ToggleSprint() end,
		[Enum.KeyCode.LeftShift] = function() self:ToggleShiftLock() end,
		[Enum.KeyCode.Space] = function () self:Jump() end,
		[Enum.KeyCode.Q] = function() self:Dash() end
	}

	return self
end

function MovementController:Init()
	UIS.InputBegan:Connect(function(input, gpe)
		if gpe then return end

		if self.movementKeys[input.KeyCode] then
			self.movementKeys[input.KeyCode]()
		end
	end)

	RS.RenderStepped:Connect(function()
		self:Update()
		local newState = self.StateController:GetState()
		if newState ~= self.lastState then
			self.lastState = newState
			print(self.lastState)
		end
	end)

end

function MovementController:Update()
	self:CheckAirStates()
	self:CheckGroundStates()
	self.Animations:UpdateAnimations(self.currentDirection, self.StateController:GetState())

	if self.facingCamera then 
		self.HRP.CFrame = CFrame.lookAt(self.HRP.Position, self.HRP.Position + Vector3.new(self.Camera.CFrame.LookVector.X, 0, self.Camera.CFrame.LookVector.Z))
	end
end

function MovementController:CheckGroundStates()
	if self.Humanoid.FloorMaterial == Enum.Material.Air then return end
	if self.dashing then return end
	local moving = self.Humanoid.MoveDirection.Magnitude > 0.05
	local moveDirection = self.Humanoid.moveDirection
	local dot = self.HRP.CFrame.LookVector:Dot(moveDirection)
	local sideDot = self.HRP.CFrame.RightVector:Dot(moveDirection)

	self.canJump = true
	self.facingCamera = false
	self.climbing = false

	if moving then
		if self.sprintToggled then
			self.StateController:SetState("Sprinting")
			self.Humanoid.WalkSpeed = self.SprintSpeed
		else
			self.Humanoid.WalkSpeed = self.WalkSpeed
			self.StateController:SetState("Walking")
			if self.shiftLocked then
				self.facingCamera = true
				if dot > 0.5 then
					self.lastDirection = self.currentDirection
					self.currentDirection = "Forward"
				elseif dot < -0.5 then
					self.lastDirection = self.currentDirection
					self.currentDirection = "Backward"
				end

				if sideDot > 0.5 then
					self.lastDirection = self.currentDirection
					self.currentDirection = "Right"
				elseif sideDot < -0.5 then
					self.lastDirection = self.currentDirection
					self.currentDirection = "Left"
				end
			else--not shiftLocked
				self.lastDirection = self.currentDirection
				self.currentDirection = "Forward"
			end
		end
	else
		self.StateController:SetState("Idle")
	end
end

function MovementController:CheckAirStates()
	if self.Humanoid.FloorMaterial ~= Enum.Material.Air then return end
	if self.dashing then return end
	if self.climbing then
		self.StateController:SetState("Climbing")
		return
	end
	
	if self.Humanoid:GetState() == Enum.HumanoidStateType.Jumping then
		self.StateController:SetState("Jumping")
	elseif self.Humanoid:GetState() == Enum.HumanoidStateType.Freefall then
		self.StateController:SetState("Jumping")
	end
end

function MovementController:ToggleSprint()
	self.sprintToggled = not self.sprintToggled
end

function MovementController:ToggleShiftLock()
	self.shiftLocked = not self.shiftLocked

	if self.shiftLocked then
		workspace.CurrentCamera.CameraType = Enum.CameraType.Custom
		local t = game:GetService("TweenService"):Create(self.Humanoid, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {CameraOffset  = Vector3.new(0, 3, 0)}):Play()
		game:GetService("UserInputService").MouseBehavior = Enum.MouseBehavior.LockCenter
	else
		workspace.CurrentCamera.CameraType = Enum.CameraType.Custom
		local t = game:GetService("TweenService"):Create(self.Humanoid, TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {CameraOffset  = Vector3.new(0, 0, 0)}):Play()
		game:GetService("UserInputService").MouseBehavior = Enum.MouseBehavior.Default
	end
end

function MovementController:Jump()
	if self.canJump then
		
		--if self.Humanoid.FloorMaterial == Enum.Material.Air then
		--	RS.RenderStepped:Connect(function()
		--		local topRay = workspace:Raycast(self.Character.Head.Position + Vector3.new(0, 5, 0), self.Character.Head.CFrame.LookVector * 4 + Vector3.new(0, 5, 0))
		--		local middleRay = workspace:Raycast(self.Character.Head.Position + Vector3.new(0, 3, 0), self.Character.Head.CFrame.LookVector * 4 + Vector3.new(0, 3, 0))
		--		local bottomRay = workspace:Raycast(self.Character.Head.Position + Vector3.new(0, 1, 0), self.Character.Head.CFrame.LookVector * 4 + Vector3.new(0, 1, 0))

		--		if middleRay and bottomRay and not topRay then
		--			print("ledge")
		--		end
		--	end)
		--end
		
		if self.Humanoid.FloorMaterial == Enum.Material.Air then
			local ray = workspace:Raycast(self.HRP.Position + Vector3.new(0, 5, 0), self.Character.Head.CFrame.LookVector * 2)

			if ray then
				--local p = Instance.new("Part")
				--p.Anchored = true
				--p.CanCollide = false
				--p.Size = Vector3.new(0.2, 0.2, self.Character.Head.CFrame.LookVector * 2)
				--p.CFrame = CFrame.lookAt(self.Character.Head.Position, ray.Position) * CFrame.new(0, 0, -self.Character.Head.CFrame.LookVector * 2/2)
				--p.Material = Enum.Material.Neon
				--p.Color = Color3.new(1, 0, 0)
				--p.Parent = workspace
				--self.HRP.AssemblyLinearVelocity =  Vector3.new(self.HRP.AssemblyLinearVelocity.X, 67, self.HRP.AssemblyLinearVelocity.Z)
				
				local Lv = Instance.new("LinearVelocity")
				Lv.Parent = self.HRP
				Lv.MaxForce = math.huge
				Lv.Attachment0 = self.HRP:WaitForChild("RootAttachment")
				Lv.VectorVelocity = Vector3.new(0, 52, 0)

				local Ao = Instance.new("AlignOrientation")
				Ao.Parent = self.HRP
				Ao.Mode = Enum.OrientationAlignmentMode.OneAttachment
				Ao.Attachment0 = self.HRP:WaitForChild("RootAttachment")
				Ao.RigidityEnabled = true
				Ao.CFrame = self.HRP.CFrame
				
				self.sprintToggled = false
				self.canJump = false
				self.climbing = true
				self.canDash = false
				self.StateController:SetState("Climbing")
				
				task.wait(0.4)
				Lv:Destroy()
				Ao:Destroy()
				self.climbing = false
				self.canDash = true
				self.HRP.AssemblyLinearVelocity = Vector3.new(0,15,0)

				
			end
		else
			self.Humanoid.Jump = true
			self.canJump = false
			self.StateController:SetState("Jumping")
		end
	end
end

function MovementController:Dash()
	if self.canDash then
		self.canDash = false
		self.dashing = true
		self.StateController:SetState("Dashing")
		local Lv = Instance.new("LinearVelocity")
		Lv.Parent = self.HRP
		Lv.MaxForce = math.huge
		Lv.Attachment0 = self.HRP:WaitForChild("RootAttachment")
		Lv.VectorVelocity = (self.Humanoid.MoveDirection.Magnitude > 0 and self.Humanoid.MoveDirection or self.HRP.CFrame.LookVector) * self.dashSpeed

		local Ao = Instance.new("AlignOrientation")
		Ao.Parent = self.HRP
		Ao.Mode = Enum.OrientationAlignmentMode.OneAttachment
		Ao.Attachment0 = self.HRP:WaitForChild("RootAttachment")
		Ao.RigidityEnabled = true
		Ao.CFrame = self.HRP.CFrame

		Debris:AddItem(Lv, self.dashTime)
		Debris:AddItem(Ao, self.dashTime)
		task.wait(self.dashTime)
		self.dashing = false
		task.wait(self.dashCD)
		self.canDash = true
	end
end

return MovementController

edit: I know i’ve been posting on the devforum a lot recently so someone has probably seen me before, apologies for that I, again, just wanna be pointed in the right direction. Still have a lot to learn

2 Likes

You could use, and add multiple state machines for cerain things, like movement machine or state.

2 Likes