How can i make vanilla CS:GO Bhop?

Anyone knows how to make vanilla CS:GO bhopping in roblox studio? Not auto hop btw, if anyone knows tell me.

You’d have to replicate the source engine movement to do that. You could try looking at the actual source code and attempt to replicate it in Roblox.

10 Likes

but its better to not replicate it, because its bad in some aspects, and also as we know bhopping is a bug, it’s not supposed to work.

What i am asking it to make a bunnyhopping like it’s not even a bug on LUA.

I don’t think it’ll be a bug if it’s intended, but just try to remake something like this and see if it’s fine, not really sure what you mean by the “bad in some aspects”, also you need to use some camera functions with a movement vector (3 axes of -1, 0 or 1 depending on what keys are pressed) to get wishdir

I am doing this right now, and you are right. I do get wish dir, but also I implemented surfing (slope handling).

So, this is how I made bhopping:

local state = {
	vel = Vector3.new(),
	onGround = true,
	groundNormal = Vector3.new(0,1,0),
	inWater = false,
	onLadder = false,
	crouching = false,
	jumpQueued = false,
	jumpHeld = false,
	canJump = true,
	surf = false,
	crouchSliding = false,
	crouchSlideTimer = 0,
	fallVel = 0,
	landed = false,
	lastLandTime = 0,
	prevCrouch = false,
	prevMoveDir = Vector3.new(), -- Added for counter-strafe
	-- For counter-strafe tap logic:
	axisHoldStart = {X = 0, Z = 0}, -- When did we start holding a direction on each axis
	axisLastDir = {X = 0, Z = 0},   -- Last nonzero direction on each axis
	axisTapTime = {X = 0, Z = 0},   -- How long the opposite was held before release
	wasOnGround = true, -- Track previous onGround state for bunnyhop/counter-strafe logic
}

local input = {
	moveDir = Vector3.new(),
	jump = false,
	crouch = false,
}

-- Input handling
local moveVector = Vector3.new()
local jumpPressed = false
local jumpPressedLast = false -- Track previous jump key state
local jumpConsumed = false    -- Prevents holding space for auto-hop
local crouchPressed = false

is the variables we need.

then make jumpressed true when player uses spacebar and crouchpressed true when left control is pressed (via UserInputService.inputbegan).

Use the same but now with input ended. jumpPressed = false, jumpConsumed = false, crouchPressed = false.

this is how you get move direction:

local function getMoveDirection()
	local dir = Vector3.new()
	if UserInputService:IsKeyDown(Enum.KeyCode.W) then dir = dir + Vector3.new(0,0,-1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.S) then dir = dir + Vector3.new(0,0,1) end
	if UserInputService:IsKeyDown(Enum.KeyCode.A) then dir = dir + Vector3.new(-1,0,0) end
	if UserInputService:IsKeyDown(Enum.KeyCode.D) then dir = dir + Vector3.new(1,0,0) end
	-- Camera relative
	local cam = workspace.CurrentCamera
	if cam then
		local cf = cam.CFrame
		local move = (cf.LookVector * -dir.Z + cf.RightVector * dir.X)
		move = Vector3.new(move.X, 0, move.Z)
		if move.Magnitude > 0 then
			return move.Unit
		end
	end
	return Vector3.new()
end

then make variables:

-- Main movement loop
local lastGrounded = true
local lastY = rootPart.Position.Y

make runservice.renderstepped and update the input, Only allow jump on the frame the key is pressed (no hold-to-bhop), Block further jumps until key is released, Store previous move direction for counter-strafe.
Make counter-strafe tap logic (Track axis hold times and tap durations).

local now = tick()
	for _, axis in {"X", "Z"} do
		local prev = state.axisLastDir[axis] or 0
		local curr = input.moveDir[axis]
		-- If we just started holding a direction (from 0 to nonzero)
		if math.abs(prev) < 0.01 and math.abs(curr) > 0.5 then
			state.axisHoldStart[axis] = now
		end
		-- If we just released a direction (from nonzero to 0)
		if math.abs(prev) > 0.5 and math.abs(curr) < 0.01 then
			state.axisTapTime[axis] = now - (state.axisHoldStart[axis] or now)
		end
		state.axisLastDir[axis] = curr
	end

now check if on the ground with raycast.

make Water/Ladder detection

create Fall velocity

state.fallVel = rootPart.Velocity.Y

make landing detection, track previous onGround state for bunnyhop/counter-strafe logic.
Make crouch slide logic.

state.prevCrouch = state.crouching
	state.crouching = input.crouch
	SourceMovement:TryStartCrouchSlide(state, input)

apply movement

state.vel = SourceMovement:Move(state, input, dt)

Apply velocity to character and make crouching.
Clamp speed for sanity

Module Script:

-- SourceMovement ModuleScript
-- Implements Source-engine-like movement physics for Roblox characters, with bhop, surfing, and crouch sliding

local SourceMovement = {}

-- Tweakable parameters
local WALK_SPEED = 16
local CROUCH_SPEED = 8
local JUMP_POWER = 50
local CROUCH_JUMP_BOOST = 1.2
local AIR_ACCEL = 34 -- Increased for more bhop speed gain
local GROUND_ACCEL = 64
local FRICTION = 8
local AIR_FRICTION = 0.5
local SLOPE_SLIDE_ANGLE = 40
local SLOPE_SLOW_ANGLE = 20
local SURF_MIN_ANGLE = 10
local SURF_MAX_ANGLE = 80
local SURF_FRICTION = 0.01
local SURF_AIR_ACCEL = 60
local SURF_MAX_SPEED = 60
local LANDING_PENALTY_VEL = 60
local LANDING_PENALTY_TIME = 0.4
local WATER_SPEED = 8
local LADDER_SPEED = 10
local MAX_AIR_SPEED = 32 -- Increased for more bhop speed gain
local CROUCH_SLIDE_SPEED = 30
local CROUCH_SLIDE_TIME = 0.5
local CROUCH_SLIDE_FRICTION = 1

local COUNTERSTRAFE_TAP_THRESHOLD = 0.15 -- seconds, must tap opposite key within this time to trigger instant stop

local function getSlopeAngle(normal)
	if normal.Y == 0 then return 90 end
	return math.deg(math.acos(normal.Y))
end

function SourceMovement:IsSurfing(groundNormal, onGround, onLadder, inWater)
	if not onGround or onLadder or inWater then return false end
	local angle = getSlopeAngle(groundNormal)
	return angle > SURF_MIN_ANGLE and angle < SURF_MAX_ANGLE
end

function SourceMovement:ApplyFriction(vel, dt, onGround, frictionOverride)
	local friction = frictionOverride or (onGround and FRICTION or AIR_FRICTION)
	local speed = vel.Magnitude
	if speed ~= 0 then
		local drop = speed * friction * dt
		local newSpeed = math.max(speed - drop, 0)
		return vel.Unit * newSpeed
	end
	return vel
end

function SourceMovement:Accelerate(vel, wishDir, wishSpeed, accel, dt, maxSpeed)
	local currentSpeed = vel:Dot(wishDir)
	local addSpeed = wishSpeed - currentSpeed
	if addSpeed <= 0 then return vel end
	local accelSpeed = accel * dt * wishSpeed
	if accelSpeed > addSpeed then accelSpeed = addSpeed end
	local newVel = vel + wishDir * accelSpeed
	if maxSpeed and newVel.Magnitude > maxSpeed then
		newVel = newVel.Unit * maxSpeed
	end
	return newVel
end

function SourceMovement:HandleSlope(vel, groundNormal)
	local angle = getSlopeAngle(groundNormal)
	if angle > SLOPE_SLIDE_ANGLE then
		-- Slide down steep slopes
		local slideDir = Vector3.new(groundNormal.X, 0, groundNormal.Z)
		if slideDir.Magnitude > 0 then
			slideDir = slideDir.Unit
			return vel + slideDir * (angle - SLOPE_SLIDE_ANGLE)
		end
	elseif angle > SLOPE_SLOW_ANGLE then
		-- Slow uphill
		return vel * 0.7
	end
	return vel
end

function SourceMovement:HandleLandingPenalty(vel, fallVel, landed, lastLandTime, now)
	if landed and fallVel < -LANDING_PENALTY_VEL and (now - lastLandTime) > 0.1 then
		return vel * 0.5, now
	end
	return vel, lastLandTime
end

-- Counter-strafe helper
local function counterStrafeVelocity(vel, moveDir, prevMoveDir, axisTapTime, canCounterStrafe)
	-- Only apply if not holding both directions (e.g. not holding A and D at the same time)
	-- X axis: A (-1) and D (+1)
	-- Z axis: W (-1) and S (+1)
	local newVel = vel

	if not canCounterStrafe then
		return newVel
	end

	if prevMoveDir and moveDir then
		-- Counter-strafe left/right (A/D)
		if prevMoveDir.X > 0.5 and moveDir.X < -0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			-- D to A
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		elseif prevMoveDir.X < -0.5 and moveDir.X > 0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
			-- A to D
			newVel = Vector3.new(0, newVel.Y, newVel.Z)
		end

		-- Counter-strafe forward/back (W/S)
		if prevMoveDir.Z > 0.5 and moveDir.Z < -0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			-- S to W
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		elseif prevMoveDir.Z < -0.5 and moveDir.Z > 0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
			-- W to S
			newVel = Vector3.new(newVel.X, newVel.Y, 0)
		end

		-- NEW: If you release the opposite key after counter-strafe (i.e. go from input to zero), stop instantly
		-- Only if the opposite key was tapped (held for less than threshold)
		-- X axis
		if math.abs(prevMoveDir.X) > 0.5 and math.abs(moveDir.X) <= 0.01 then
			if axisTapTime and axisTapTime.X and axisTapTime.X > 0 and axisTapTime.X <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(0, newVel.Y, newVel.Z)
			end
		end
		-- Z axis
		if math.abs(prevMoveDir.Z) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
			if axisTapTime and axisTapTime.Z and axisTapTime.Z > 0 and axisTapTime.Z <= COUNTERSTRAFE_TAP_THRESHOLD then
				newVel = Vector3.new(newVel.X, newVel.Y, 0)
			end
		end
	end

	return newVel
end

function SourceMovement:Move(state, input, dt)
	-- state: {vel, onGround, groundNormal, inWater, onLadder, crouching, jumpQueued, jumpHeld, canJump, surf, crouchSliding, crouchSlideTimer, fallVel, landed, lastLandTime, prevMoveDir, axisTapTime, wasOnGround}
	-- input: {moveDir, jump, crouch}
	local vel = state.vel
	local onGround = state.onGround
	local groundNormal = state.groundNormal
	local inWater = state.inWater
	local onLadder = state.onLadder
	local crouching = input.crouch
	local jump = input.jump
	local moveDir = input.moveDir
	local dt = dt
	local now = tick()
	local surf = self:IsSurfing(groundNormal, onGround, onLadder, inWater)
	state.surf = surf

	-- Counter-strafe logic (only on ground and not bunnyhopping)
	local canCounterStrafe = false
	if onGround and state.wasOnGround then
		canCounterStrafe = true
	end

	if canCounterStrafe then
		vel = counterStrafeVelocity(vel, moveDir, state.prevMoveDir, state.axisTapTime, canCounterStrafe)
		-- Reset axisTapTime after using it, so it only applies once per tap
		if state.axisTapTime then
			if math.abs(state.prevMoveDir and state.prevMoveDir.X or 0) > 0.5 and math.abs(moveDir.X) <= 0.01 then
				state.axisTapTime.X = 0
			end
			if math.abs(state.prevMoveDir and state.prevMoveDir.Z or 0) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
				state.axisTapTime.Z = 0
			end
		end
	end

	-- Water/Ladder
	if inWater then
		vel = moveDir * WATER_SPEED
		if jump then vel = vel + Vector3.new(0, JUMP_POWER * 0.5, 0) end
		return vel
	elseif onLadder then
		vel = moveDir * LADDER_SPEED
		if jump then vel = vel + Vector3.new(0, LADDER_SPEED, 0) end
		return vel
	end

	-- Crouch sliding
	if state.crouchSliding then
		vel = self:ApplyFriction(vel, dt, true, CROUCH_SLIDE_FRICTION)
		vel = self:Accelerate(vel, moveDir, CROUCH_SLIDE_SPEED, GROUND_ACCEL, dt, CROUCH_SLIDE_SPEED)
		state.crouchSlideTimer = state.crouchSlideTimer - dt
		if state.crouchSlideTimer <= 0 or not crouching then
			state.crouchSliding = false
		end
		return vel
	end

	-- Surfing
	if surf then
		-- Frictionless, high air acceleration, can't jump
		vel = self:ApplyFriction(vel, dt, true, SURF_FRICTION)
		if moveDir.Magnitude > 0 then
			vel = self:Accelerate(vel, moveDir.Unit, SURF_MAX_SPEED, SURF_AIR_ACCEL, dt, SURF_MAX_SPEED)
		end
		-- Project velocity onto surf plane
		local surfNormal = groundNormal
		vel = vel - surfNormal * vel:Dot(surfNormal)
		return vel
	end

	-- Friction
	vel = self:ApplyFriction(vel, dt, onGround)

	-- Slope
	if onGround then
		vel = self:HandleSlope(vel, groundNormal)
	end

	-- Acceleration
	local wishSpeed = crouching and CROUCH_SPEED or WALK_SPEED
	local accel = onGround and GROUND_ACCEL or AIR_ACCEL
	if not onGround then wishSpeed = math.min(wishSpeed, MAX_AIR_SPEED) end
	if moveDir.Magnitude > 0 then
		vel = self:Accelerate(vel, moveDir.Unit, wishSpeed, accel, dt, (onGround and nil or MAX_AIR_SPEED))
	end

	-- Jump (no auto-hop: must release and repress jump)
	if jump and onGround and state.canJump then
		vel = Vector3.new(vel.X, JUMP_POWER, vel.Z)
		if crouching then
			vel = vel * CROUCH_JUMP_BOOST
		end
		state.canJump = false
	elseif not jump and onGround then
		state.canJump = true
	end

	-- Air control (strafe jumping)
	if not onGround and moveDir.Magnitude > 0 then
		local airControl = 0.4 -- Slightly increased for more air control
		local proj = vel:Dot(moveDir.Unit)
		if proj < wishSpeed then
			vel = vel + moveDir.Unit * AIR_ACCEL * dt * airControl
		end
	end

	-- Inertia (momentum preserved)
	-- Already handled by not resetting velocity

	-- Landing penalty
	vel, state.lastLandTime = self:HandleLandingPenalty(vel, state.fallVel, state.landed, state.lastLandTime, now)

	return vel
end

function SourceMovement:TryStartCrouchSlide(state, input)
	-- Start crouch slide if moving fast, on ground, and just pressed crouch
	if state.onGround and not state.crouchSliding and input.crouch and not state.prevCrouch and state.vel.Magnitude > WALK_SPEED * 1.2 then
		state.crouchSliding = true
		state.crouchSlideTimer = CROUCH_SLIDE_TIME
	end
end

return SourceMovement

Modify if you don’t like something, it’s not perfect and I only made it in like one hour.

2 Likes

also it’s still working like an auto hop, so you still have to modify it

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