feat: init project + basic ship controller
This commit is contained in:
4
.editorconfig
Normal file
4
.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
1
icon.svg
Normal file
1
icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
43
icon.svg.import
Normal file
43
icon.svg.import
Normal file
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c6rk0cfu71wnd"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
82
main.tscn
Normal file
82
main.tscn
Normal file
@@ -0,0 +1,82 @@
|
||||
[gd_scene format=3 uid="uid://biwfc6l2lnu11"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b83vmbx10mlqe" path="res://ship_controller.gd" id="1_0xm2m"]
|
||||
[ext_resource type="PackedScene" uid="uid://iogkdi8dv0nv" path="res://projectile.tscn" id="2_1bvp3"]
|
||||
[ext_resource type="Script" uid="uid://inqwtwfui2tr" path="res://ship_camera.gd" id="2_h2yge"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_1bvp3"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_lquwl"]
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_h2yge"]
|
||||
|
||||
[sub_resource type="Sky" id="Sky_1bvp3"]
|
||||
sky_material = SubResource("ProceduralSkyMaterial_h2yge")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_0xm2m"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_1bvp3")
|
||||
ambient_light_source = 3
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1472247091]
|
||||
|
||||
[node name="Ship" type="CharacterBody3D" parent="." unique_id=122234868 node_paths=PackedStringArray("ship_model", "muzzle")]
|
||||
script = ExtResource("1_0xm2m")
|
||||
turn_speed = 4.975
|
||||
projectile_scene = ExtResource("2_1bvp3")
|
||||
ship_model = NodePath("ShipModel")
|
||||
muzzle = NodePath("Muzzle")
|
||||
|
||||
[node name="ShipModel" type="MeshInstance3D" parent="Ship" unique_id=352448396]
|
||||
mesh = SubResource("BoxMesh_1bvp3")
|
||||
|
||||
[node name="HoverRay" type="RayCast3D" parent="Ship" unique_id=1076533010]
|
||||
target_position = Vector3(0, -5, 0)
|
||||
|
||||
[node name="Muzzle" type="Marker3D" parent="Ship" unique_id=322799309]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.5)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ship" unique_id=567390614]
|
||||
shape = SubResource("BoxShape3D_lquwl")
|
||||
|
||||
[node name="CameraRig" type="Node3D" parent="." unique_id=920354244 node_paths=PackedStringArray("target")]
|
||||
script = ExtResource("2_h2yge")
|
||||
target = NodePath("../Ship")
|
||||
follow_distance = 8.57
|
||||
position_smoothing = 18.56
|
||||
rotation_smoothing = 14.5
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="CameraRig" unique_id=2079353001]
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1690012445]
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=2019756943]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12, 0.07247639, -21.964855)
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=842749163]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -24, 0.07247639, 17.169182)
|
||||
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="CSGCombiner3D" unique_id=2106821369]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -36, 6.410921, -17.84053)
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1236194522]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -48, 6.410921, 14.210682)
|
||||
|
||||
[node name="CSGBox3D5" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1570750803]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -36, -5.170101, -25.111805)
|
||||
|
||||
[node name="CSGBox3D6" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1205494666]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -51.54701, 1.1025181, 0)
|
||||
|
||||
[node name="CSGBox3D7" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1295905165]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -60, 12.749367, -34.99634)
|
||||
|
||||
[node name="CSGBox3D8" type="CSGBox3D" parent="CSGCombiner3D" unique_id=749127710]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -72, 12.749367, 0)
|
||||
|
||||
[node name="CSGBox3D9" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1499064177]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -16.63602, 0)
|
||||
size = Vector3(500, 1, 500)
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1014540785]
|
||||
environment = SubResource("Environment_0xm2m")
|
||||
57
project.godot
Normal file
57
project.godot
Normal file
@@ -0,0 +1,57 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Mercury"
|
||||
run/main_scene="uid://biwfc6l2lnu11"
|
||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[input]
|
||||
|
||||
accelerate={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
brake={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
strafe_left={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
strafe_right={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
boost={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
shoot={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Jolt Physics"
|
||||
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
35
projectile.gd
Normal file
35
projectile.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
extends Area3D
|
||||
class_name HovercraftProjectile
|
||||
## Simple bullet that travels forward and destroys itself on hit or timeout.
|
||||
|
||||
@export var speed: float = 80.0
|
||||
@export var lifetime: float = 3.0
|
||||
@export var damage: float = 10.0
|
||||
|
||||
var _direction: Vector3 = Vector3.FORWARD
|
||||
var _age: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Use the node's forward direction if no explicit direction was set
|
||||
if _direction == Vector3.FORWARD:
|
||||
_direction = - global_transform.basis.z
|
||||
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func set_direction(dir: Vector3) -> void:
|
||||
_direction = dir.normalized()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
global_position += _direction * speed * delta
|
||||
_age += delta
|
||||
if _age >= lifetime:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if body.has_method("take_damage"):
|
||||
body.take_damage(damage)
|
||||
queue_free()
|
||||
1
projectile.gd.uid
Normal file
1
projectile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cpqdkciafcu51
|
||||
16
projectile.tscn
Normal file
16
projectile.tscn
Normal file
@@ -0,0 +1,16 @@
|
||||
[gd_scene format=3 uid="uid://iogkdi8dv0nv"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cpqdkciafcu51" path="res://projectile.gd" id="1_a85iw"]
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_akgne"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_a85iw"]
|
||||
|
||||
[node name="Projectile" type="Area3D" unique_id=24893414]
|
||||
script = ExtResource("1_a85iw")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1868961436]
|
||||
mesh = SubResource("SphereMesh_akgne")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1475750571]
|
||||
shape = SubResource("SphereShape3D_a85iw")
|
||||
46
ship_camera.gd
Normal file
46
ship_camera.gd
Normal file
@@ -0,0 +1,46 @@
|
||||
extends Node3D
|
||||
class_name ShipCamera
|
||||
## Third-person camera for a freely flying ship.
|
||||
## Stays behind and above the ship regardless of pitch/yaw.
|
||||
## Attach as a sibling of the ship (child of the scene root), NOT as a child
|
||||
## of the ship, so it can lag smoothly.
|
||||
|
||||
@export var target: ShipController
|
||||
@export var follow_distance: float = 12.0
|
||||
@export var follow_height: float = 3.0 ## offset along the ship's local UP
|
||||
@export var look_ahead: float = 4.0
|
||||
@export var position_smoothing: float = 5.0
|
||||
@export var rotation_smoothing: float = 6.0
|
||||
|
||||
@onready var camera: Camera3D = $Camera3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not camera:
|
||||
camera = Camera3D.new()
|
||||
add_child(camera)
|
||||
camera.current = true
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not target:
|
||||
return
|
||||
|
||||
# "Behind" the ship in its local space (accounts for pitch + yaw)
|
||||
var ship_back := target.global_transform.basis.z # +Z is behind in Godot
|
||||
var ship_up := target.global_transform.basis.y
|
||||
|
||||
var desired_pos := (
|
||||
target.global_position
|
||||
+ ship_back * follow_distance
|
||||
+ ship_up * follow_height
|
||||
)
|
||||
|
||||
global_position = global_position.lerp(desired_pos, position_smoothing * delta)
|
||||
|
||||
# Look at a point ahead of the ship
|
||||
var ship_forward := -target.global_transform.basis.z
|
||||
var look_target := target.global_position + ship_forward * look_ahead
|
||||
|
||||
var desired_transform := global_transform.looking_at(look_target, Vector3.UP)
|
||||
global_transform = global_transform.interpolate_with(desired_transform, rotation_smoothing * delta)
|
||||
1
ship_camera.gd.uid
Normal file
1
ship_camera.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://inqwtwfui2tr
|
||||
262
ship_controller.gd
Normal file
262
ship_controller.gd
Normal file
@@ -0,0 +1,262 @@
|
||||
extends CharacterBody3D
|
||||
class_name ShipController
|
||||
## FP4-style ship controller for Godot 4.6
|
||||
##
|
||||
## Full 3D flight — the ship flies wherever the mouse aims.
|
||||
## Mouse X = yaw, Mouse Y = pitch. W accelerates along the aim direction,
|
||||
## S brakes, A/D strafe laterally. No gravity, no hover, no ground.
|
||||
## The visual model applies cosmetic roll/pitch on top of the actual orientation.
|
||||
|
||||
# ── Movement tuning ──────────────────────────────────────────────────────────
|
||||
@export_group("Movement")
|
||||
@export var max_speed: float = 30.0
|
||||
@export var acceleration: float = 40.0
|
||||
@export var brake_deceleration: float = 50.0
|
||||
@export var strafe_speed: float = 20.0
|
||||
@export var friction: float = 12.0 ## passive drag when no input
|
||||
|
||||
@export_group("Boost")
|
||||
@export var boost_multiplier: float = 1.8
|
||||
@export var boost_max: float = 100.0
|
||||
@export var boost_drain_rate: float = 30.0
|
||||
@export var boost_recharge_rate: float = 15.0
|
||||
@export var boost_min_to_activate: float = 10.0
|
||||
|
||||
# ── Cosmetic tilt ────────────────────────────────────────────────────────────
|
||||
@export_group("Visual Tilt")
|
||||
@export var max_roll_deg: float = 25.0 ## extra bank on strafe / turning
|
||||
@export var max_pitch_deg: float = 15.0 ## extra nose-down on accel
|
||||
@export var tilt_lerp_speed: float = 6.0
|
||||
|
||||
# ── Mouse ────────────────────────────────────────────────────────────────────
|
||||
@export_group("Mouse")
|
||||
@export var mouse_sensitivity: float = 0.003
|
||||
@export var pitch_limit_deg: float = 85.0 ## clamp so you can't fully invert
|
||||
|
||||
# ── Combat ───────────────────────────────────────────────────────────────────
|
||||
@export_group("Combat")
|
||||
@export var fire_rate: float = 10.0
|
||||
@export var projectile_scene: PackedScene
|
||||
|
||||
# ── Node references ──────────────────────────────────────────────────────────
|
||||
@export_group("Node References")
|
||||
@export var ship_model: Node3D ## visual mesh that gets cosmetic tilt
|
||||
@export var muzzle: Marker3D ## bullet spawn point
|
||||
|
||||
# ── Internal state ───────────────────────────────────────────────────────────
|
||||
var _forward_speed: float = 0.0
|
||||
var _strafe_speed: float = 0.0
|
||||
|
||||
# Mouse-driven aim targets (body smoothly rotates toward these)
|
||||
var _target_yaw: float = 0.0
|
||||
var _target_pitch: float = 0.0
|
||||
var _current_yaw: float = 0.0
|
||||
var _current_pitch: float = 0.0
|
||||
|
||||
var _boost_energy: float = 0.0
|
||||
var _is_boosting: bool = false
|
||||
var _fire_cooldown: float = 0.0
|
||||
|
||||
# Cosmetic tilt state (applied to ship_model only)
|
||||
var _visual_roll: float = 0.0
|
||||
var _visual_extra_pitch: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
_boost_energy = boost_max
|
||||
_current_yaw = rotation.y
|
||||
_current_pitch = rotation.x
|
||||
_target_yaw = _current_yaw
|
||||
_target_pitch = _current_pitch
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
_target_yaw -= event.relative.x * mouse_sensitivity
|
||||
_target_pitch -= event.relative.y * mouse_sensitivity
|
||||
# Clamp pitch so the ship can't go fully vertical / inverted
|
||||
var limit := deg_to_rad(pitch_limit_deg)
|
||||
_target_pitch = clampf(_target_pitch, -limit, limit)
|
||||
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
else:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_handle_turning(delta)
|
||||
_handle_boost(delta)
|
||||
_handle_movement(delta)
|
||||
_apply_velocity(delta)
|
||||
_handle_visual_tilt(delta)
|
||||
_handle_shooting(delta)
|
||||
|
||||
|
||||
# ── Turning (yaw + pitch) ───────────────────────────────────────────────────
|
||||
## Instead of lerp_angle (which breaks on large deltas and can snap backward
|
||||
## near ±π), we use move_toward on the angular difference. This guarantees:
|
||||
## 1. The ship always turns toward the target, never away from it.
|
||||
## 2. The turn rate is capped at max_turn_rate rad/s so fast mouse flicks
|
||||
## produce a fast but controlled rotation, not a teleport or reversal.
|
||||
|
||||
@export var max_turn_rate: float = 12.0 ## radians/sec — max rotation speed
|
||||
|
||||
func _handle_turning(delta: float) -> void:
|
||||
# Yaw — target is unbounded (no wraparound bugs), difference is always correct
|
||||
var yaw_diff := _target_yaw - _current_yaw
|
||||
var yaw_step := minf(absf(yaw_diff), max_turn_rate * delta)
|
||||
_current_yaw += signf(yaw_diff) * yaw_step
|
||||
|
||||
# Pitch — same approach
|
||||
var pitch_diff := _target_pitch - _current_pitch
|
||||
var pitch_step := minf(absf(pitch_diff), max_turn_rate * delta)
|
||||
_current_pitch += signf(pitch_diff) * pitch_step
|
||||
|
||||
rotation.y = _current_yaw
|
||||
rotation.x = _current_pitch
|
||||
|
||||
|
||||
# ── Movement along the ship's facing direction ──────────────────────────────
|
||||
|
||||
func _handle_movement(delta: float) -> void:
|
||||
var forward_input: float = 0.0
|
||||
var strafe_input: float = 0.0
|
||||
|
||||
if Input.is_action_pressed("accelerate"):
|
||||
forward_input = 1.0
|
||||
if Input.is_action_pressed("brake"):
|
||||
forward_input -= 1.0
|
||||
if Input.is_action_pressed("strafe_left"):
|
||||
strafe_input -= 1.0
|
||||
if Input.is_action_pressed("strafe_right"):
|
||||
strafe_input += 1.0
|
||||
|
||||
var effective_max := max_speed * _boost_value()
|
||||
var effective_accel := acceleration * _boost_value()
|
||||
|
||||
# Forward / brake
|
||||
if forward_input > 0.0:
|
||||
_forward_speed = move_toward(_forward_speed, effective_max, effective_accel * delta)
|
||||
elif forward_input < 0.0:
|
||||
_forward_speed = move_toward(_forward_speed, 0.0, brake_deceleration * delta)
|
||||
else:
|
||||
_forward_speed = move_toward(_forward_speed, 0.0, friction * delta)
|
||||
|
||||
# Strafe
|
||||
if strafe_input != 0.0:
|
||||
_strafe_speed = move_toward(_strafe_speed, strafe_speed * strafe_input, acceleration * delta)
|
||||
else:
|
||||
_strafe_speed = move_toward(_strafe_speed, 0.0, friction * delta)
|
||||
|
||||
|
||||
# ── Boost ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _handle_boost(delta: float) -> void:
|
||||
if Input.is_action_pressed("boost") and _boost_energy > boost_min_to_activate:
|
||||
_is_boosting = true
|
||||
_boost_energy = maxf(0.0, _boost_energy - boost_drain_rate * delta)
|
||||
if _boost_energy <= 0.0:
|
||||
_is_boosting = false
|
||||
else:
|
||||
_is_boosting = false
|
||||
_boost_energy = minf(boost_max, _boost_energy + boost_recharge_rate * delta)
|
||||
|
||||
|
||||
func _boost_value() -> float:
|
||||
return boost_multiplier if _is_boosting else 1.0
|
||||
|
||||
|
||||
# ── Apply velocity in 3D ────────────────────────────────────────────────────
|
||||
|
||||
func _apply_velocity(delta: float) -> void:
|
||||
# Forward is wherever the ship is pointing (includes pitch + yaw)
|
||||
var forward_dir := -transform.basis.z
|
||||
var right_dir := transform.basis.x
|
||||
|
||||
var desired := forward_dir * _forward_speed + right_dir * _strafe_speed
|
||||
|
||||
# Clamp total speed
|
||||
var max_total := max_speed * _boost_value()
|
||||
if desired.length() > max_total:
|
||||
desired = desired.normalized() * max_total
|
||||
|
||||
velocity = desired
|
||||
move_and_slide()
|
||||
|
||||
|
||||
# ── Cosmetic tilt on the visual model ────────────────────────────────────────
|
||||
## The CharacterBody3D itself rotates to face the aim direction (yaw + pitch).
|
||||
## On top of that, the child ship_model gets extra roll from strafing/turning
|
||||
## and extra pitch from acceleration — purely visual seasoning.
|
||||
|
||||
func _handle_visual_tilt(delta: float) -> void:
|
||||
if not ship_model:
|
||||
return
|
||||
|
||||
# ── Roll from turning + strafing ──
|
||||
# yaw is unbounded now, so the raw difference is the true angular lag
|
||||
var yaw_delta: float = _target_yaw - _current_yaw
|
||||
|
||||
var strafe_input: float = 0.0
|
||||
if Input.is_action_pressed("strafe_left"):
|
||||
strafe_input -= 1.0
|
||||
if Input.is_action_pressed("strafe_right"):
|
||||
strafe_input += 1.0
|
||||
|
||||
var target_roll_deg: float = (-strafe_input * max_roll_deg * 0.6) + (yaw_delta * max_roll_deg * 2.0)
|
||||
target_roll_deg = clampf(target_roll_deg, -max_roll_deg, max_roll_deg)
|
||||
|
||||
# ── Extra pitch from acceleration ──
|
||||
var forward_input: float = 0.0
|
||||
if Input.is_action_pressed("accelerate"):
|
||||
forward_input = 1.0
|
||||
if Input.is_action_pressed("brake"):
|
||||
forward_input -= 1.0
|
||||
|
||||
var target_extra_pitch_deg: float = - forward_input * max_pitch_deg
|
||||
if _is_boosting:
|
||||
target_extra_pitch_deg *= 1.4
|
||||
|
||||
# Smooth interpolation
|
||||
_visual_roll = lerp(_visual_roll, deg_to_rad(target_roll_deg), tilt_lerp_speed * delta)
|
||||
_visual_extra_pitch = lerp(_visual_extra_pitch, deg_to_rad(target_extra_pitch_deg), tilt_lerp_speed * delta)
|
||||
|
||||
# Apply roll and extra pitch on top of the body's real orientation
|
||||
ship_model.rotation = Vector3(_visual_extra_pitch, 0.0, _visual_roll)
|
||||
|
||||
|
||||
# ── Shooting ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func _handle_shooting(delta: float) -> void:
|
||||
_fire_cooldown = maxf(0.0, _fire_cooldown - delta)
|
||||
|
||||
if Input.is_action_pressed("shoot") and _fire_cooldown <= 0.0 and projectile_scene:
|
||||
_fire_cooldown = 1.0 / fire_rate
|
||||
var bullet := projectile_scene.instantiate() as Node3D
|
||||
get_tree().current_scene.add_child(bullet)
|
||||
|
||||
if muzzle:
|
||||
bullet.global_position = muzzle.global_position
|
||||
bullet.global_transform.basis = muzzle.global_transform.basis
|
||||
else:
|
||||
bullet.global_position = global_position + (-transform.basis.z * 2.0)
|
||||
bullet.global_transform.basis = global_transform.basis
|
||||
|
||||
# Pass the full 3D forward direction to the bullet
|
||||
if bullet.has_method("set_direction"):
|
||||
bullet.set_direction(-global_transform.basis.z)
|
||||
|
||||
|
||||
# ── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
func get_boost_ratio() -> float:
|
||||
return _boost_energy / boost_max
|
||||
|
||||
func get_speed_ratio() -> float:
|
||||
return _forward_speed / (max_speed * boost_multiplier)
|
||||
|
||||
func is_boosting() -> bool:
|
||||
return _is_boosting
|
||||
1
ship_controller.gd.uid
Normal file
1
ship_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b83vmbx10mlqe
|
||||
Reference in New Issue
Block a user