Hi,
I made a movement module for my parkour game and the more I write code to expand on the module, the more messy it gets. Like really messy. All I really want to clean is the Wallrun part of the module but everything else is managable.
Here’s the entire module:
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local CollectionService = game:GetService("CollectionService")
local SoundService = game:GetService("SoundService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local PlayerSettings = require(ReplicatedStorage.PlayerSettings)
local Trove = require(ReplicatedStorage.Packages._Index["sleitnick_trove@1.1.0"]["trove"])
local Speedlines = require(ReplicatedStorage.Source.Classes.Speedlines)
local RaycastUtil = require(script.RaycastUtil)
local PlaySound = require(ReplicatedStorage.Source.Dependencies.Util.playSound)
local Assets = ReplicatedStorage.Assets
local Animations = Assets.Animations.Movement
local CurrentCamera = workspace.CurrentCamera
local Player = game.Players.LocalPlayer
local moveDirectionDB = false
local WaitTimer = PlayerSettings.BaseYield
local WallrunUpdate = nil :: RBXScriptConnection?
local Tags = {
Wallrun = "Wallrun",
Walljump = "Walljump",
};
local Blacklisted = {} :: any
local LoadedAnimations = {} :: any
local Movement = {}
Movement.__index = Movement
export type ClassType = typeof( setmetatable({} :: {
isWallrunning: boolean,
isEnabled: boolean,
character: Model?,
speedlines: Speedlines.ClassType?,
--// wall run
lastJump: number,
lastWallDirection: string?, -- wall run and wall jump..
jumpDebounceDelay: number,
wallrunParameters: RaycastParams,
previousWall: Instance?,
Connections: Trove.ClassType,
}, Movement) )
function Movement.new(): ClassType
local self = {
isWallrunning = false,
--isWalljumping = false,
isEnabled = false,
character = nil,
speedlines = nil,
lastJump = tick(),
lastWallDirection = nil,
jumpDebounceDelay = 0.05,
wallrunParameters = RaycastParams.new(),
previousWall = nil,
Connections = Trove.new(),
};
setmetatable(self, Movement)
self:_init()
return self
end
function Movement._init(self: ClassType): ()
self:_setupCharacterAdded()
end
function Movement._setupCharacterAdded(self: ClassType): ()
Players.LocalPlayer.CharacterAppearanceLoaded:Connect(function(character: Model)
self.character = character
if self.isEnabled and self.character then
self:Stop()
self:Start()
for _, v in pairs(self.character:GetChildren()) do
if not v:IsA("BasePart") then
continue
end
table.insert(Blacklisted, v)
end
self.wallrunParameters.FilterType = Enum.RaycastFilterType.Exclude
self.wallrunParameters.FilterDescendantsInstances = Blacklisted
end
end)
if Players.LocalPlayer then
if not self.character then
self.character = Players.LocalPlayer.Character:: Model
end
end
if self.character then
for _, v in pairs(self.character:GetChildren()) do
if not v:IsA("BasePart") then
continue
end
table.insert(Blacklisted, v)
end
self.wallrunParameters.FilterType = Enum.RaycastFilterType.Exclude
self.wallrunParameters.FilterDescendantsInstances = Blacklisted
end
end
function Movement.Start(self: ClassType): ()
self.isEnabled = true
if self.character then
local Humanoid = self.character:FindFirstChild("Humanoid") :: Humanoid
local RootPart = self.character:FindFirstChild("HumanoidRootPart") :: BasePart
local Animator = Humanoid:FindFirstChild("Animator") :: Animator
self.Connections:Connect(Humanoid:GetPropertyChangedSignal("MoveDirection"), function(...: any)
self:onMoveDirection(Humanoid, RootPart)
end)
if UserInputService.TouchEnabled then -- mobile jump detection
local TouchControlFrame = Player
:WaitForChild("PlayerGui")
:FindFirstChild("TouchGui")
:FindFirstChild("TouchControlFrame")
local JumpButton = TouchControlFrame:FindFirstChild("JumpButton") :: ImageButton
self.Connections:Connect(JumpButton.Activated, function(inputObject: InputObject, clickCount: number)
self:onJumpRequest(Humanoid, RootPart)
end)
end
self.Connections:Connect(UserInputService.InputBegan, function(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent then return end
if input.KeyCode == Enum.KeyCode.Space then
self:onJumpRequest(Humanoid, RootPart)
end
end)
self.Connections:Connect(Humanoid.StateChanged, function(old: Enum.HumanoidStateType, new: Enum.HumanoidStateType)
self:onStateChanged(old, Humanoid, RootPart)
end)
if not self.speedlines then
self.speedlines = Speedlines.new(self.character, PlayerSettings.SpeedlinesMinimumSpeed, PlayerSettings.SpeedlinesMinimumRate)
end
if self.speedlines then
self.speedlines:Start()
end
for _, animation in pairs(Animations:GetChildren()) do
LoadedAnimations[animation.Name] = Animator:LoadAnimation(animation)
end
end
end
function Movement.Stop(self: ClassType): ()
self.Connections:Clean()
self.isEnabled = false
if self.character then
local Humanoid = self.character:FindFirstChild("Humanoid") :: Humanoid
Humanoid.WalkSpeed = PlayerSettings.CharacterWalkSpeed
end
if self.speedlines then
self.speedlines:Destroy()
self.speedlines = nil
end
table.clear(LoadedAnimations)
end
function Movement.onMoveDirection(self: ClassType, Humanoid, Root): ()
local isGrounded = if Humanoid.FloorMaterial == Enum.Material.Air then false else true
local velocity = Root.CFrame:Inverse() * (Root.Position + Root.AssemblyLinearVelocity)
local yDirection = math.atan2(velocity.X, -velocity.Z)
local roundedDirection = math.ceil(math.deg(yDirection) - 0.5)
local isStrafingOrBackwards = if roundedDirection >= 70 and roundedDirection <= 100 or
roundedDirection <= -70 and roundedDirection >= -100 or
roundedDirection <= -135 or roundedDirection >= 135
then true else false
local isBackwards = if roundedDirection <= -135 or roundedDirection >= 135
then true else false
if Humanoid:GetState() ~= Enum.HumanoidStateType.Jumping
and Humanoid:GetState() ~= Enum.HumanoidStateType.Freefall
and isGrounded then
if Humanoid.MoveDirection ~= Vector3.new(0, 0, 0) and not isStrafingOrBackwards and moveDirectionDB == false and Humanoid.WalkSpeed < PlayerSettings.AccelerationMaxSpeed then
moveDirectionDB = true
while Humanoid.MoveDirection ~= Vector3.new(0, 0, 0) and Humanoid.WalkSpeed < PlayerSettings.AccelerationMaxSpeed do
Humanoid.WalkSpeed = Humanoid.WalkSpeed + 1
task.wait(WaitTimer)
WaitTimer = WaitTimer / 1.1
end
moveDirectionDB = false
elseif Humanoid.MoveDirection == Vector3.new(0, 0, 0) or isStrafingOrBackwards then
WaitTimer = PlayerSettings.BaseYield
Humanoid.WalkSpeed = PlayerSettings.CharacterWalkSpeed
end
end
end
function Movement.onJumpRequest(self: ClassType, Humanoid, Root): ()
local currentTime = tick()
local isGrounded = if Humanoid.FloorMaterial == Enum.Material.Air then false else true
local velocity = Root.AssemblyLinearVelocity.Magnitude
if currentTime - self.lastJump >= self.jumpDebounceDelay then
self.lastJump = currentTime
if self.isWallrunning and WallrunUpdate then
WallrunUpdate:Disconnect()
Humanoid:ChangeState(Enum.HumanoidStateType.Jumping) -- force jump
Humanoid.WalkSpeed = PlayerSettings.AccelerationMaxSpeed -- keep their speed
PlaySound("rbxassetid://5864343876", Root, SoundService.Character, 0.3)
for _, v in pairs(LoadedAnimations) do
if not v:IsA("AnimationTrack") then
continue
end
if v.IsPlaying then
v:Stop()
end
end
self.isWallrunning = false
end
if not isGrounded and velocity >= 21 then
self:Wallrun(Humanoid, Root)
end
if not isGrounded then
self:Walljump(Humanoid, Root)
end
end
end
function Movement.Walljump(self: ClassType, Humanoid, Root)
local isGrounded = if Humanoid.FloorMaterial == Enum.Material.Air or Humanoid:GetState() == Enum.HumanoidStateType.Freefall then false else true
local _front = RaycastUtil.FrontCheck(Root, PlayerSettings.FrontRayLength, self.wallrunParameters) :: RaycastResult
if _front and CollectionService:HasTag(_front.Instance, Tags.Walljump )then
local Rotate = _front.Instance:GetAttribute("Rotate") or false
RaycastUtil.DebugAttatchment(_front.Position)
RaycastUtil.VisibleRay(Root, _front)
PlaySound("rbxassetid://1724363484", _front.Instance, SoundService.Character, 0.3)
Humanoid:ChangeState(Enum.HumanoidStateType.Jumping) -- force jump
if Rotate then
CurrentCamera.CFrame = CFrame.fromEulerAnglesXYZ(0, math.rad(180) , 0) * CurrentCamera.CFrame
end
Humanoid.WalkSpeed = PlayerSettings.AccelerationMaxSpeed -- keep their speed
end
end
function Movement.Wallrun(self: ClassType, Humanoid, Root)
local isGrounded = if Humanoid.FloorMaterial == Enum.Material.Air or Humanoid:GetState() == Enum.HumanoidStateType.Freefall then false else true
local velocity = Root.AssemblyLinearVelocity.Magnitude
if self.previousWall then
local LeftWallCheck = RaycastUtil.LeftCheck(Root, PlayerSettings.WallrunRayLength, self.wallrunParameters) :: RaycastResult
local RightWallCheck = RaycastUtil.RightCheck(Root, PlayerSettings.WallrunRayLength, self.wallrunParameters) :: RaycastResult
local Result = LeftWallCheck or RightWallCheck
if Result and self.previousWall == Result.Instance and not isGrounded then
--print("Player tried to wallrun while freefalling on previous wall")
return
end
end
local _left = RaycastUtil.LeftCheck(Root, PlayerSettings.WallrunRayLength, self.wallrunParameters) :: RaycastResult
local _right = RaycastUtil.RightCheck(Root, PlayerSettings.WallrunRayLength, self.wallrunParameters) :: RaycastResult
local _result = _left or _right
if _result and not CollectionService:HasTag(_result.Instance, Tags.Wallrun) then
if WallrunUpdate then
WallrunUpdate:Disconnect()
self.isWallrunning = false
Humanoid.WalkSpeed = PlayerSettings.AccelerationMaxSpeed
for _, v in pairs(LoadedAnimations) do
if not v:IsA("AnimationTrack") then
continue
end
if v.IsPlaying then
v:Stop()
end
end
end
return
end
task.spawn(function()
WallrunUpdate = RunService.RenderStepped:Connect(function(deltaTime: number)
local vectorVelocity = Root.CFrame:Inverse() * (Root.Position + Root.AssemblyLinearVelocity)
local yDirection = math.atan2(vectorVelocity.X, -vectorVelocity.Z)
local roundedDirection = math.ceil(math.deg(yDirection) - 0.5)
local LeftWallCheck = RaycastUtil.LeftCheck(Root, PlayerSettings.WallrunRayLength, self.wallrunParameters) :: RaycastResult
local RightWallCheck = RaycastUtil.RightCheck(Root, PlayerSettings.WallrunRayLength, self.wallrunParameters) :: RaycastResult
local Result = LeftWallCheck or RightWallCheck
if Result == LeftWallCheck then
self.lastWallDirection = "Left" -- shiftlock
LoadedAnimations.WallrunLeft:Play()
if roundedDirection > 37 and WallrunUpdate then
WallrunUpdate:Disconnect()
self.isWallrunning = false
for _, v in pairs(LoadedAnimations) do
if not v:IsA("AnimationTrack") then
continue
end
if v.IsPlaying then
v:Stop()
end
end
end
else
self.lastWallDirection = "Right" -- shiftlock
LoadedAnimations.WallrunRight:Play()
if roundedDirection < -37 and WallrunUpdate then
WallrunUpdate:Disconnect()
self.isWallrunning = false
for _, v in pairs(LoadedAnimations) do
if not v:IsA("AnimationTrack") then
continue
end
if v.IsPlaying then
v:Stop()
end
end
end
end
if Result and velocity >= 21 and CollectionService:HasTag(Result.Instance, Tags.Wallrun) then
self.isWallrunning = true
RaycastUtil.DebugAttatchment(Result.Position)
RaycastUtil.VisibleRay(Root, Result)
Root.AssemblyLinearVelocity = Vector3.new(Root.AssemblyLinearVelocity.X,-1,Root.AssemblyLinearVelocity.Z)
self.previousWall = Result.Instance
elseif not Result
or velocity < 21
or Humanoid:GetState() ~= Enum.HumanoidStateType.Freefall then
self.isWallrunning = false
if WallrunUpdate then
for _, v in pairs(LoadedAnimations) do
if not v:IsA("AnimationTrack") then
continue
end
if v.IsPlaying then
v:Stop()
end
end
WallrunUpdate:Disconnect()
self.isWallrunning = false
--Humanoid.WalkSpeed = PlayerSettings.AccelerationMaxSpeed
end
end
end)
end)
end
function Movement.onStateChanged(self: ClassType, old, Humanoid, Root): ()
if old == Enum.HumanoidStateType.Landed then
if Humanoid.MoveDirection == Vector3.new(0,0,0) then -- tanding still after jump
Humanoid.WalkSpeed = PlayerSettings.CharacterWalkSpeed
end
self.previousWall = nil
if WallrunUpdate then
WallrunUpdate:Disconnect()
for _, v in pairs(LoadedAnimations) do
if not v:IsA("AnimationTrack") then
continue
end
if v.IsPlaying then
v:Stop()
end
end
self.isWallrunning = false
end
end
end
function Movement.Destroy(self: ClassType): ()
self.Connections:Destroy()
if self.speedlines then -- destroy
self.speedlines:Destroy()
end
end
return Movement
Thank you for reading