Skip to content

Instantly share code, notes, and snippets.

@cozywitchcraft
Last active June 24, 2024 15:47
Show Gist options
  • Save cozywitchcraft/00c90382ff2b157d23ad246819e32e6b to your computer and use it in GitHub Desktop.
Save cozywitchcraft/00c90382ff2b157d23ad246819e32e6b to your computer and use it in GitHub Desktop.
-- StarterCharacterScripts/AntiWallhop
local RunService = game:GetService("RunService")
local CAST_POINTS = {
-- Center line
Vector3.new(0, -0.75, 0),
Vector3.new(0, -0.75, 1),
Vector3.new(0, -0.75, -1),
-- Corners
Vector3.new(-1, -0.75, -1),
Vector3.new(-1, -0.75, 1),
Vector3.new(1, -0.75, -1),
Vector3.new(1, -0.75, 1),
}
local DebugVisuals = require(script.DebugVisuals)
local character = script.Parent
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
local rootPart = character:WaitForChild("HumanoidRootPart") :: BasePart
-- Saves an __index call per frame
local isRigTypeR6 = humanoid.RigType == Enum.HumanoidRigType.R6
local params = RaycastParams.new()
params.CollisionGroup = rootPart.CollisionGroup
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
params.RespectCanCollide = true
params.IgnoreWater = true
local postSimulationConnection
local function getHipHeight(halfSize: Vector3): number
-- Using halfSize saves an __index call
local hipHeight = 0.6 * halfSize.Y + humanoid.HipHeight
-- R15 only relies on the hip height while R6 takes legs into consideration
if isRigTypeR6 then
local legHeight = 0
local leftLeg = character:FindFirstChild("Left Leg")
local rightLeg = character:FindFirstChild("Right Leg")
if leftLeg then
legHeight = math.max(legHeight, leftLeg.Size.Y)
end
if rightLeg then
legHeight = math.max(legHeight, rightLeg.Size.Y)
end
hipHeight += legHeight
end
return hipHeight
end
local function isGrounded(): boolean
local cframe = rootPart.CFrame
local halfSize = 0.4 * rootPart.Size -- 0.1-stud skin
local factor = 1.5
local verticalVelocity = rootPart.AssemblyLinearVelocity.Y
local minVelocity = -125 * halfSize.Y
if verticalVelocity > 0 then
return false -- Prevents double jumping
elseif verticalVelocity < minVelocity then
factor += verticalVelocity / minVelocity -- Prevents bouncing
end
local direction = getHipHeight(halfSize) * factor * -Vector3.yAxis
DebugVisuals.reset()
for _, point in CAST_POINTS do
local origin = cframe:PointToWorldSpace(halfSize * point)
local result = workspace:Raycast(origin, direction, params)
DebugVisuals.drawLine(origin, direction, result)
if result then
return true
end
end
return false
end
local function onPostSimulation(deltaTime: number)
debug.profilebegin("AntiWallhop::onPostSimulation")
local isFalling = humanoid:GetState() == Enum.HumanoidStateType.Freefall
-- We're only worried about detecting the floor while the humanoid is falling.
-- This is because wallhopping only occurs when entering the landing state pre-
-- simulation, so we move it post-simulation.
if isFalling and isGrounded() then
humanoid:ChangeState(Enum.HumanoidStateType.Landed)
end
debug.profileend()
end
local function enableAntiWallhop()
if postSimulationConnection then
return
end
-- Disable humanoid's built-in floor detection
humanoid:SetStateEnabled(Enum.HumanoidStateType.Landed, false)
-- Start custom floor detection after physics simulation
postSimulationConnection = RunService.PostSimulation:Connect(onPostSimulation)
end
local function disableAntiWallhop()
if not postSimulationConnection then
return
end
humanoid:SetStateEnabled(Enum.HumanoidStateType.Landed, true)
postSimulationConnection = postSimulationConnection:Disconnect()
end
local function toggleAntiWallhop()
if script:GetAttribute("AntiWallhopEnabled") then
enableAntiWallhop()
else
disableAntiWallhop()
end
end
local function toggleDebugVisuals()
if script:GetAttribute("ShowDebugVisuals") then
DebugVisuals.mount(character)
else
DebugVisuals.unmount()
end
end
-- Support collision groups (e.g. player-to-player non-collision)
rootPart:GetPropertyChangedSignal("CollisionGroup"):Connect(function()
params.CollisionGroup = rootPart.CollisionGroup
end)
script:GetAttributeChangedSignal("AntiWallhopEnabled"):Connect(toggleAntiWallhop)
script:GetAttributeChangedSignal("ShowDebugVisuals"):Connect(toggleDebugVisuals)
toggleAntiWallhop()
toggleDebugVisuals()
local MAX_LINES = 7
local isShowing = false
local lines: { LineHandleAdornment } = {}
local lineIndex = 1
local terrain = workspace.Terrain
local DebugVisuals = {}
function DebugVisuals.mount(character: Model)
if isShowing then
return
end
for _ = 1, MAX_LINES do
local line = Instance.new("LineHandleAdornment")
line.Visible = false
line.Adornee = terrain
-- HACK: Lines are destroyed when character is destroyed
line.Parent = character
table.insert(lines, line)
end
isShowing = true
end
function DebugVisuals.unmount()
if not isShowing then
return
end
for _, line in lines do
line:Destroy()
end
table.clear(lines)
isShowing = false
end
function DebugVisuals.drawLine(origin, direction, result)
if not isShowing then
return
end
local color = Color3.new(result and 0 or 1, result and 1 or 0, 0)
local line = lines[lineIndex]
line.CFrame = CFrame.new(origin, origin + direction)
line.Length = direction.Magnitude
line.Thickness = 3
line.Color3 = color
line.Visible = true
lineIndex += 1
end
function DebugVisuals.reset()
if not isShowing then
return
end
for _, line in lines do
line.Visible = false
end
lineIndex = 1
end
return DebugVisuals
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment