Files
2025-10-11 05:03:59 -07:00

203 lines
7.0 KiB
GDScript

extends CharacterBody3D
const MOUSE_SENSITIVITY = 2.5
const CAMERA_SMOOTH_SPEED = 10.0
const MOVE_SPEED = 3.0
const FRICTION = 10.0
const JUMP_VELOCITY = 8.0
const BULLET_SPEED = 9.0
const Bullet = preload("res://bullet.tscn")
# Define our FPS and TPS player views using Euler angles.
var _yaw: float = 0.0
var _pitch: float = 0.0
# XZ direction the player is looking at.
var _dir := Vector3(sin(_yaw), 0, cos(_yaw))
# TPS camera.
var _tps_camera_proximity: float = 3.0
var _tps_camera_look_from := Vector3()
enum CameraType {
CAM_FIXED, ## Fixed camera perspective.
CAM_FPS, ## First-person perspective.
CAM_TPS, ## Third-person perspective.
}
# Current camera type.
# (Note that we toggle this in `_ready()`, so it actually starts with FPS camera.)
var _cam_type := CameraType.CAM_FIXED
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
func _ready() -> void:
# Capture the mouse (stops the mouse cursor from showing and ensures it stays within the window).
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# We define the TPS camera in global space by setting it
# as `top_level` so it ignores the parent transform.
$Rig/Camera_TPS.top_level = true
# Perform the logic to create FPS view to start with.
cycle_camera_type()
func _input(input_event: InputEvent) -> void:
if input_event is InputEventMouseMotion:
_yaw -= input_event.screen_relative.x * MOUSE_SENSITIVITY * 0.001
_pitch += input_event.screen_relative.y * MOUSE_SENSITIVITY * 0.002
_pitch = clamp(_pitch, -PI, PI)
$Rig.rotation = Vector3(0, _yaw, 0)
func _update_camera(delta: float) -> void:
# Keep the player direction up-to-date based on the yaw.
_dir.x = sin(_yaw)
_dir.z = cos(_yaw)
# Rotate the head (and FPS camera and firing origin) with the
# pitch from the mouse.
$Rig/Head.rotation = Vector3(_pitch * -0.5, 0, 0)
match _cam_type:
CameraType.CAM_TPS:
# We will focus the TPS camera on the head of the player.
var target: Vector3 = $Rig/Head.get_global_transform_interpolated().origin
# Calculate a position to look at the player from.
var pos := target
# The camera should be behind the player, so offset the camera relative to direction.
pos.x += _dir.x * _tps_camera_proximity
pos.z += _dir.z * _tps_camera_proximity
# Move the TPS camera up and down depending on the pitch.
# There's no special formula here, just something that looks okay.
pos.y += 2.0 + _pitch * _tps_camera_proximity * 0.2
# Offset from the old `_tps_camera_look_from` to the new position
# we want the TPS camera to move to.
var offset: Vector3 = pos - _tps_camera_look_from
var l: float = offset.length()
# We cap how far we allow the TPS camera to move on each update,
# so we get a smooth movement rather than snapping.
var tps_cam_speed: float = CAMERA_SMOOTH_SPEED * delta
# If we are trying to move further than the maximum allowed,
# we resize the offset to `tps_cam_speed`.
if l > tps_cam_speed:
offset *= tps_cam_speed / l
# Move the TPS camera.
_tps_camera_look_from += offset
# `look_at_from_position()` does all the magic for us.
$Rig/Camera_TPS.look_at_from_position(_tps_camera_look_from, target, Vector3(0, 1, 0))
# For a real TPS camera, some other things to try:
# - Ray cast from the player towards the camera to prevent it looking through walls.
# The SpringArm3D node can be useful here.
# - Try smoothing the camera by yaw/pitch from the player rather than offset.
func cycle_camera_type() -> void:
match _cam_type:
CameraType.CAM_FIXED:
_cam_type = CameraType.CAM_FPS
$Rig/Head/Camera_FPS.make_current()
CameraType.CAM_FPS:
_cam_type = CameraType.CAM_TPS
$Rig/Camera_TPS.make_current()
CameraType.CAM_TPS:
_cam_type = CameraType.CAM_FIXED
get_node(^"../Camera_Fixed").make_current()
# Hide body in FPS view (but keep shadow casting to improve spatial awareness).
if _cam_type == CameraType.CAM_FPS:
$Rig/Mesh_Body.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
else:
$Rig/Mesh_Body.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
func _process(delta: float) -> void:
if Input.is_action_just_pressed(&"cycle_camera_type"):
cycle_camera_type()
if Input.is_action_just_pressed(&"toggle_physics_interpolation"):
get_tree().physics_interpolation = not get_tree().physics_interpolation
if Input.is_action_just_pressed(&"fire"):
var bullet: RigidBody3D = Bullet.instantiate()
# Figure out where we want the bullet to spawn.
# We use a dummy Node offset from the head, but you may want to use e.g.
# a BoneAttachment3D, or dummy node on a weapon.
var transform_3d: Transform3D = $Rig/Head/Fire_Origin.get_global_transform_interpolated()
bullet.position = transform_3d.origin
# We can calculate the direction the bullet should travel from the basis (rotation)
# of the dummy Node.
var bul_dir: Vector3 = transform_3d.basis[2].normalized()
# Give our physics bullet some starting velocity.
bullet.linear_velocity = bul_dir * -BULLET_SPEED
get_parent().add_child(bullet)
# A moving start for a bullet using physics interpolation can be done
# by resetting, *then* offsetting the position in the direction of travel.
# This means that on the first tick the bullet will be moving rather than
# standing still, as standing still on the first tick can look unnatural.
bullet.reset_physics_interpolation()
bullet.position -= bul_dir * (1.0 - Engine.get_physics_interpolation_fraction())
# If we pressed reset, or went too far from the origin, move back to the origin.
if Input.is_action_just_pressed(&"reset_position") or position.length() > 10.0:
position = Vector3(0, 1, 0)
velocity = Vector3()
reset_physics_interpolation()
_yaw = 0.0
_pitch = 0.0
$Rig.rotation = Vector3(0, _yaw, 0)
if Input.is_action_just_pressed(&"jump") and is_on_floor():
velocity.y += JUMP_VELOCITY
# We update our camera every frame.
# Our camera is not physics interpolated, as we want fast response from the mouse.
# However in the case of first-person and third-person views, the position is indirectly
# inherited from physics-interpolated player, so we get nice smooth motion while still
# having quick mouse response.
_update_camera(delta)
# When physics interpolation is active on the node,
# you should move it on the physics tick (physics_process)
# rather than on the frame (process).
func _physics_process(delta: float) -> void:
var move := Vector3()
# Calculate movement relative to the player's coordinate system.
var input: Vector2 = Input.get_vector(&"move_left", &"move_right", &"move_forward", &"move_backward") * MOVE_SPEED
move.x = input.x
move.z = input.y
# Apply gravity.
move.y -= gravity * delta
# Apply mouse rotation to the move, so that it is now in global space.
move = move.rotated(Vector3(0, 1, 0), _yaw)
# Apply the global space move to the physics.
velocity += move
move_and_slide()
# Apply friction to horizontal motion in a tick rate-independent manner.
var friction_delta := exp(-FRICTION * delta)
velocity = Vector3(velocity.x * friction_delta, velocity.y, velocity.z * friction_delta)