Skip to content

Commit

Permalink
implemented basic boat 3d to setup a boat
Browse files Browse the repository at this point in the history
  • Loading branch information
ninetailsrabbit committed Dec 29, 2024
1 parent d4071c5 commit 4f8409e
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 15 deletions.
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# quality-godot-first-person-2
Actually good first person controller for the Godot Engine.
MIT License (credit Colormatic Studios)

This first person controller was made because there aren't many first person controllers for Godot, and the ones that do exist are pretty bad.
It is highly customizable and comes with many features, QOL, and clean code.

Some parts came from StayAtHomeDev's FPS tutorial. You can find that [here](https://www.youtube.com/playlist?list=PLEHvj4yeNfeF6s-UVs5Zx5TfNYmeCiYwf).

# Directions
Move with WASD, space to jump, shift to sprint, C to crouch.

**FEATURES:**
- Extremely configurable
- In-air momentum
- Motion smoothing
- FOV smoothing
- Movement animations
- Crouching
- Sprinting
- 2 crosshairs/reticles, one is animated (more to come?)
- Controller/GamePad support (enabled through code, see wiki)
- In-editor tools (enable editable children to use)

If you make a cool game with this addon, I would love to hear about it!

# Wiki
**To start out**, you should probably remap all of the movement keys to your own control set.

You can make this a super basic controller by just disabling everything.

**How to add controller/GamePad support**
- In the controls export group, there is a commented section at the end that says "Uncomment this if you want full controller support". Uncomment that block.
- Make a key map for each direction (left, right, up, down) and map them to your joystick.
- Write in these keymaps in the controls section of the player settings.
- In the `handle_head_rotation` function, there is another block of commented code that says the same thing. Uncomment that too.
- You should now be able to look around with the joystick. Make sure you add the other controls to the input map. (movement, jumping, crouching, sprinting, etc.)

**Slope/staircase:**
Credit to @roberto-urbani23
In the character inspector, you can uncheck Stop on Slope and set the max angle to 89 (for some reason, 90 will make the player stuck). Also Snap Length to 1 otherwise your character will not remain attached to stairs if you sprint while going downstairs.

**How to change settings:**
Click on the character node and there should be settings in the "Feature Settings" group.

**How to add animations for a mesh:**
- Create a function for your animation and attach it to `_physics_process` to call it every frame.
- Use `input_dir` as a boolean (it is actually a `Vector2`) to know if the player is walking.
- Use the `state` member variable to tell if the player is sprinting or crouching.
- Use the `is_on_floor` function to tell if the player is standing or falling.

**How to change reticles (crosshairs):**
Change the "Default Reticle" setting to your reticle file.
During runtime:
Use the `change_reticle` function on the character.

**How to create a new reticle:**
- Choose a reticle to base it off of.
- Open that reticle and save it as a new reticle.
- Remove the script from the reticle and create a new one. (for some reason you have to do this)
- Edit the reticle to your needs.
- Follow the "how to change reticles" directions to use it.

**How to use the editor tools:**
- Enable editable children on the `CharacterBody` node
- Use the options in the Properties tab to change things
- These changes apply in runtime as well
87 changes: 87 additions & 0 deletions components/motion/3D/boat/boat_3d.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
## Recommended to set linear damp to 1 and angular damp to 2
class_name Boat3D extends RigidBody3D

signal started_engine
signal stopped_engine

@export var boat_mesh: MeshInstance3D
@export var water_level: float = 0.0
@export_category("Engine")
@export var boat_engine_force: float = 20.0
@export var boat_reverse_engine_force: float = 10.0
@export var boat_acceleration: float = 5.0
@export var boat_reverse_acceleration: float = 5.0
@export var boat_steering_force: float = 1.0
@export_category("Bouyancy")
@export var buoyancy_root: Node3D:
set(value):
if value != buoyancy_root:
buoyancy_root = value

if is_node_ready() and buoyancy_root:
buoyancy_spots.assign(buoyancy_root.get_children())
@export var buoyancy_force: float = 20.0
@export var buoyancy_force_variation: float = 2.0
@export_category("Boat rudder")
@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

@onready var body: MeshInstance3D = $Body

var current_engine_force: float = 0.0
var buoyancy_spots: Array[Node3D] = []

var engine_on: bool = false:
set(value):
if value != engine_on:
engine_on = value

if engine_on:
started_engine.emit()
else:
current_engine_force = 0
stopped_engine.emit()


func _ready() -> void:
if buoyancy_root:
buoyancy_spots.assign(buoyancy_root.get_children())


func _physics_process(delta: float) -> void:
if engine_on:

if Input.is_action_pressed(InputControls.VehicleAccelerate):
if boat_acceleration > 0:
current_engine_force = lerp(current_engine_force, boat_engine_force, boat_acceleration * delta)
else:
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)
else:
current_engine_force = boat_reverse_engine_force

apply_central_force(global_transform.basis * (Vector3.BACK * 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)


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)
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ random_pitch = 1.5
streams_count = 1
stream_0/stream = SubResource("AudioStream_sjrlx")

[node name="FirstPersonController" type="CharacterBody3D"]
[node name="FirstPersonController" type="CharacterBody3D" groups=["player"]]
process_mode = 1
collision_layer = 2
collision_mask = 21
Expand Down Expand Up @@ -298,6 +298,7 @@ bob_head = NodePath("Head")
script = ExtResource("14_smxor")

[node name="FireArmWeaponHolder" type="Node3D" parent="CameraController/Head/CameraShake3D" node_paths=PackedStringArray("actor", "camera_controller", "camera_recoil_node")]
process_mode = 4
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.33, -0.55, -0.56)
script = ExtResource("15_cjftc")
actor = NodePath("../../../..")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ class_name CameraController3D extends Node3D
@onready var current_horizontal_limit: int:
set(value):
current_horizontal_limit = clamp(value, 0, 360)


@onready var root_node: Window = get_tree().root

var last_mouse_input: Vector2
var mouse_sensitivity: float = 3.0
Expand All @@ -54,14 +55,15 @@ 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(get_tree().root.get_final_transform())
last_mouse_input = motion.relative
var motion: InputEventMouseMotion = event.xformed_by(root_node.get_final_transform())
last_mouse_input += motion.relative


func _ready() -> void:
assert(actor is Node3D, "CameraController: actor Node3D is not set, this camera controller needs a reference to apply the camera movement")
assert(actor is Node3D, "CameraController: actor FirstPersonController 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
Expand All @@ -80,8 +82,11 @@ func _physics_process(delta: float) -> void:
headbob(delta)
rotate_camera(last_mouse_input)


func rotate_camera(motion: Vector2) -> void:
if motion.is_zero_approx():
return

var mouse_sens: float = mouse_sensitivity / 1000 # radians/pixel, 3 becomes 0.003

var twist_input: float = motion.x * mouse_sens ## Giro
Expand All @@ -92,7 +97,7 @@ func rotate_camera(motion: Vector2) -> void:

actor.rotation_degrees.y = limit_horizontal_rotation(actor.rotation_degrees.y)
rotation_degrees.x = limit_vertical_rotation(rotation_degrees.x)

actor.orthonormalize()
orthonormalize()

Expand Down Expand Up @@ -125,7 +130,6 @@ func unlock() -> void:
locked = false



func swing_head(delta: float) -> void:
if swing_head_enabled and actor.is_grounded:
var direction = actor.motion_input.input_direction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ class_name GroundState extends MachineState
@export var friction: float = 10.0
@export_group("Stair stepping")
## Define if the behaviour to step & down stairs it's enabled
@export var stair_stepping_enabled := true
@export var stair_stepping_enabled: bool = true
## Maximum height in meters the player can step up.
@export var max_step_up := 0.6
@export var max_step_up: float = 0.6
## Maximum height in meters the player can step down.
@export var max_step_down := -0.6
@export var max_step_down: float = -0.6
## Shortcut for converting vectors to vertical
@export var vertical := Vector3(0, 1, 0)
@export var vertical: Vector3 = Vector3(0, 1, 0)
## Shortcut for converting vectors to horizontal
@export var horizontal := Vector3(1, 0, 1)
@export var horizontal: Vector3 = Vector3(1, 0, 1)
@export_group("Input actions")
@export var run_input_action: StringName = InputControls.RunAction
@export var jump_input_action: StringName = InputControls.JumpAction
Expand All @@ -28,15 +28,15 @@ class_name GroundState extends MachineState
@export var crawl_animation: StringName = InputControls.CrawlAction

var current_speed: float = 0
var stair_stepping := false
var stair_stepping: bool = false


func physics_update(delta):
if not actor.is_grounded:
apply_gravity(gravity_force, delta)

if actor.is_falling() and not stair_stepping:
FSM.change_state_to("Fall")
FSM.change_state_to(Fall)


func apply_gravity(force: float = gravity_force, delta: float = get_physics_process_delta_time()):
Expand Down
38 changes: 38 additions & 0 deletions components/motion/3D/first-person/new_first_person_controller.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
extends CharacterBody3D

@onready var head: Node3D = $Head
@onready var camera_shake_3d: Camera3D = $Head/CameraShake3D


var mouse_input: Vector2 = Vector2.ZERO
var look_sensitivity = 0.005


func _unhandled_key_input(_event: InputEvent) -> void:
if Input.is_action_just_pressed("ui_cancel"):
CursorManager.switch_mouse_capture_mode()


func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
var motion: InputEventMouseMotion = event.xformed_by(get_tree().root.get_final_transform())
mouse_input = motion.relative


func _ready() -> void:
CursorManager.capture_mouse()


func _physics_process(delta: float) -> void:
rotate_y(-mouse_input.x * look_sensitivity)
camera_shake_3d.rotate_x(-mouse_input.y * look_sensitivity)
camera_shake_3d.rotation.x = clamp(camera_shake_3d.rotation.x, -1.5, 1.5)

mouse_input = Vector2.ZERO


func switch_mouse_capture_mode() -> void:
if InputHelper.is_mouse_visible():
InputHelper.capture_mouse()
else:
InputHelper.show_mouse_cursor()
17 changes: 17 additions & 0 deletions components/motion/3D/first-person/new_first_person_controller.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[gd_scene load_steps=3 format=3 uid="uid://c5hoxcyk8eujg"]

[ext_resource type="Script" path="res://components/motion/3D/first-person/new_first_person_controller.gd" id="1_es6b1"]

[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2h3by"]

[node name="NewFirstPersonController" type="CharacterBody3D"]
script = ExtResource("1_es6b1")

[node name="StandCollisionShape" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
shape = SubResource("CapsuleShape3D_2h3by")

[node name="Head" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.75, 0)

[node name="CameraShake3D" type="Camera3D" parent="Head"]

0 comments on commit 4f8409e

Please sign in to comment.