From 1bd54e425e08f2fe6c7921c625aefc0268f9cbd7 Mon Sep 17 00:00:00 2001 From: yvmou_ Date: Sun, 13 Sep 2026 02:09:22 +0800 Subject: [PATCH] =?UTF-8?q?ai=E5=AE=9E=E7=8E=B0=E5=9F=BA=E7=A1=80=E9=80=9A?= =?UTF-8?q?=E8=AF=9D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- default_bus_layout.tres | 37 +++++++++++++++++ scenes/level/player.tscn | 17 ++++++++ scripts/player/3d_godot_robot.gd | 3 +- scripts/player/player.gd | 64 ++++++++++++++--------------- scripts/player/spring_arm_offset.gd | 18 +++++--- scripts/player/voice_chat.gd | 55 +++++++++++++++++++++++++ scripts/player/voice_chat.gd.uid | 1 + 7 files changed, 156 insertions(+), 39 deletions(-) create mode 100644 default_bus_layout.tres create mode 100644 scripts/player/voice_chat.gd create mode 100644 scripts/player/voice_chat.gd.uid diff --git a/default_bus_layout.tres b/default_bus_layout.tres new file mode 100644 index 0000000..a87c555 --- /dev/null +++ b/default_bus_layout.tres @@ -0,0 +1,37 @@ +[gd_resource type="AudioBusLayout" format=3 uid="uid://c8prq6lu7oaae"] + +[sub_resource type="AudioEffectCapture" id="AudioEffectCapture_voice"] +buffer_length = 0.2 + +[sub_resource type="AudioEffectReverb" id="AudioEffectReverb_j3pel"] +resource_name = "reverb" +hipass = 1.0 + +[sub_resource type="AudioEffectPanner" id="AudioEffectPanner_g28q7"] +resource_name = "pan" + +[resource] +bus/1/name = &"mic" +bus/1/solo = false +bus/1/mute = true +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = &"Master" +bus/1/effect/0/effect = SubResource("AudioEffectCapture_voice") +bus/1/effect/0/enabled = true +bus/2/name = &"RaytracedReverb" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = 0.0 +bus/2/send = &"Master" +bus/2/effect/0/effect = SubResource("AudioEffectReverb_j3pel") +bus/2/effect/0/enabled = true +bus/3/name = &"RaytracedAmbient" +bus/3/solo = false +bus/3/mute = false +bus/3/bypass_fx = false +bus/3/volume_db = 0.0 +bus/3/send = &"Master" +bus/3/effect/0/effect = SubResource("AudioEffectPanner_g28q7") +bus/3/effect/0/enabled = true diff --git a/scenes/level/player.tscn b/scenes/level/player.tscn index f99b460..fb9c9de 100644 --- a/scenes/level/player.tscn +++ b/scenes/level/player.tscn @@ -2,11 +2,16 @@ [ext_resource type="Script" uid="uid://c2si8gkbnde0c" path="res://scripts/player/player.gd" id="1_tdh26"] [ext_resource type="Script" uid="uid://v8j54rbc0ik" path="res://scripts/player/3d_godot_robot.gd" id="2_mexc5"] +[ext_resource type="PackedScene" uid="uid://cxuh415268dqx" path="res://assets/characters/zombie.glb" id="3_0potc"] [ext_resource type="ArrayMesh" uid="uid://dm4j7r8tq2x5c" path="res://assets/characters/player/meshes/godot_robot_chest.tres" id="3_chest"] [ext_resource type="ArrayMesh" uid="uid://c6oak2v5p8s1a" path="res://assets/characters/player/meshes/godot_robot_face.tres" id="3_face"] [ext_resource type="ArrayMesh" uid="uid://bt25tokyr00xs" path="res://assets/characters/player/meshes/godot_robot_bottom.tres" id="3_hbshb"] [ext_resource type="ArrayMesh" uid="uid://b3f7h1naq4w6e" path="res://assets/characters/player/meshes/godot_robot_limbs_and_head.tres" id="3_limbs"] +[ext_resource type="AnimationLibrary" uid="uid://dj2grhmw35lxn" path="res://assets/characters/zombie_idle.glb" id="4_fesw4"] [ext_resource type="Script" uid="uid://bj7yrijm7bppq" path="res://scripts/player/spring_arm_offset.gd" id="4_ygggd"] +[ext_resource type="AnimationLibrary" uid="uid://bcj3urpoubxqe" path="res://assets/characters/zombie_jump.glb" id="5_qce6l"] +[ext_resource type="Script" uid="uid://3i8mgirdlpli" path="res://scripts/player/voice_chat.gd" id="5_voice"] +[ext_resource type="AnimationLibrary" uid="uid://wncmdkjlpsw6" path="res://assets/characters/zombie_run.glb" id="6_bfcyu"] [ext_resource type="PackedScene" uid="uid://difodi4a2r6vc" path="res://assets/items/weapons/models/sword.fbx" id="16_sword"] [ext_resource type="FontFile" uid="uid://diapabmalpcrj" path="res://assets/fonts/Kurland.ttf" id="16_xgf8p"] [ext_resource type="PackedScene" uid="uid://dj7318sc4o37l" path="res://assets/items/weapons/models/sword_big.fbx" id="17_sword_big"] @@ -4572,6 +4577,15 @@ collision_mask = 7 transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0) shape = SubResource("CylinderShape3D_2l1sl") +[node name="zombie" parent="GodotRobot3D" unique_id=191730779 instance=ExtResource("3_0potc")] +transform = Transform3D(-0.405, 0, 6.11533e-08, 0, 0.405, 0, -6.11533e-08, 0, -0.405, 0.030783774, 0, -0.029726265) +visible = false + +[node name="AnimationPlayer" type="AnimationPlayer" parent="GodotRobot3D/zombie" unique_id=1360715266] +libraries/zombie_idle = ExtResource("4_fesw4") +libraries/zombie_jump = ExtResource("5_qce6l") +libraries/zombie_run = ExtResource("6_bfcyu") + [node name="RobotArmature" type="Node3D" parent="GodotRobot3D" unique_id=91632620] transform = Transform3D(0.5784721, 0, 0, 0, 0.5784721, 0, 0, 0, 0.5784721, 0, 0, 0) @@ -4893,3 +4907,6 @@ font = ExtResource("16_xgf8p") [node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=1634180657] replication_config = SubResource("SceneReplicationConfig_wb3a5") + +[node name="Voice" type="Node" parent="." unique_id=44300263] +script = ExtResource("5_voice") diff --git a/scripts/player/3d_godot_robot.gd b/scripts/player/3d_godot_robot.gd index 4c70bf6..d7b1511 100644 --- a/scripts/player/3d_godot_robot.gd +++ b/scripts/player/3d_godot_robot.gd @@ -1,6 +1,7 @@ -class_name Body +#class_name Body extends Node3D +# 线性插值速度 const LERP_VELOCITY: float = 0.15 @export_category("Objects") diff --git a/scripts/player/player.gd b/scripts/player/player.gd index 25a73ae..141e77f 100644 --- a/scripts/player/player.gd +++ b/scripts/player/player.gd @@ -3,17 +3,17 @@ extends CharacterBody3D enum SkinColor { BLUE, YELLOW, GREEN, RED } -const NORMAL_SPEED = 6.0 -const SPRINT_SPEED = 10.0 -const JUMP_VELOCITY = 7.5 -const FALL_GRAVITY_MULTIPLIER = 1.6 -const BASE_NICKNAME_HEIGHT := 2.0 -const SERVER_ANIMATION_REQUESTS_PER_SECOND := 10.0 -const SERVER_ANIMATION_REQUEST_BURST := 6.0 -const PICKUP_ANIMATION_DELAY_MSEC := 1000 -const PICKUP_ANIMATION_WINDOW_MSEC := 2500 -const PICKUP_REQUEST_COOLDOWN_MSEC := 1000 -const ALLOWED_ANIMATION_STATES := { +@export var 正常速度 = 6.0 +@export var 冲刺速度 = 10.0 +@export var 跳跃初速度 = 7.5 +@export var 下落重力倍率 = 1.6 +@export var 昵称基准高度 := 2.0 +@export var 服务器每秒动画请求数 := 10.0 +@export var 服务器动画请求突发上限 := 6.0 +@export var 拾取动画延迟毫秒 := 1000 +@export var 拾取动画时间窗口毫秒 := 2500 +@export var 拾取请求冷却毫秒 := 1000 +const 允许动画状态 := { &"Idle": true, &"Run": true, &"Sprint": true, @@ -66,10 +66,10 @@ var _animation_sequence := 0 var _last_applied_animation_sequence := 0 var _last_requested_animation: StringName = &"" var _appearance_sync_requesters: Dictionary = {} -var _server_animation_request_tokens := SERVER_ANIMATION_REQUEST_BURST +var _server_animation_request_tokens := 服务器动画请求突发上限 var _last_server_animation_token_update_msec := 0 var _server_pickup_animation_started_msec := -1 -var _last_server_pickup_request_msec := -PICKUP_REQUEST_COOLDOWN_MSEC +var _last_server_pickup_request_msec := -拾取请求冷却毫秒 var _pickup_area_camera_yaw_offset := 0.0 var _equipped_hat_visual_id := "" @@ -103,7 +103,7 @@ func _ready(): animation_player.animation_finished.connect(_on_animation_finished) _body.play_animation_state(&"Idle") nickname.visible = true - _set_nickname_height(BASE_NICKNAME_HEIGHT) + _set_nickname_height(昵称基准高度) call_deferred("_update_nickname_height") if not multiplayer.is_server(): call_deferred("_request_equipment_appearance") @@ -194,14 +194,14 @@ func _physics_process(delta): has_double_jumped = false if Input.is_action_just_pressed("jump"): - velocity.y = JUMP_VELOCITY + velocity.y = 跳跃初速度 can_double_jump = true _request_animation(&"Jump", true) else: _apply_gravity(delta) if can_double_jump and not has_double_jumped and Input.is_action_just_pressed("jump"): - velocity.y = JUMP_VELOCITY + velocity.y = 跳跃初速度 has_double_jumped = true can_double_jump = false _request_animation(&"Jump2", true) @@ -217,7 +217,7 @@ func _physics_process(delta): func _apply_gravity(delta: float) -> void: if is_on_floor(): return - var gravity_multiplier = FALL_GRAVITY_MULTIPLIER if velocity.y < 0 else 1.0 + var gravity_multiplier = 下落重力倍率 if velocity.y < 0 else 1.0 velocity.y -= gravity * gravity_multiplier * delta @@ -239,7 +239,7 @@ func _on_animation_finished(animation_name: StringName) -> void: func _request_animation(state: StringName, restart: bool = false) -> void: - if not ALLOWED_ANIMATION_STATES.has(state): + if not 允许动画状态.has(state): return if not restart and _last_requested_animation == state: return @@ -255,7 +255,7 @@ func _request_animation(state: StringName, restart: bool = false) -> void: func request_animation_state(state: StringName) -> void: if not multiplayer.is_server() or not _is_owner_request(): return - if not ALLOWED_ANIMATION_STATES.has(state): + if not 允许动画状态.has(state): return if not _server_consume_animation_request_token(): return @@ -275,8 +275,8 @@ func _server_consume_animation_request_token() -> bool: else: var elapsed_seconds := (now - _last_server_animation_token_update_msec) / 1000.0 _server_animation_request_tokens = minf( - SERVER_ANIMATION_REQUEST_BURST, - _server_animation_request_tokens + elapsed_seconds * SERVER_ANIMATION_REQUESTS_PER_SECOND + 服务器动画请求突发上限, + _server_animation_request_tokens + elapsed_seconds * 服务器每秒动画请求数 ) _last_server_animation_token_update_msec = now @@ -303,7 +303,7 @@ func sync_animation_state(state: StringName, sequence: int) -> void: return _last_applied_animation_sequence = sequence - if not ALLOWED_ANIMATION_STATES.has(state): + if not 允许动画状态.has(state): return # The owner already played the input-driven state immediately. @@ -366,9 +366,9 @@ func _move() -> void: func _is_running() -> bool: if Input.is_action_pressed("shift"): - _current_speed = SPRINT_SPEED + _current_speed = 冲刺速度 return true - _current_speed = NORMAL_SPEED + _current_speed = 正常速度 return false @@ -704,12 +704,12 @@ func _update_nickname_height(hat_id: String = "") -> void: func _calculate_nickname_height(hat_id: String) -> float: - var target_height := BASE_NICKNAME_HEIGHT + var target_height := 拾取动画延迟毫秒 var equipped_hat := _get_hat_node(hat_id) if equipped_hat: var hat_top := _get_visual_top(equipped_hat) if hat_top > -INF and hat_top < INF: - target_height = max(BASE_NICKNAME_HEIGHT, hat_top + nickname_clearance) + target_height = max(拾取动画延迟毫秒, hat_top + nickname_clearance) return target_height @@ -724,9 +724,9 @@ func apply_synced_nickname_height(height: float) -> void: return nickname.visible = not _is_local_first_person() if height > -INF and height < INF: - _set_nickname_height(maxf(BASE_NICKNAME_HEIGHT, height)) + _set_nickname_height(maxf(昵称基准高度, height)) else: - _set_nickname_height(BASE_NICKNAME_HEIGHT) + _set_nickname_height(昵称基准高度) func _is_local_first_person() -> bool: @@ -734,7 +734,7 @@ func _is_local_first_person() -> bool: func get_current_nickname_height() -> float: - return nickname.position.y if nickname else BASE_NICKNAME_HEIGHT + return nickname.position.y if nickname else 昵称基准高度 func _get_hat_node(hat_id: String) -> Node3D: @@ -808,15 +808,15 @@ func request_pickup() -> void: if not multiplayer.is_server() or not _is_owner_request(): return var now := Time.get_ticks_msec() - if now - _last_server_pickup_request_msec < PICKUP_REQUEST_COOLDOWN_MSEC: + if now - _last_server_pickup_request_msec < 拾取请求冷却毫秒: return _last_server_pickup_request_msec = now var animation_elapsed := now - _server_pickup_animation_started_msec if ( _server_pickup_animation_started_msec < 0 - or animation_elapsed < PICKUP_ANIMATION_DELAY_MSEC - or animation_elapsed > PICKUP_ANIMATION_WINDOW_MSEC + or animation_elapsed < 拾取动画时间窗口毫秒 + or animation_elapsed > 拾取动画时间窗口毫秒 ): return if not _is_grounded_on_server(): diff --git a/scripts/player/spring_arm_offset.gd b/scripts/player/spring_arm_offset.gd index 4362c2c..ed7125f 100644 --- a/scripts/player/spring_arm_offset.gd +++ b/scripts/player/spring_arm_offset.gd @@ -28,6 +28,12 @@ func _ready() -> void: _pitch = _spring_arm.rotation.x if _spring_arm else 0.0 _apply_axis_lock() _apply_perspective() + + # 进入直接切换第一人称 + is_first_person = not is_first_person + _apply_perspective() + perspective_changed.emit(is_first_person) + get_viewport().set_input_as_handled() func _process(_delta: float) -> void: @@ -45,12 +51,12 @@ func _unhandled_input(event: InputEvent) -> void: if current_scene.is_camera_input_blocked(): return - if event.is_action_pressed("toggle_camera"): - is_first_person = not is_first_person - _apply_perspective() - perspective_changed.emit(is_first_person) - get_viewport().set_input_as_handled() - return + #if event.is_action_pressed("toggle_camera"): + #is_first_person = not is_first_person + #_apply_perspective() + #perspective_changed.emit(is_first_person) + #get_viewport().set_input_as_handled() + #return if not (event is InputEventMouseMotion): return diff --git a/scripts/player/voice_chat.gd b/scripts/player/voice_chat.gd new file mode 100644 index 0000000..5130ab9 --- /dev/null +++ b/scripts/player/voice_chat.gd @@ -0,0 +1,55 @@ +extends Node +## 最简近距离语音:按住 C(talk)采集麦克风 → 广播给所有对端 → +## 在说话者位置的 RaytracedAudioPlayer3D 回放。 +## 距离衰减/声像由 3D 音频完成,墙体闷音、环境混响由 raytraced_audio 插件完成。 +## 依赖:default_bus_layout.tres 的 mic 总线(静音 + AudioEffectCapture)、 +## RaytracedReverb/RaytracedAmbient 总线(插件用)、player.tscn 根节点下的 Voice 节点。 + +@export var max_distance := 25.0 + +var _capture: AudioEffectCapture +var _voice_3d: RaytracedAudioPlayer3D +var _playback: AudioStreamGeneratorPlayback + + +func _ready() -> void: + _capture = AudioServer.get_bus_effect(AudioServer.get_bus_index("mic"), 0) + _voice_3d = RaytracedAudioPlayer3D.new() + var gen := AudioStreamGenerator.new() + gen.mix_rate = 48000.0 # 与 audio/driver/mix_rate 一致,否则变速变调 + _voice_3d.stream = gen + _voice_3d.position = Vector3(0, 1.5, 0) # 嘴部高度 + _voice_3d.max_distance = max_distance + add_child(_voice_3d) + if is_multiplayer_authority() and DisplayServer.get_name() != "headless": + var mic := AudioStreamPlayer.new() + mic.stream = AudioStreamMicrophone.new() + mic.bus = "mic" + add_child(mic) + mic.play() + # 监听器只建在本地玩家身上(多个监听器会互相抢 current) + var listener := RaytracedAudioListener.new() + listener.position = Vector3(0, 1.6, 0) # 头部高度 + add_child(listener) + listener.owner = get_parent() # 插件的 ambient 计算会读 owner,代码创建的节点必须手动指定 + else: + _voice_3d.play() + _playback = _voice_3d.get_stream_playback() + + +func _process(_delta: float) -> void: + if not is_multiplayer_authority() or _capture == null: + return + if not Input.is_action_pressed("talk"): + _capture.clear_buffer() # 松开时丢弃积压,避免下次开口带旧音频 + return + var frames := _capture.get_buffer(_capture.get_frames_available()) + if not frames.is_empty(): + receive_voice.rpc(frames) + + +@rpc("any_peer", "call_remote", "unreliable_ordered", 1) +func receive_voice(frames: PackedVector2Array) -> void: + if not _playback: + _playback = _voice_3d.get_stream_playback() + _playback.push_buffer(frames) diff --git a/scripts/player/voice_chat.gd.uid b/scripts/player/voice_chat.gd.uid new file mode 100644 index 0000000..69ba5a6 --- /dev/null +++ b/scripts/player/voice_chat.gd.uid @@ -0,0 +1 @@ +uid://3i8mgirdlpli