mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2025-12-16 13:30:07 +01:00
203 lines
7.0 KiB
GDScript
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)
|