Skip to content

Instantly share code, notes, and snippets.

@TellonUK
Last active August 31, 2025 20:25
Show Gist options
  • Select an option

  • Save TellonUK/eed3224e1b3890b1d57dcf1374a8293a to your computer and use it in GitHub Desktop.

Select an option

Save TellonUK/eed3224e1b3890b1d57dcf1374a8293a to your computer and use it in GitHub Desktop.
extends CharacterBody3D
# Movement constants - based on Quake/Half-Life physics
const MAX_VELOCITY_AIR = 0.6
const MAX_VELOCITY_GROUND = 6.0
const MAX_ACCELERATION = 10 * MAX_VELOCITY_GROUND
const STOP_SPEED = 1.5
const FRICTION = 8.0 # Increased for less sliding, more Quake-like stopping
# Exported settings
@export var jump_height: float = 1.0 # Multiplier for jump height calculation
@export var bhop_frames: int = 3 # Frames of tolerance for bunny hopping
@export var additive_bhop: bool = false # More forgiving bhop mode
@export var sensitivity: float = 0.002 # Mouse sensitivity
@export var joypad_sensitivity: float = 2.0 # Mouse sensitivity
@export var joypad_deadzone: float = 0.1 # Deadzone for analog sticks
# Walking behavior
@export var walk_speed_multiplier: float = 0.5 # Half speed when walking
@export var walk_threshold: float = 0.5 # Stick magnitude below this => walk
@export var use_analog_scaling: bool = true # Scale speed by stick tilt (on top of walk)
# Camera and input
var mouse_motion := Vector2.ZERO
@onready var camera_pivot: Node3D = $CameraPivot
# Movement variables
var direction = Vector3()
var wish_jump = false
var bhop_frame_count: int = 0
# Cached per-frame input strength (0..1) and walking flag
var move_strength: float = 0.0
var is_walking: bool = false
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _physics_process(delta: float) -> void:
handle_camera_rotation(delta)
process_input()
process_movement(delta)
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
mouse_motion = -event.relative * sensitivity
if event.is_action_pressed("ui_cancel"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func handle_camera_rotation(delta: float) -> void:
rotate_y(mouse_motion.x)
camera_pivot.rotate_x(mouse_motion.y)
camera_pivot.rotation_degrees.x = clampf(
camera_pivot.rotation_degrees.x, -90.0, 90.0
)
mouse_motion = Vector2.ZERO
var joypad_look = Vector2.ZERO
joypad_look.x = Input.get_axis("look_left", "look_right")
joypad_look.y = Input.get_axis("look_up", "look_down")
if joypad_look.length() > joypad_deadzone:
rotate_y(-joypad_look.x * joypad_sensitivity * delta)
camera_pivot.rotate_x(-joypad_look.y * joypad_sensitivity * delta)
func process_input():
# Read analog move vector (x = left/right, y = forward/back)
var mv2 := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
# Apply deadzone
if mv2.length() < joypad_deadzone:
mv2 = Vector2.ZERO
# Build world-space desired direction (don’t normalize yet; we need strength)
direction = (transform.basis.z * mv2.y) + (transform.basis.x * mv2.x)
# Store analog strength (0..1)
move_strength = clamp(mv2.length(), 0.0, 1.0)
# Keyboard jump button
wish_jump = Input.is_action_just_pressed("jump")
# Determine walking:
# - if 'walk' is held on keyboard OR
# - if stick is slightly pressed (below threshold but above deadzone)
var walk_from_stick := move_strength > 0.0 and move_strength < walk_threshold
is_walking = Input.is_action_pressed("walk") or walk_from_stick
func process_movement(delta: float):
# Handle basic jump first (like original script)
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = sqrt(jump_height * 2.0 * abs(get_gravity().y))
# Apply gravity
if not is_on_floor():
velocity += get_gravity() * delta
# Normalized wish direction for acceleration math
var wish_dir = direction.normalized()
# Compute effective speeds/accel based on walking & analog strength
var base_ground_speed := MAX_VELOCITY_GROUND
var base_air_speed := MAX_VELOCITY_AIR
var base_accel := MAX_ACCELERATION
# Optional continuous analog scaling of top speed
var analog_scale := move_strength if (use_analog_scaling and move_strength > 0.0) else 1.0
# Walk halves both speed and acceleration
var walk_scale := walk_speed_multiplier if is_walking else 1.0
var eff_ground_speed := base_ground_speed * analog_scale * walk_scale
var eff_air_speed := base_air_speed * analog_scale * walk_scale
var eff_accel := base_accel * walk_scale # acceleration halves when walking
# Handle bhop frame tolerance for advanced users
if is_on_floor():
bhop_frame_count = 0
else:
bhop_frame_count += 1
# Movement physics (pass effective max accel into accelerate)
if is_on_floor():
# Special case: if jumping, apply air physics for bunny hop
if wish_jump and bhop_frame_count == 0:
velocity = update_velocity_air(wish_dir, eff_air_speed, eff_accel, delta)
wish_jump = false
else:
velocity = update_velocity_ground(wish_dir, eff_ground_speed, eff_accel, delta)
else:
velocity = update_velocity_air(wish_dir, eff_air_speed, eff_accel, delta)
# Move the player once velocity has been calculated
move_and_slide()
# Accelerate now takes max_accel as a parameter
func accelerate(wish_dir: Vector3, max_velocity: float, max_accel: float, delta: float):
# Get our current speed as a projection of velocity onto the wish_dir
var current_speed = velocity.dot(wish_dir)
# Amount to add is limited by remaining speed budget and accel cap
var add_speed = clamp(max_velocity - current_speed, 0, max_accel * delta)
# Apply acceleration
var new_velocity = velocity + add_speed * wish_dir
# Additive bhop mode: more forgiving, velocity converges to input direction
if additive_bhop and not is_on_floor() and wish_dir.length() > 0:
var horizontal_vel = Vector3(velocity.x, 0, velocity.z)
var desired_vel = wish_dir * min(horizontal_vel.length() + add_speed, max_velocity * 5)
new_velocity.x = lerp(velocity.x, desired_vel.x, delta * 8.0)
new_velocity.z = lerp(velocity.z, desired_vel.z, delta * 8.0)
return new_velocity
# Pass effective speeds/accel down
func update_velocity_ground(wish_dir: Vector3, eff_ground_speed: float, eff_accel: float, delta: float):
# Apply more aggressive friction for less sliding
var speed = velocity.length()
if speed > 0.0:
var control = max(STOP_SPEED, speed)
var drop = control * FRICTION * delta
# More aggressive friction application
var new_speed = max(speed - drop, 0.0)
velocity *= new_speed / speed
# Additional stopping help when no input (like original script)
if wish_dir.length() == 0:
var horizontal_vel = Vector3(velocity.x, 0, velocity.z)
var decel_rate = FRICTION * 2.0 * delta # Extra deceleration
velocity.x = move_toward(velocity.x, 0, decel_rate * horizontal_vel.length())
velocity.z = move_toward(velocity.z, 0, decel_rate * horizontal_vel.length())
return accelerate(wish_dir, eff_ground_speed, eff_accel, delta)
func update_velocity_air(wish_dir: Vector3, eff_air_speed: float, eff_accel: float, delta: float):
# Do not apply any friction in the air
return accelerate(wish_dir, eff_air_speed, eff_accel, delta)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment