From 544f434d9eacafdd0b471fc97f4c5445cc32dd30 Mon Sep 17 00:00:00 2001 From: ninetailsrabbit Date: Tue, 31 Dec 2024 14:37:48 +0000 Subject: [PATCH] minor fixes and improved the first person controller with new mechanics --- autoload/general/game_globals.gd | 1 + autoload/general/global_day_night_clock.gd | 81 ++++++++++++++++--- .../persistence/settings/input_controls.gd | 4 + .../3D/interactables/interactable_3d.gd | 2 +- .../3D/interactors/raycast_interactor_3d.gd | 2 +- .../3D/ui/interactable_information.gd | 9 ++- components/motion/3D/boat/boat_3d.gd | 57 +++++++++---- .../controller/first_person_controller.gd | 23 ++++-- .../controller/first_person_controller.tscn | 16 +++- .../mechanics/camera_controller_3d.gd | 6 +- .../controller/states/ground/ground.gd | 1 - .../controller/states/special/ladder_climb.gd | 73 +++++++++++++++++ .../states/special/special_state.gd | 34 ++++++++ .../controller/states/special/swim.gd | 44 ++++++++-- .../any_to_ladder_climb_transition.gd.gd | 8 ++ components/motion/3D/ladder/ladder_3d.gd | 16 ++++ components/motion/3D/ocean/ocean.gd | 65 +++++++++++++++ project.godot | 5 ++ 18 files changed, 395 insertions(+), 52 deletions(-) create mode 100644 components/motion/3D/first-person/controller/states/special/ladder_climb.gd create mode 100644 components/motion/3D/first-person/controller/states/transitions/any_to_ladder_climb_transition.gd.gd create mode 100644 components/motion/3D/ladder/ladder_3d.gd create mode 100644 components/motion/3D/ocean/ocean.gd diff --git a/autoload/general/game_globals.gd b/autoload/general/game_globals.gd index dce844c..ae37592 100644 --- a/autoload/general/game_globals.gd +++ b/autoload/general/game_globals.gd @@ -11,6 +11,7 @@ const interactables_collision_layer: int = 32 const grabbables_collision_layer: int = 64 const bullets_collision_layer: int = 128 const playing_cards_collision_layer: int = 256 +const ladders_collision_layer: int = 512 #endregion diff --git a/autoload/general/global_day_night_clock.gd b/autoload/general/global_day_night_clock.gd index d55d6e0..5f2a78a 100644 --- a/autoload/general/global_day_night_clock.gd +++ b/autoload/general/global_day_night_clock.gd @@ -2,11 +2,15 @@ extends Node signal time_tick(day: int, hour: int, minute: int) +signal hour_passed +signal day_passed const MinutesPerDay: int = 1440 const MinutesPerHour: int = 60 +const DayHourLength: int = MinutesPerDay / MinutesPerHour const InGameToRealMinuteDuration := TAU / MinutesPerDay +@export var emit_tick_signal: bool = false ## This value when it's 1.0 means that one minute in real time translates into one second in-game, so modify this value as is needed @export var in_game_speed: float = 1.0 @export var initial_day: int = 0 @@ -18,27 +22,44 @@ const InGameToRealMinuteDuration := TAU / MinutesPerDay initial_hour = clampi(hour, 0, 23) time = InGameToRealMinuteDuration * MinutesPerHour * initial_hour +var is_running: bool = false var time: float = 0.0 var past_minute: int = -1 ## Get the current value time to use on gradient color curves and change the environment tempererature according to the day time -var curve_value: float = 0.0 - +var curve_value: float = 0.0: + set(value): + if value != curve_value: + curve_value = clampf(value, 0.0, 1.0) var current_period: String = "AM" -var current_day: int = 0 -var current_hour: int = 0 -var current_minute: int = 0 - +var current_day: int = 0: + set(value): + if value != current_day: + current_day = maxi(0, value) + day_passed.emit() +var current_hour: int = 0: + set(value): + if value != current_hour: + current_hour = value + current_hour = clampi(value, 0, 23) + hour_passed.emit() +var current_minute: int = 0: + set(value): + if value != current_minute: + current_minute = value + current_minute = clampi(value, 0, 59) func _ready() -> void: set_process(false) - + func _process(delta: float) -> void: time += delta * InGameToRealMinuteDuration * in_game_speed + _recalculate_time() - curve_value = (sin(time - PI / 2.0) + 1.0) / 2.0 + curve_value = get_curve_value() - _recalculate_time() + if curve_value >= 1.0: + curve_value = 0.0 func start(day: int = initial_day, hour: int = initial_hour, minute: int = initial_minute) -> void: @@ -53,13 +74,46 @@ func start(day: int = initial_day, hour: int = initial_hour, minute: int = initi time = InGameToRealMinuteDuration * MinutesPerHour * current_hour + is_running = true set_process(true) func stop() -> void: + is_running = false set_process(false) +func total_seconds() -> int: + if current_day > 0: + current_day * seconds() + + return seconds() + + +func seconds(hour: int = current_hour, minute: int = current_minute) -> int: + return (hour * MinutesPerHour * MinutesPerHour) + minute * MinutesPerHour + + +func get_curve_value(hour: int = current_hour, minute: int = current_minute) -> float: + var current_time_fraction = (seconds(hour, minute) * 1000) / (DayHourLength * MinutesPerHour * MinutesPerHour * 1000.0) + + + return clampf(current_time_fraction, 0.0, 1.0) + + +func time_display() -> String: + var hour: int = current_hour + var minute: int = current_minute + + if hour < 10: + "0" + str(hour) + + if minute < 10: + "0" + str(minute) + + return "%s:%s" % [hour, minute] + + func change_day_to(new_day: int) -> void: initial_day = new_day @@ -73,16 +127,17 @@ func change_minute_to(new_minute: int) -> void: func _recalculate_time() -> void: - var total_minutes = int(time / InGameToRealMinuteDuration) - var current_day_minutes = fmod(total_minutes, MinutesPerDay) + var total_minutes = int(time / InGameToRealMinuteDuration) + initial_minute + var current_day_minutes = fmod(total_minutes, MinutesPerDay) + initial_minute current_day = initial_day + int(total_minutes / MinutesPerDay) current_hour = int(current_day_minutes / MinutesPerHour) - current_minute = initial_minute + int(fmod(current_day_minutes, MinutesPerHour)) + current_minute = int(fmod(current_day_minutes, MinutesPerHour)) if past_minute != current_minute: past_minute = current_minute current_period = "AM" if current_hour < 12 else "PM" - time_tick.emit(current_day, current_hour, current_minute) + if emit_tick_signal: + time_tick.emit(current_day, current_hour, current_minute) diff --git a/autoload/persistence/settings/input_controls.gd b/autoload/persistence/settings/input_controls.gd index 679b210..e50e661 100644 --- a/autoload/persistence/settings/input_controls.gd +++ b/autoload/persistence/settings/input_controls.gd @@ -33,6 +33,8 @@ const ToggleInventory: StringName = &"toggle_inventory" const Drag: StringName = &"drag" +const ClimbLadder: StringName = &"climb_ladder" + const VehicleAccelerate: StringName = &"vehicle_accelerate" const VehicleReverseAccelerate: StringName = &"vehicle_reverse_accelerate" const VehicleSteerRight: StringName = &"vehicle_steer_right" @@ -40,5 +42,7 @@ const VehicleSteerLeft: StringName = &"vehicle_steer_left" const VehicleHandBrake: StringName = &"vehicle_hand_brake" const StartVehicleEngine: StringName = &"start_vehicle_engine" +const StopDrivingBoat: StringName = &"stop_driving_boat" + const PerformanceMetrics: StringName = &"performance_metrics" const PauseGame: StringName = &"pause" diff --git a/components/interaction/3D/interactables/interactable_3d.gd b/components/interaction/3D/interactables/interactable_3d.gd index 519ecab..13bf565 100644 --- a/components/interaction/3D/interactables/interactable_3d.gd +++ b/components/interaction/3D/interactables/interactable_3d.gd @@ -167,4 +167,4 @@ func on_canceled_interaction() -> void: GlobalGameEvents.interactable_canceled_interaction.emit(self) -#endregion \ No newline at end of file +#endregion diff --git a/components/interaction/3D/interactors/raycast_interactor_3d.gd b/components/interaction/3D/interactors/raycast_interactor_3d.gd index b924da3..e5d63ec 100644 --- a/components/interaction/3D/interactors/raycast_interactor_3d.gd +++ b/components/interaction/3D/interactors/raycast_interactor_3d.gd @@ -69,4 +69,4 @@ func unfocus(interactable: Interactable3D = current_interactable): interacting = false enabled = true - interactable.unfocused.emit() \ No newline at end of file + interactable.unfocused.emit() diff --git a/components/interaction/3D/ui/interactable_information.gd b/components/interaction/3D/ui/interactable_information.gd index c60649b..51b85fd 100644 --- a/components/interaction/3D/ui/interactable_information.gd +++ b/components/interaction/3D/ui/interactable_information.gd @@ -9,15 +9,16 @@ var current_interactable: Interactable3D func _enter_tree() -> void: mouse_filter = MouseFilter.MOUSE_FILTER_IGNORE - GlobalGameEvents.interactable_focused.connect(on_interactable_focused) - GlobalGameEvents.interactable_unfocused.connect(on_interactable_unfocused) - GlobalGameEvents.interactable_canceled_interaction.connect(on_interactable_unfocused) - func _ready() -> void: information_label.text = "" information_label.hide() + GlobalGameEvents.interactable_focused.connect(on_interactable_focused) + GlobalGameEvents.interactable_unfocused.connect(on_interactable_unfocused) + GlobalGameEvents.interactable_canceled_interaction.connect(on_interactable_unfocused) + GlobalGameEvents.interactable_interacted.connect(on_interactable_unfocused) + func on_interactable_focused(interactable: Interactable3D) -> void: current_interactable = interactable diff --git a/components/motion/3D/boat/boat_3d.gd b/components/motion/3D/boat/boat_3d.gd index 58d8513..018bd85 100644 --- a/components/motion/3D/boat/boat_3d.gd +++ b/components/motion/3D/boat/boat_3d.gd @@ -1,10 +1,12 @@ ## Recommended to set linear damp to 1 and angular damp to 2 class_name Boat3D extends RigidBody3D +const GroupName: StringName = &"boats" + signal started_engine signal stopped_engine -@export var boat_mesh: MeshInstance3D +@export var boat_mesh: Node3D @export var water_level: float = 0.0 @export_category("Engine") @export var boat_engine_force: float = 20.0 @@ -26,11 +28,13 @@ signal stopped_engine @export var boat_rudder: Node3D @export var boat_rudder_maximum_rotation: Vector3 = Vector3.ZERO @export var boat_rudder_idle_rotation: Vector3 = Vector3.ZERO -@export var boat_rudder_lerp_factor: float = 15.0 +@export var boat_rudder_lerp_factor: float = 5.0 +@export var boat_rudder_idle_lerp_factor: float = 2.0 +var motion_input: TransformedInput = TransformedInput.new(self) var current_engine_force: float = 0.0 var buoyancy_spots: Array[Node3D] = [] - +var is_being_driven: bool = false var engine_on: bool = false: set(value): if value != engine_on: @@ -43,13 +47,41 @@ var engine_on: bool = false: stopped_engine.emit() +func _enter_tree() -> void: + add_to_group(GroupName) + + func _ready() -> void: if buoyancy_root: buoyancy_spots.assign(buoyancy_root.get_children()) +func start_engine() -> void: + engine_on = true + + +func stop_engine() -> void: + engine_on = false + + +func drive() -> void: + is_being_driven = true + + +func stop_drive() -> void: + is_being_driven = false + + func _physics_process(delta: float) -> void: - if engine_on: + if engine_on and is_being_driven: + if boat_rudder: + motion_input.update() + var steering_input: float = -motion_input.input_direction_horizontal_axis + + if is_zero_approx(steering_input): + boat_rudder.rotation_degrees = boat_rudder.rotation_degrees.lerp(boat_rudder_idle_rotation, delta * boat_rudder_idle_lerp_factor) + else: + boat_rudder.rotation_degrees = boat_rudder.rotation_degrees.lerp(sign(steering_input) * boat_rudder_maximum_rotation, delta * boat_rudder_lerp_factor) if Input.is_action_pressed(InputControls.VehicleAccelerate): if boat_acceleration > 0: @@ -58,13 +90,7 @@ func _physics_process(delta: float) -> void: current_engine_force = boat_engine_force apply_central_force(global_transform.basis * (Vector3.FORWARD * current_engine_force) ) - - if Input.is_action_pressed(InputControls.VehicleSteerLeft): - apply_torque(Vector3.UP * boat_steering_force) - - if Input.is_action_pressed(InputControls.VehicleSteerRight): - apply_torque(Vector3.DOWN * boat_steering_force) - + elif Input.is_action_pressed(InputControls.VehicleReverseAccelerate): if boat_acceleration > 0: current_engine_force = lerp(current_engine_force, boat_reverse_engine_force, boat_reverse_acceleration * delta) @@ -73,13 +99,16 @@ func _physics_process(delta: float) -> void: apply_central_force(global_transform.basis * (Vector3.BACK * current_engine_force) ) + if not is_zero_approx(current_engine_force): if Input.is_action_pressed(InputControls.VehicleSteerLeft): apply_torque(Vector3.UP * boat_steering_force) if Input.is_action_pressed(InputControls.VehicleSteerRight): apply_torque(Vector3.DOWN * boat_steering_force) - - + else: + if boat_rudder: + boat_rudder.rotation_degrees = boat_rudder.rotation_degrees.lerp(boat_rudder_idle_rotation, delta * boat_rudder_idle_lerp_factor) + for buoyancy_spot: Node3D in buoyancy_spots: if buoyancy_spot.global_position.y <= water_level: - apply_force(Vector3.UP * randf_range(buoyancy_force - buoyancy_force_variation, buoyancy_force) * -buoyancy_spot.global_position, buoyancy_spot.global_position - global_position) + apply_force(mass * Vector3.UP * randf_range(buoyancy_force - buoyancy_force_variation, buoyancy_force) * -buoyancy_spot.global_position, buoyancy_spot.global_position - global_position) diff --git a/components/motion/3D/first-person/controller/first_person_controller.gd b/components/motion/3D/first-person/controller/first_person_controller.gd index 15c98b5..eb7710d 100644 --- a/components/motion/3D/first-person/controller/first_person_controller.gd +++ b/components/motion/3D/first-person/controller/first_person_controller.gd @@ -1,6 +1,8 @@ @icon("res://components/motion/3D/first-person/controller/first_person_controller.svg") class_name FirstPersonController extends CharacterBody3D +const GroupName: StringName = &"player" + @export var mouse_mode_switch_input_actions: Array[String] = ["ui_cancel"] @export_group("Camera FOV") @export var dinamic_camera_fov: bool = true @@ -30,6 +32,7 @@ class_name FirstPersonController extends CharacterBody3D if value != stairs: stairs = value _update_wall_checkers() +@export var ladder_climb: bool = false @onready var debug_ui: CanvasLayer = $DebugUI @onready var finite_state_machine: FiniteStateMachine = $FiniteStateMachine @onready var camera: CameraShake3D = $CameraController/Head/CameraShake3D @@ -45,6 +48,8 @@ class_name FirstPersonController extends CharacterBody3D @onready var left_wall_checker_2: RayCast3D = %LeftWallChecker2 @onready var ceil_shape_cast: ShapeCast3D = $CeilShapeCast +@onready var ladder_cast_detector: ShapeCast3D = $LadderCastDetector + @onready var footsteps_manager_3d: FootstepsManager3D = $FootstepsManager3D @onready var animation_player: AnimationPlayer = $AnimationPlayer @@ -55,7 +60,6 @@ class_name FirstPersonController extends CharacterBody3D @onready var original_camera_fov = camera.fov @onready var fire_arm_weapon_holder: FireArmWeaponHolder = $CameraController/Head/CameraShake3D/FireArmWeaponHolder -const group_name = "player" var was_grounded: bool = false var is_grounded: bool = false @@ -66,10 +70,10 @@ var last_direction: Vector3 = Vector3.ZERO func _unhandled_key_input(_event: InputEvent) -> void: if InputHelper.is_any_action_just_pressed(mouse_mode_switch_input_actions): switch_mouse_capture_mode() - + func _enter_tree() -> void: - add_to_group(group_name) + add_to_group(GroupName) func _ready() -> void: @@ -86,7 +90,8 @@ func _ready() -> void: RunToWalkTransition.new(), AnyToWallRunTransition.new(), WallRunToFallTransition.new(), - WallRunToWallJumpTransition.new() + WallRunToWallJumpTransition.new(), + AnyToLadderClimbTransition.new() ]) finite_state_machine.state_changed.connect(on_state_changed) @@ -150,11 +155,17 @@ func get_current_wall_side() -> Vector3: func lock_movement() -> void: finite_state_machine.lock_state_machine() - camera_controller.lock() func unlock_movement() -> void: finite_state_machine.unlock_state_machine() + + +func lock_camera() -> void: + camera_controller.lock() + + +func unlock_camera() -> void: camera_controller.unlock() @@ -216,7 +227,7 @@ func on_state_changed(_from: MachineState, to: MachineState) -> void: #if interactable.lock_player_on_interact: #lock_movement() # -# + #func on_interactable_canceled_interaction(_interactable: Interactable3D) -> void: #unlock_movement() #camera.make_current() diff --git a/components/motion/3D/first-person/controller/first_person_controller.tscn b/components/motion/3D/first-person/controller/first_person_controller.tscn index 74079ea..56ca172 100644 --- a/components/motion/3D/first-person/controller/first_person_controller.tscn +++ b/components/motion/3D/first-person/controller/first_person_controller.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=37 format=3 uid="uid://bx3bh475g3jjf"] +[gd_scene load_steps=39 format=3 uid="uid://bx3bh475g3jjf"] [ext_resource type="Script" path="res://components/motion/3D/first-person/controller/first_person_controller.gd" id="1_v7v7g"] [ext_resource type="PackedScene" uid="uid://bcj2w63oj13e5" path="res://components/motion/3D/first-person/controller/debug_ui/first_person_debug_ui.tscn" id="2_ml2dd"] @@ -17,6 +17,7 @@ [ext_resource type="Script" path="res://components/motion/3D/first-person/controller/states/air/wall_run.gd" id="14_2jtjm"] [ext_resource type="Script" path="res://components/cards/camera/3D/shake/camera_shake_3d.gd" id="14_smxor"] [ext_resource type="Script" path="res://components/motion/3D/first-person/shooter/weapons/firearm_weapon_holder.gd" id="15_cjftc"] +[ext_resource type="Script" path="res://components/motion/3D/first-person/controller/states/special/ladder_climb.gd" id="15_puccb"] [ext_resource type="Script" path="res://components/motion/3D/first-person/shooter/weapons/motion/weapon_sway.gd" id="16_2a3la"] [ext_resource type="Script" path="res://components/motion/3D/first-person/shooter/weapons/motion/weapon_noise.gd" id="17_vpodh"] [ext_resource type="Script" path="res://components/motion/3D/first-person/shooter/weapons/motion/weapon_tilt.gd" id="18_2tnx5"] @@ -126,6 +127,9 @@ height = 1.0 [sub_resource type="SphereShape3D" id="SphereShape3D_plvkn"] +[sub_resource type="BoxShape3D" id="BoxShape3D_wwv7u"] +size = Vector3(0.3, 0.3, 0.5) + [sub_resource type="FastNoiseLite" id="FastNoiseLite_dfk1p"] noise_type = 3 seed = 50 @@ -228,6 +232,10 @@ script = ExtResource("14_2jtjm") actor = NodePath("../../..") gravity_force = 0.6 +[node name="LadderClimb" type="Node" parent="FiniteStateMachine/Special" node_paths=PackedStringArray("actor")] +script = ExtResource("15_puccb") +actor = NodePath("../../..") + [node name="StandCollisionShape" type="CollisionShape3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) shape = SubResource("CapsuleShape3D_1od8w") @@ -246,6 +254,12 @@ shape = SubResource("SphereShape3D_plvkn") target_position = Vector3(0, 0.65, 0) debug_shape_custom_color = Color(0, 0.854902, 0, 1) +[node name="LadderCastDetector" type="ShapeCast3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.29395, -0.535157) +shape = SubResource("BoxShape3D_wwv7u") +target_position = Vector3(0, 0, 0) +debug_shape_custom_color = Color(0.652263, 0.380637, 0.944592, 1) + [node name="WallCheckers" type="Node3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) diff --git a/components/motion/3D/first-person/controller/mechanics/camera_controller_3d.gd b/components/motion/3D/first-person/controller/mechanics/camera_controller_3d.gd index 028e2bc..74a2c34 100644 --- a/components/motion/3D/first-person/controller/mechanics/camera_controller_3d.gd +++ b/components/motion/3D/first-person/controller/mechanics/camera_controller_3d.gd @@ -1,7 +1,7 @@ @icon("res://components/motion/3D/first-person/controller/mechanics/camera_controller_3d.svg") class_name CameraController3D extends Node3D -@export var actor: FirstPersonController +@export var actor: Node3D @export var camera: Camera3D ## 0 Means the rotation on the Y-axis is not limited @export_range(0, 360, 1, "degrees") var camera_vertical_limit = 89 @@ -55,7 +55,6 @@ var bob_index: float = 0.0 var bob_vector: Vector3 = Vector3.ZERO - func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion and InputHelper.is_mouse_captured(): var motion: InputEventMouseMotion = event.xformed_by(root_node.get_final_transform()) @@ -63,7 +62,7 @@ func _unhandled_input(event: InputEvent) -> void: func _ready() -> void: - assert(actor is Node3D, "CameraController: actor FirstPersonController is not set, this camera controller needs a reference to apply the camera movement") + assert(actor is Node3D, "CameraController: the Node3D actor is not set, this camera controller needs a reference to apply the camera movement") current_horizontal_limit = camera_horizontal_limit current_vertical_limit = camera_vertical_limit @@ -142,7 +141,6 @@ func swing_head(delta: float) -> void: func headbob(delta: float) -> void: if bob_enabled and actor.is_grounded and not actor.finite_state_machine.locked: - bob_index += bob_speed * delta if actor.is_grounded and not actor.motion_input.input_direction.is_zero_approx(): diff --git a/components/motion/3D/first-person/controller/states/ground/ground.gd b/components/motion/3D/first-person/controller/states/ground/ground.gd index 9858bcc..24cd738 100644 --- a/components/motion/3D/first-person/controller/states/ground/ground.gd +++ b/components/motion/3D/first-person/controller/states/ground/ground.gd @@ -69,7 +69,6 @@ func stair_step_up(): or actor.front_close_wall_checker.is_colliding() or actor.back_close_wall_checker.is_colliding(): return - stair_stepping = false if actor.motion_input.world_coordinate_space_direction.is_zero_approx(): diff --git a/components/motion/3D/first-person/controller/states/special/ladder_climb.gd b/components/motion/3D/first-person/controller/states/special/ladder_climb.gd new file mode 100644 index 0000000..d0b803a --- /dev/null +++ b/components/motion/3D/first-person/controller/states/special/ladder_climb.gd @@ -0,0 +1,73 @@ +class_name LadderClimb extends SpecialState + +## When automatic ladder detection is enabled, we give a cooldown time to not get stuck +## on the ladder when released +@export var dismount_jump_amount: float = 3.5 +@export var cooldown_time: float = 1.5 + +var current_ladder: Ladder3D +var cooldown_timer: Timer + + +func handle_input(event: InputEvent) -> void: + if current_ladder and current_ladder.press_to_release and Input.is_action_just_pressed(current_ladder.input_action_to_climb_ladder): + current_ladder = null + FSM.change_state_to(Fall) + + +func ready() -> void: + _create_cooldown_timer() + + +func enter() -> void: + ## This is necessary so that handle input is not thrown at the moment before enter() is called. + await get_tree().physics_frame + current_ladder = actor.ladder_cast_detector.get_collider(0).get_parent() + + if current_ladder == null: + FSM.change_state_to(Fall) + + actor.velocity = Vector3.ZERO + + +func exit(_next_state: MachineState) -> void: + if current_ladder and not current_ladder.press_to_climb and is_instance_valid(cooldown_timer): + cooldown_timer.start(cooldown_time) + + current_ladder = null + + +func physics_update(delta: float) -> void: + if actor.is_on_floor() or current_ladder == null: + FSM.change_state_to(Fall) + return + + var position_relative_to_ladder: Vector3 = current_ladder.global_transform.affine_inverse() * actor.global_position + var climb_direction: float = actor.motion_input.input_direction_vertical_axis + + actor.velocity.y = lerp(actor.velocity.y, -climb_direction * speed, delta * acceleration) + + var actor_height_reference: float = actor.stand_collision_shape.shape.height / 2.0 + var is_above_top_of_ladder: bool = position_relative_to_ladder.y > current_ladder.top_of_ladder.position.y - actor_height_reference + + if is_above_top_of_ladder: + actor.velocity = Vector3(0, dismount_jump_amount + actor.stand_collision_shape.shape.height, 0) + actor.velocity += actor.global_transform.basis.z * dismount_jump_amount + current_ladder = null + + actor.move_and_slide() + + detect_jump() + detect_swim() + + +func _create_cooldown_timer(): + if cooldown_timer == null: + cooldown_timer = Timer.new() + cooldown_timer.name = "LadderClimbCooldownTimer" + cooldown_timer.process_callback = Timer.TIMER_PROCESS_PHYSICS + cooldown_timer.wait_time = cooldown_time + cooldown_timer.autostart = false + cooldown_timer.one_shot = true + + add_child(cooldown_timer) diff --git a/components/motion/3D/first-person/controller/states/special/special_state.gd b/components/motion/3D/first-person/controller/states/special/special_state.gd index 45c432a..cde8955 100644 --- a/components/motion/3D/first-person/controller/states/special/special_state.gd +++ b/components/motion/3D/first-person/controller/states/special/special_state.gd @@ -12,6 +12,15 @@ class_name SpecialState extends MachineState @export var jump_input_action: StringName = InputControls.JumpAction +func _unhandled_input(event: InputEvent) -> void: + if actor.ladder_climb and not FSM.current_state_is_by_class(LadderClimb) \ + and actor.ladder_cast_detector.is_colliding(): + var ladder: Ladder3D = actor.ladder_cast_detector.get_collider(0).get_parent() + + if ladder.press_to_climb and Input.is_action_just_pressed(ladder.input_action_to_climb_ladder): + FSM.change_state_to(LadderClimb) + + func apply_gravity(force: float = gravity_force, delta: float = get_physics_process_delta_time()): actor.velocity += VectorHelper.up_direction_opposite_vector3(actor.up_direction) * force * delta @@ -21,5 +30,30 @@ func detect_jump() -> void: FSM.change_state_to(Jump) +func detect_swim() -> void: + if FSM.states.has("Swim") and actor.swim: + var swim_state: Swim = FSM.states["Swim"] as Swim + + if swim_state.eyes.global_position.y <= swim_state.water_height: + FSM.change_state_to(Swim) + + +func detect_ladder() -> void: + if actor.ladder_climb and actor.ladder_cast_detector.is_colliding(): + var ladder: Ladder3D = actor.ladder_cast_detector.get_collider(0).get_parent() + + if not ladder.press_to_climb: + FSM.change_state_to(LadderClimb) + + +func detect_ladder_input() -> void: + if actor.ladder_climb and not FSM.current_state_is_by_class(LadderClimb) \ + and actor.ladder_cast_detector.is_colliding(): + var ladder: Ladder3D = actor.ladder_cast_detector.get_collider(0).get_parent() + + if ladder.press_to_climb and Input.is_action_just_pressed(ladder.input_action_to_climb_ladder): + FSM.change_state_to(LadderClimb) + + func get_speed() -> float: return side_speed if actor.motion_input.input_direction in VectorHelper.horizontal_directions_v2 else speed diff --git a/components/motion/3D/first-person/controller/states/special/swim.gd b/components/motion/3D/first-person/controller/states/special/swim.gd index 8232ec7..d4ad779 100644 --- a/components/motion/3D/first-person/controller/states/special/swim.gd +++ b/components/motion/3D/first-person/controller/states/special/swim.gd @@ -1,5 +1,6 @@ class_name Swim extends SpecialState + @export var speed_reduction_on_water_entrance: float = 2.0 @export var eyes: Node3D @export var safe_submerged_margin: float = 0.15 @@ -8,15 +9,36 @@ class_name Swim extends SpecialState ##the height at which water is in the global world Y axis to detect if it's submerged or not var water_height: float = 0.0 - var was_underwater: bool = false -var is_underwater: bool = false +var is_underwater: bool = false: + set(value): + if value != is_underwater: + is_underwater = value + + if is_inside_tree() and ocean: + ocean.underwater.visible = is_underwater + +var ocean: Ocean + +func handle_input(event: InputEvent) -> void: + detect_ladder_input() + + +func ready() -> void: + ocean = get_tree().get_first_node_in_group(Ocean.GroupName) + if ocean: + water_height = ocean.water_level + ocean.water_level_changed.connect(on_water_level_changed) + func enter(): actor.velocity /= speed_reduction_on_water_entrance + actor.velocity.y = gravity_force + actor.move_and_slide() ## TODO - APPLY SUBMERGED AND OTHER UNDERWATER EFFECTS + func exit(_next_state: MachineState): pass ## TODO - SUBMERGED AND REFRACTION EFFECTS RESET @@ -40,15 +62,16 @@ func physics_update(delta: float): if was_underwater and not is_underwater: actor.velocity += actor.up_direction * underwater_exit_impulse - if actor.global_position.y > water_height: + if actor.global_position.y > water_height or (not is_underwater and actor.is_on_floor()): FSM.change_state_to(Fall) + detect_ladder() + actor.move_and_slide() - func accelerate(delta: float = get_physics_process_delta_time()): - var direction = actor.current_input_direction() + var direction = actor.motion_input.world_coordinate_space_direction var camera_direction = Camera3DHelper.forward_direction(actor.camera) var current_speed = get_speed() @@ -68,10 +91,17 @@ func accelerate(delta: float = get_physics_process_delta_time()): else: actor.velocity = direction * current_speed - -func decelerate(delta: float = get_physics_process_delta_time()) -> void: +func decelerate(delta: float = get_physics_process_delta_time()) -> void: + ## With this line we make sure the swim impulse is canceled and start to apply the gravity force in the water + if is_underwater and actor.velocity.y > 0: + actor.velocity.y = (VectorHelper.up_direction_opposite_vector3(actor.up_direction) * gravity_force).y + if friction > 0: actor.velocity = lerp(actor.velocity, Vector3(0, actor.velocity.y if is_underwater else 0.0, 0), clamp(friction * delta, 0, 1.0)) else: actor.velocity = Vector3(0, actor.velocity.y if is_underwater else 0.0, 0) + + +func on_water_level_changed(new_water_level: float) -> void: + water_height = new_water_level diff --git a/components/motion/3D/first-person/controller/states/transitions/any_to_ladder_climb_transition.gd.gd b/components/motion/3D/first-person/controller/states/transitions/any_to_ladder_climb_transition.gd.gd new file mode 100644 index 0000000..3c023e0 --- /dev/null +++ b/components/motion/3D/first-person/controller/states/transitions/any_to_ladder_climb_transition.gd.gd @@ -0,0 +1,8 @@ +class_name AnyToLadderClimbTransition extends MachineTransition + + +func should_transition() -> bool: + if to_state is LadderClimb: + return to_state.cooldown_timer.is_stopped() + + return false diff --git a/components/motion/3D/ladder/ladder_3d.gd b/components/motion/3D/ladder/ladder_3d.gd new file mode 100644 index 0000000..582a3f0 --- /dev/null +++ b/components/motion/3D/ladder/ladder_3d.gd @@ -0,0 +1,16 @@ +class_name Ladder3D extends Node3D + +@export var climb_area: Area3D +@export var top_of_ladder: Marker3D +@export var press_to_climb: bool = false +@export var press_to_release: bool = true +@export var input_action_to_climb_ladder: StringName = InputControls.ClimbLadder +@export var input_action_to_release_ladder: StringName = InputControls.ClimbLadder + + +func _ready() -> void: + climb_area.collision_layer = GameGlobals.ladders_collision_layer + climb_area.collision_mask = 0 + climb_area.monitorable = true + climb_area.monitoring = false + climb_area.priority = 1 diff --git a/components/motion/3D/ocean/ocean.gd b/components/motion/3D/ocean/ocean.gd new file mode 100644 index 0000000..1c15705 --- /dev/null +++ b/components/motion/3D/ocean/ocean.gd @@ -0,0 +1,65 @@ +class_name Ocean extends MeshInstance3D + +signal water_level_changed(new_water_level: float) + +const GroupName: StringName = &"ocean" + +## The water level with respect to the y-axis +@export var water_level: float = 0.0: + set(value): + if value != water_level: + water_level = value + water_level_changed.emit(water_level) +## When the boat beyond this distance from the center of the ocean, the ocean is repositioned to create the sensation of infinity. +@export var region_distance: float = 500.0 +@export var region_distance_checker_interval_time: float = 5.0: + set(value): + if value != region_distance_checker_interval_time: + region_distance_checker_interval_time = maxf(1.0, value) + + if is_inside_tree() and is_instance_valid(region_distance_timer): + region_distance_timer.stop() + region_distance_timer.wait_time = region_distance_checker_interval_time + region_distance_timer.start() + +@export var boat: Boat3D + +@onready var underwater: MeshInstance3D = $Underwater + +## If the player moves far away from the center of the mesh origin, +## this mesh changes its global position to that of the player to maintain a sense of infinite ocean. +var boat_distance_from_ocean_center: float = 0.0 +## A more performant way instead of calculating the distance each frame in the _process function +var region_distance_timer: Timer + +func _enter_tree() -> void: + add_to_group(GroupName) + _create_region_distance_timer() + + +func _ready() -> void: + if boat == null: + boat = get_tree().get_first_node_in_group(Boat3D.GroupName) + + global_position.y = water_level + + +func _create_region_distance_timer(): + if region_distance_timer == null: + region_distance_timer = Timer.new() + region_distance_timer.name = "RegionDistanceTimer" + region_distance_timer.process_callback = Timer.TIMER_PROCESS_PHYSICS + region_distance_timer.wait_time = region_distance_checker_interval_time + region_distance_timer.autostart = true + region_distance_timer.one_shot = false + + add_child(region_distance_timer) + region_distance_timer.timeout.connect(on_region_distance_timer_timeout) + + +func on_region_distance_timer_timeout() -> void: + boat_distance_from_ocean_center = NodePositioner.local_distance_to_v3(boat.boat_3d, self) + + if boat_distance_from_ocean_center >= region_distance: + boat_distance_from_ocean_center = 0.0 + global_position = Vector3(boat.boat_3d.global_position.x, water_level, boat.boat_3d.global_position.z) diff --git a/project.godot b/project.godot index 4e80fdc..6d52600 100644 --- a/project.godot +++ b/project.godot @@ -296,3 +296,8 @@ locale/translations=PackedStringArray("res://localization/translations/de.po", " 2d_physics/layer_9="PlayingCards" 3d_physics/layer_9="PlayingCards" 3d_physics/layer_10="DeckPiles" + +[physics] + +common/physics_ticks_per_second=120 +common/max_physics_steps_per_frame=12