From 09b25cf9456b0f096c4bbb090f2b693cf8feb442 Mon Sep 17 00:00:00 2001 From: blujai831 Date: Sat, 29 Mar 2025 02:12:43 -0700 Subject: [PATCH] Iterate upon controls. --- characters/base/character.gd | 74 ++++++--- .../player_character_controller.gd | 68 ++------ .../player_character_controller.gd.uid | 2 +- test/blender_test_map.tscn | 18 ++- test/prototype_player_character_controller.gd | 57 +++++++ ...ototype_player_character_controller.gd.uid | 1 + vfx/gameplay_camera.gd | 152 ++++++++++++++++++ vfx/gameplay_camera.gd.uid | 1 + 8 files changed, 288 insertions(+), 85 deletions(-) create mode 100644 test/prototype_player_character_controller.gd create mode 100644 test/prototype_player_character_controller.gd.uid create mode 100644 vfx/gameplay_camera.gd create mode 100644 vfx/gameplay_camera.gd.uid diff --git a/characters/base/character.gd b/characters/base/character.gd index 4137622..3f67fec 100644 --- a/characters/base/character.gd +++ b/characters/base/character.gd @@ -130,6 +130,8 @@ var _freeze_lerp_requested := false var _freeze_lerp_target := Vector3.ZERO var _carrying: Node3D = null +@onready var _shapecast_object := PhysicsShapeQueryParameters3D.new() + #endregion #region Public Properties @@ -165,7 +167,7 @@ var impetus := Vector3.ZERO: if value.is_zero_approx(): impetus = Vector3.ZERO else: - impetus = value.normalized() + impetus = value.limit_length() ## World-space direction the character "wants" / is "trying" to move in. var global_impetus: Vector3: get(): @@ -173,7 +175,7 @@ var global_impetus: Vector3: if gi.is_zero_approx(): return Vector3.ZERO else: - return gi.normalized() + return gi.limit_length() set(value): impetus = (global_position + value)*global_transform ## Whether the character is "trying" to jump. @@ -440,7 +442,7 @@ func _reorient() -> void: match _current_state_properties.yaw_orientation: CharacterStateProperties.OrientationMode.MANUAL: # If manual, do not modify yaw. - target_forward = -basis.z + target_forward = -global_basis.z CharacterStateProperties.OrientationMode.FOLLOW_VELOCITY: # If follow velocity, face velocity direction. if !linear_velocity.is_zero_approx(): @@ -452,14 +454,14 @@ func _reorient() -> void: CharacterStateProperties.OrientationMode.FOLLOW_NORMAL: # If follow normal, face away from the wall if there is one. # Else, mimic manual. - target_forward = wall_normal if _touching_wall else -basis.z + target_forward = wall_normal if _touching_wall else -global_basis.z # Determine target pitch. var target_upward := Vector3.ZERO var old_target_forward := target_forward match _current_state_properties.pitch_orientation: CharacterStateProperties.OrientationMode.MANUAL: # If manual, do not modify pitch. - target_upward = basis.y + target_upward = global_basis.y CharacterStateProperties.OrientationMode.FOLLOW_VELOCITY: # If follow velocity, add influence from velocity to pitch. target_upward = ground_normal @@ -525,29 +527,17 @@ func _do_standard_motion(delta: float) -> void: # Check where we are trying to go. var effective_impetus := _get_effective_impetus() # Check whether in free mode. - var free_mode := ( - _current_state_properties.physics_mode == - CharacterStateProperties.PhysicsMode.FREE - ) # Use air physics iff either in free mode or not grounded (by state). - var use_air_physics := free_mode || !is_grounded() + var use_air_physics := air_physics_active() # Calculate "volitional" velocity # (component of velocity projected into the space # that our impetus is allowed to affect). - var volitional_velocity := linear_velocity - if !free_mode: - volitional_velocity -= ( - volitional_velocity.project(ground_normal) - ) - # Determine which top speed to use. - var top_speed: float = ( - _top_air_speed if use_air_physics else _top_ground_speed - ) + var volitional_velocity := get_volitional_velocity() # Check if we are exceeding top speed. - var too_fast := volitional_velocity.length() > top_speed + var too_fast := going_too_fast() # Regardless, also check how close we are to top speed, 1.0 max. var top_speed_proximity: float = clamp( - volitional_velocity.length()/top_speed, 0.0, 1.0 + get_proportional_speed(), 0.0, 1.0 ) # Use no friction with air physics. # Otherwise, calculate friction according to speed @@ -635,7 +625,7 @@ func _do_standard_motion(delta: float) -> void: func _shapecast(from: Vector3, to: Vector3) -> Dictionary: # prepare from/to sweep var space_state := get_world_3d().direct_space_state - var query := PhysicsShapeQueryParameters3D.new() + var query := _shapecast_object query.collide_with_areas = false query.collide_with_bodies = true query.collision_mask = 0xffffffff @@ -769,7 +759,10 @@ func _apply_jump_deceleration(delta: float) -> void: ## Whether the current state counts as grounded. func is_grounded() -> bool: - return _current_state_properties.counts_as_grounded + return ( + _current_state_properties && + _current_state_properties.counts_as_grounded + ) ## Whether there is a current contact that counts as ground. func is_really_grounded() -> bool: @@ -971,6 +964,41 @@ func pick_up(what: Node3D, force: bool = false) -> void: _carrying = what force_change_state(&'pick-up') +## Whether our current physics mode is free mode. +func is_free_moving() -> bool: + return _current_state_properties && ( + _current_state_properties.physics_mode == + CharacterStateProperties.PhysicsMode.FREE + ) + +## Whether we currently move according to air physics. +## [br][br] +## If false, then we currently move according to ground physics. +func air_physics_active() -> bool: + return is_free_moving() || !is_grounded() + +## Component of linear velocity that can be affected by impetus. +## [br][br] +## If we are free-moving, then this is simply our linear velocity. +## Otherwise, it equals linear velocity projected onto ground plane. +func get_volitional_velocity() -> Vector3: + if is_free_moving(): + return linear_velocity + else: + return linear_velocity - linear_velocity.project(ground_normal) + +## Top air speed if air physics active, else top ground speed. +func get_top_speed() -> float: + return _top_air_speed if air_physics_active() else _top_ground_speed + +## Whether volitional velocity exceeds top speed. +func going_too_fast() -> bool: + return get_volitional_velocity().length() > get_top_speed() + +## Current volitional speed as compared to (divided by) top speed. +func get_proportional_speed() -> float: + return get_volitional_velocity().length()/get_top_speed() + #endregion #region Protected Methods diff --git a/characters/controllers/player_character_controller.gd b/characters/controllers/player_character_controller.gd index 381bf8c..9aaae34 100644 --- a/characters/controllers/player_character_controller.gd +++ b/characters/controllers/player_character_controller.gd @@ -1,57 +1,17 @@ class_name PlayerCharacterController extends Node -@export var camera: Camera3D = null -@export var character: Character = null -@export var camera_sensitivity: float = 2.0 -@export var camera_lerp_speed: float = 4.0 -@export var camera_distance: float = 5.0 -@export var camera_min_distance: float = 2.0 -@export var camera_max_distance: float = 10.0 -@export var camera_pitch: float = -0.5 -@export var camera_min_pitch: float = -1.0 -@export var camera_max_pitch: float = -0.2 +@export var character: Character +@export var camera: Camera3D -@onready var camera_yaw: float = ( - character.global_rotation.y if character else 0.0 -) - -func _physics_process(delta: float) -> void: - if camera && character: - var look_input := Vector3( - Input.get_axis(&'look_left', &'look_right'), - Input.get_axis(&'look_down', &'look_up'), - Input.get_axis(&'zoom_in', &'zoom_out') - ) - camera_distance = clamp( - camera_distance + PI*look_input.z*camera_sensitivity*delta, - camera_min_distance, camera_max_distance - ) - camera_yaw = fposmod( - camera_yaw - look_input.x*camera_sensitivity*delta, - 2.0*PI - ) - camera_pitch = clamp( - camera_pitch + look_input.y*camera_sensitivity*delta, - camera_min_pitch, camera_max_pitch - ) - var bas := Basis.IDENTITY.rotated( - Vector3.RIGHT, camera_pitch - ).rotated( - Vector3.UP, camera_yaw - ) - bas = Basis.looking_at(-bas.z, character.ground_normal) - var posn := character.global_position + camera_distance*bas.z - var lerp_weight: float = 1.0 - 0.5**(camera_lerp_speed*delta) - camera.global_basis = camera.global_basis.slerp(bas, lerp_weight) - camera.global_position = lerp(camera.global_position, posn, lerp_weight) - var move_input := Vector2( - Input.get_axis(&'move_left', &'move_right'), - Input.get_axis(&'move_forward', &'move_backward') - ) - character.global_impetus = ( - move_input.x*camera.basis.x + - move_input.y*camera.basis.z - ) - character.jump_impetus = Input.is_action_pressed(&'jump') - character.action1_impetus = Input.is_action_pressed(&'action1') - character.action2_impetus = Input.is_action_pressed(&'action2') +func _physics_process(_delta: float) -> void: + var move_input := Vector2( + Input.get_axis(&'move_left', &'move_right'), + Input.get_axis(&'move_forward', &'move_backward') + ) + character.global_impetus = ( + move_input.x*camera.global_basis.x + + move_input.y*camera.global_basis.z + ) + character.jump_impetus = Input.is_action_pressed(&'jump') + character.action1_impetus = Input.is_action_pressed(&'action1') + character.action2_impetus = Input.is_action_pressed(&'action2') diff --git a/characters/controllers/player_character_controller.gd.uid b/characters/controllers/player_character_controller.gd.uid index 6a27ddf..de7743a 100644 --- a/characters/controllers/player_character_controller.gd.uid +++ b/characters/controllers/player_character_controller.gd.uid @@ -1 +1 @@ -uid://d26dyumvg37cp +uid://16os114krms3 diff --git a/test/blender_test_map.tscn b/test/blender_test_map.tscn index f107697..4fb2dad 100644 --- a/test/blender_test_map.tscn +++ b/test/blender_test_map.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=16 format=3 uid="uid://6wjqqijnie4p"] +[gd_scene load_steps=17 format=3 uid="uid://6wjqqijnie4p"] +[ext_resource type="Script" uid="uid://cmoaplk7fly6y" path="res://vfx/gameplay_camera.gd" id="1_0cecx"] [ext_resource type="PackedScene" uid="uid://cp3qagrsuarl5" path="res://models/test/blender_test_map.blend" id="1_jy2qr"] [ext_resource type="PackedScene" uid="uid://gis0gxap8i8t" path="res://characters/test_stick.tscn" id="2_hmvb4"] -[ext_resource type="Script" uid="uid://d26dyumvg37cp" path="res://characters/controllers/player_character_controller.gd" id="3_0cecx"] +[ext_resource type="Script" uid="uid://16os114krms3" path="res://characters/controllers/player_character_controller.gd" id="3_0cecx"] [sub_resource type="Gradient" id="Gradient_hmvb4"] offsets = PackedFloat32Array(0, 0.327869, 0.663934, 1) @@ -63,8 +64,11 @@ data = PackedVector3Array(10.1543, 0, -43.3539, 50, 0, -50, 14.3313, 0, -39.177, [node name="Node3D" type="Node3D"] -[node name="Camera3D" type="Camera3D" parent="."] +[node name="Camera3D" type="Camera3D" parent="." node_paths=PackedStringArray("target")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -10.4229, 13.1742, -7.10402) +script = ExtResource("1_0cecx") +target = NodePath("../TestStick") +player_control = true [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment_5t8ro") @@ -83,12 +87,12 @@ surface_material_override/0 = SubResource("StandardMaterial3D_fdbvi") shape = SubResource("ConcavePolygonShape3D_jy2qr") [node name="TestStick" parent="." instance=ExtResource("2_hmvb4")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -9.54375, 17.2871, -13.5584) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12.7073, 17.2871, 0.809647) -[node name="PlayerCharacterController" type="Node" parent="TestStick" node_paths=PackedStringArray("camera", "character")] +[node name="PlayerCharacterController" type="Node" parent="TestStick" node_paths=PackedStringArray("character", "camera")] script = ExtResource("3_0cecx") -camera = NodePath("../../Camera3D") character = NodePath("..") -metadata/_custom_type_script = "uid://d26dyumvg37cp" +camera = NodePath("../../Camera3D") +metadata/_custom_type_script = "uid://16os114krms3" [editable path="StaticBody3D/blender_test_map"] diff --git a/test/prototype_player_character_controller.gd b/test/prototype_player_character_controller.gd new file mode 100644 index 0000000..6fb4f54 --- /dev/null +++ b/test/prototype_player_character_controller.gd @@ -0,0 +1,57 @@ +class_name PrototypePlayerCharacterController extends Node + +@export var camera: Camera3D = null +@export var character: Character = null +@export var camera_sensitivity: float = 2.0 +@export var camera_lerp_speed: float = 4.0 +@export var camera_distance: float = 5.0 +@export var camera_min_distance: float = 2.0 +@export var camera_max_distance: float = 10.0 +@export var camera_pitch: float = -0.5 +@export var camera_min_pitch: float = -1.0 +@export var camera_max_pitch: float = -0.2 + +@onready var camera_yaw: float = ( + character.global_rotation.y if character else 0.0 +) + +func _physics_process(delta: float) -> void: + if camera && character: + var look_input := Vector3( + Input.get_axis(&'look_left', &'look_right'), + Input.get_axis(&'look_down', &'look_up'), + Input.get_axis(&'zoom_in', &'zoom_out') + ) + camera_distance = clamp( + camera_distance + PI*look_input.z*camera_sensitivity*delta, + camera_min_distance, camera_max_distance + ) + camera_yaw = fposmod( + camera_yaw - look_input.x*camera_sensitivity*delta, + 2.0*PI + ) + camera_pitch = clamp( + camera_pitch + look_input.y*camera_sensitivity*delta, + camera_min_pitch, camera_max_pitch + ) + var bas := Basis.IDENTITY.rotated( + Vector3.RIGHT, camera_pitch + ).rotated( + Vector3.UP, camera_yaw + ) + bas = Basis.looking_at(-bas.z, character.ground_normal) + var posn := character.global_position + camera_distance*bas.z + var lerp_weight: float = 1.0 - 0.5**(camera_lerp_speed*delta) + camera.global_basis = camera.global_basis.slerp(bas, lerp_weight) + camera.global_position = lerp(camera.global_position, posn, lerp_weight) + var move_input := Vector2( + Input.get_axis(&'move_left', &'move_right'), + Input.get_axis(&'move_forward', &'move_backward') + ) + character.global_impetus = ( + move_input.x*camera.global_basis.x + + move_input.y*camera.global_basis.z + ) + character.jump_impetus = Input.is_action_pressed(&'jump') + character.action1_impetus = Input.is_action_pressed(&'action1') + character.action2_impetus = Input.is_action_pressed(&'action2') diff --git a/test/prototype_player_character_controller.gd.uid b/test/prototype_player_character_controller.gd.uid new file mode 100644 index 0000000..6a27ddf --- /dev/null +++ b/test/prototype_player_character_controller.gd.uid @@ -0,0 +1 @@ +uid://d26dyumvg37cp diff --git a/vfx/gameplay_camera.gd b/vfx/gameplay_camera.gd new file mode 100644 index 0000000..0488f76 --- /dev/null +++ b/vfx/gameplay_camera.gd @@ -0,0 +1,152 @@ +class_name GameplayCamera extends Camera3D + +@export var target: Node3D +@export var target_distance: float = 5.0 +@export var target_min_distance: float = 2.0 +@export var target_max_distance: float = 10.0 +@export var target_decentering_factor: float = 0.4 +@export var follow_lerp_speed: float = 0.999 +@export var turn_lerp_speed: float = 0.999 +@export var fov_lerp_speed: float = 0.999 +@export var clip_margin: float = 0.5 +@export var gimbal_margin: float = 0.75 +@export var player_control := false +@export var look_speed: float = 1.0 +@export var look_lerp_speed: float = 0.99 +@export var zoom_speed: float = 4.0 +@export var mouse_sensitivity: float = 0.03125 +@export var mouse_wheel_sensitivity: float = 5.0 +@export var analog_sensitivity: float = 1.0 +@export var analog_zoom_sensitivity: float = 1.0 + +var target_direction := Vector3.ZERO +var look_impetus := Vector3.ZERO +var look_velocity := Vector3.ZERO + +@onready var _raycast_object := PhysicsRayQueryParameters3D.new() + +func _ready() -> void: + snap_to_target() + +func _update_mouse_mode() -> void: + if player_control: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + else: + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + +func _unhandled_input(event: InputEvent) -> void: + if !player_control: return + var move_event := event as InputEventMouseMotion + var button_event := event as InputEventMouseButton + if move_event: + look_impetus.x -= move_event.relative.x*mouse_sensitivity + look_impetus.y += move_event.relative.y*mouse_sensitivity + elif button_event: + match button_event.button_index: + MOUSE_BUTTON_WHEEL_UP: + look_impetus.z -= mouse_wheel_sensitivity + MOUSE_BUTTON_WHEEL_DOWN: + look_impetus.z += mouse_wheel_sensitivity + +func _handle_analog_input() -> void: + if !player_control: return + look_impetus.x -= Input.get_axis(&'look_left', &'look_right') + look_impetus.y += Input.get_axis(&'look_down', &'look_up') + look_impetus.z += Input.get_axis(&'zoom_in', &'zoom_out') + +func _respond_to_impetus(delta: float) -> void: + var gck: float = _gimbal_check() + if ( + (look_impetus.y > 0.0 and gck < -gimbal_margin) || + (look_impetus.y < 0.0 and gck > gimbal_margin) + ): + look_impetus.y = 0.0 + var desired_look_velocity := Vector3( + look_impetus.x*look_speed*target_distance, + look_impetus.y*look_speed*target_distance, + look_impetus.z*zoom_speed + ) + look_impetus = look_impetus.limit_length() + look_velocity = lerp(look_velocity, desired_look_velocity, look_lerp_speed) + _try_go( + global_position + + global_basis.x*look_velocity.x*delta + + global_basis.y*look_velocity.y*delta + ) + target_distance = clamp( + target_distance + look_velocity.z*delta, + target_min_distance, target_max_distance + ) + look_impetus = Vector3.ZERO + +func _try_go(where: Vector3) -> void: + _raycast_object.from = global_position + _raycast_object.to = where + ( + (where - global_position).normalized()*clip_margin + ) + var raycast_results := ( + get_world_3d().direct_space_state.intersect_ray(_raycast_object) + ) + if raycast_results.is_empty(): + global_position = where + else: + global_position = raycast_results.position + ( + raycast_results.normal*clip_margin + ) + +func snap_to_target() -> void: + if !target: return + target_direction = ( + -target.global_basis.z - target.global_basis.y/2.0 + ).normalized() + _follow_target(0.0, true) + +func _gimbal_check() -> float: + return target_direction.dot(target.global_basis.y) + +func _follow_target( + delta: float, + snap: bool = false +) -> void: + if !target: return + var velocity := Vector3.ZERO + var top_speed: float = 30.0 + if target is RigidBody3D: + velocity = (target as RigidBody3D).linear_velocity + if target is Character: + top_speed = (target as Character).get_top_speed() + var decentering: float = target_distance*target_decentering_factor + var target_position := ( + target.global_position + decentering*target.global_basis.y + ) + var target_offset := target_position - global_position + if !snap: + target_direction = target_offset.normalized() + var desired_position := target_position - target_distance*target_direction + _try_go(desired_position if snap else lerp( + global_position, desired_position, + 1.0 - (1.0 - follow_lerp_speed)**delta + )) + if abs(_gimbal_check()) <= lerp(gimbal_margin, 1.0, 0.5): + var desired_basis := Basis.looking_at( + target_offset, + target.global_basis.y + ) + global_basis = desired_basis if snap else global_basis.slerp( + desired_basis, + 1.0 - (1.0 - turn_lerp_speed)**delta + ) + var desired_fov: float = min( + 75.0 + 15.0*velocity.project(global_basis.z).length()/top_speed, + 179.0 + ) + fov = desired_fov if snap else lerp( + fov, desired_fov, + 1.0 - (1.0 - fov_lerp_speed)**delta + ) + +func _physics_process(delta: float) -> void: + _update_mouse_mode() + _handle_analog_input() + _respond_to_impetus(delta) + _follow_target(delta) diff --git a/vfx/gameplay_camera.gd.uid b/vfx/gameplay_camera.gd.uid new file mode 100644 index 0000000..4d62b85 --- /dev/null +++ b/vfx/gameplay_camera.gd.uid @@ -0,0 +1 @@ +uid://cmoaplk7fly6y