ai实现基础通话功能
This commit is contained in:
@@ -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
|
||||||
@@ -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://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="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://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://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://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="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="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="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="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"]
|
[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)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||||
shape = SubResource("CylinderShape3D_2l1sl")
|
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]
|
[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)
|
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]
|
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=1634180657]
|
||||||
replication_config = SubResource("SceneReplicationConfig_wb3a5")
|
replication_config = SubResource("SceneReplicationConfig_wb3a5")
|
||||||
|
|
||||||
|
[node name="Voice" type="Node" parent="." unique_id=44300263]
|
||||||
|
script = ExtResource("5_voice")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class_name Body
|
#class_name Body
|
||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
|
# 线性插值速度
|
||||||
const LERP_VELOCITY: float = 0.15
|
const LERP_VELOCITY: float = 0.15
|
||||||
|
|
||||||
@export_category("Objects")
|
@export_category("Objects")
|
||||||
|
|||||||
+32
-32
@@ -3,17 +3,17 @@ extends CharacterBody3D
|
|||||||
|
|
||||||
enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
||||||
|
|
||||||
const NORMAL_SPEED = 6.0
|
@export var 正常速度 = 6.0
|
||||||
const SPRINT_SPEED = 10.0
|
@export var 冲刺速度 = 10.0
|
||||||
const JUMP_VELOCITY = 7.5
|
@export var 跳跃初速度 = 7.5
|
||||||
const FALL_GRAVITY_MULTIPLIER = 1.6
|
@export var 下落重力倍率 = 1.6
|
||||||
const BASE_NICKNAME_HEIGHT := 2.0
|
@export var 昵称基准高度 := 2.0
|
||||||
const SERVER_ANIMATION_REQUESTS_PER_SECOND := 10.0
|
@export var 服务器每秒动画请求数 := 10.0
|
||||||
const SERVER_ANIMATION_REQUEST_BURST := 6.0
|
@export var 服务器动画请求突发上限 := 6.0
|
||||||
const PICKUP_ANIMATION_DELAY_MSEC := 1000
|
@export var 拾取动画延迟毫秒 := 1000
|
||||||
const PICKUP_ANIMATION_WINDOW_MSEC := 2500
|
@export var 拾取动画时间窗口毫秒 := 2500
|
||||||
const PICKUP_REQUEST_COOLDOWN_MSEC := 1000
|
@export var 拾取请求冷却毫秒 := 1000
|
||||||
const ALLOWED_ANIMATION_STATES := {
|
const 允许动画状态 := {
|
||||||
&"Idle": true,
|
&"Idle": true,
|
||||||
&"Run": true,
|
&"Run": true,
|
||||||
&"Sprint": true,
|
&"Sprint": true,
|
||||||
@@ -66,10 +66,10 @@ var _animation_sequence := 0
|
|||||||
var _last_applied_animation_sequence := 0
|
var _last_applied_animation_sequence := 0
|
||||||
var _last_requested_animation: StringName = &""
|
var _last_requested_animation: StringName = &""
|
||||||
var _appearance_sync_requesters: Dictionary = {}
|
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 _last_server_animation_token_update_msec := 0
|
||||||
var _server_pickup_animation_started_msec := -1
|
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 _pickup_area_camera_yaw_offset := 0.0
|
||||||
var _equipped_hat_visual_id := ""
|
var _equipped_hat_visual_id := ""
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ func _ready():
|
|||||||
animation_player.animation_finished.connect(_on_animation_finished)
|
animation_player.animation_finished.connect(_on_animation_finished)
|
||||||
_body.play_animation_state(&"Idle")
|
_body.play_animation_state(&"Idle")
|
||||||
nickname.visible = true
|
nickname.visible = true
|
||||||
_set_nickname_height(BASE_NICKNAME_HEIGHT)
|
_set_nickname_height(昵称基准高度)
|
||||||
call_deferred("_update_nickname_height")
|
call_deferred("_update_nickname_height")
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
call_deferred("_request_equipment_appearance")
|
call_deferred("_request_equipment_appearance")
|
||||||
@@ -194,14 +194,14 @@ func _physics_process(delta):
|
|||||||
has_double_jumped = false
|
has_double_jumped = false
|
||||||
|
|
||||||
if Input.is_action_just_pressed("jump"):
|
if Input.is_action_just_pressed("jump"):
|
||||||
velocity.y = JUMP_VELOCITY
|
velocity.y = 跳跃初速度
|
||||||
can_double_jump = true
|
can_double_jump = true
|
||||||
_request_animation(&"Jump", true)
|
_request_animation(&"Jump", true)
|
||||||
else:
|
else:
|
||||||
_apply_gravity(delta)
|
_apply_gravity(delta)
|
||||||
|
|
||||||
if can_double_jump and not has_double_jumped and Input.is_action_just_pressed("jump"):
|
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
|
has_double_jumped = true
|
||||||
can_double_jump = false
|
can_double_jump = false
|
||||||
_request_animation(&"Jump2", true)
|
_request_animation(&"Jump2", true)
|
||||||
@@ -217,7 +217,7 @@ func _physics_process(delta):
|
|||||||
func _apply_gravity(delta: float) -> void:
|
func _apply_gravity(delta: float) -> void:
|
||||||
if is_on_floor():
|
if is_on_floor():
|
||||||
return
|
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
|
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:
|
func _request_animation(state: StringName, restart: bool = false) -> void:
|
||||||
if not ALLOWED_ANIMATION_STATES.has(state):
|
if not 允许动画状态.has(state):
|
||||||
return
|
return
|
||||||
if not restart and _last_requested_animation == state:
|
if not restart and _last_requested_animation == state:
|
||||||
return
|
return
|
||||||
@@ -255,7 +255,7 @@ func _request_animation(state: StringName, restart: bool = false) -> void:
|
|||||||
func request_animation_state(state: StringName) -> void:
|
func request_animation_state(state: StringName) -> void:
|
||||||
if not multiplayer.is_server() or not _is_owner_request():
|
if not multiplayer.is_server() or not _is_owner_request():
|
||||||
return
|
return
|
||||||
if not ALLOWED_ANIMATION_STATES.has(state):
|
if not 允许动画状态.has(state):
|
||||||
return
|
return
|
||||||
if not _server_consume_animation_request_token():
|
if not _server_consume_animation_request_token():
|
||||||
return
|
return
|
||||||
@@ -275,8 +275,8 @@ func _server_consume_animation_request_token() -> bool:
|
|||||||
else:
|
else:
|
||||||
var elapsed_seconds := (now - _last_server_animation_token_update_msec) / 1000.0
|
var elapsed_seconds := (now - _last_server_animation_token_update_msec) / 1000.0
|
||||||
_server_animation_request_tokens = minf(
|
_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
|
_last_server_animation_token_update_msec = now
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ func sync_animation_state(state: StringName, sequence: int) -> void:
|
|||||||
return
|
return
|
||||||
_last_applied_animation_sequence = sequence
|
_last_applied_animation_sequence = sequence
|
||||||
|
|
||||||
if not ALLOWED_ANIMATION_STATES.has(state):
|
if not 允许动画状态.has(state):
|
||||||
return
|
return
|
||||||
|
|
||||||
# The owner already played the input-driven state immediately.
|
# The owner already played the input-driven state immediately.
|
||||||
@@ -366,9 +366,9 @@ func _move() -> void:
|
|||||||
|
|
||||||
func _is_running() -> bool:
|
func _is_running() -> bool:
|
||||||
if Input.is_action_pressed("shift"):
|
if Input.is_action_pressed("shift"):
|
||||||
_current_speed = SPRINT_SPEED
|
_current_speed = 冲刺速度
|
||||||
return true
|
return true
|
||||||
_current_speed = NORMAL_SPEED
|
_current_speed = 正常速度
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
@@ -704,12 +704,12 @@ func _update_nickname_height(hat_id: String = "") -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _calculate_nickname_height(hat_id: String) -> float:
|
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)
|
var equipped_hat := _get_hat_node(hat_id)
|
||||||
if equipped_hat:
|
if equipped_hat:
|
||||||
var hat_top := _get_visual_top(equipped_hat)
|
var hat_top := _get_visual_top(equipped_hat)
|
||||||
if hat_top > -INF and hat_top < INF:
|
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
|
return target_height
|
||||||
|
|
||||||
|
|
||||||
@@ -724,9 +724,9 @@ func apply_synced_nickname_height(height: float) -> void:
|
|||||||
return
|
return
|
||||||
nickname.visible = not _is_local_first_person()
|
nickname.visible = not _is_local_first_person()
|
||||||
if height > -INF and height < INF:
|
if height > -INF and height < INF:
|
||||||
_set_nickname_height(maxf(BASE_NICKNAME_HEIGHT, height))
|
_set_nickname_height(maxf(昵称基准高度, height))
|
||||||
else:
|
else:
|
||||||
_set_nickname_height(BASE_NICKNAME_HEIGHT)
|
_set_nickname_height(昵称基准高度)
|
||||||
|
|
||||||
|
|
||||||
func _is_local_first_person() -> bool:
|
func _is_local_first_person() -> bool:
|
||||||
@@ -734,7 +734,7 @@ func _is_local_first_person() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func get_current_nickname_height() -> float:
|
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:
|
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():
|
if not multiplayer.is_server() or not _is_owner_request():
|
||||||
return
|
return
|
||||||
var now := Time.get_ticks_msec()
|
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
|
return
|
||||||
_last_server_pickup_request_msec = now
|
_last_server_pickup_request_msec = now
|
||||||
|
|
||||||
var animation_elapsed := now - _server_pickup_animation_started_msec
|
var animation_elapsed := now - _server_pickup_animation_started_msec
|
||||||
if (
|
if (
|
||||||
_server_pickup_animation_started_msec < 0
|
_server_pickup_animation_started_msec < 0
|
||||||
or animation_elapsed < PICKUP_ANIMATION_DELAY_MSEC
|
or animation_elapsed < 拾取动画时间窗口毫秒
|
||||||
or animation_elapsed > PICKUP_ANIMATION_WINDOW_MSEC
|
or animation_elapsed > 拾取动画时间窗口毫秒
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if not _is_grounded_on_server():
|
if not _is_grounded_on_server():
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ func _ready() -> void:
|
|||||||
_apply_axis_lock()
|
_apply_axis_lock()
|
||||||
_apply_perspective()
|
_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:
|
func _process(_delta: float) -> void:
|
||||||
_apply_axis_lock()
|
_apply_axis_lock()
|
||||||
@@ -45,12 +51,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
if current_scene.is_camera_input_blocked():
|
if current_scene.is_camera_input_blocked():
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.is_action_pressed("toggle_camera"):
|
#if event.is_action_pressed("toggle_camera"):
|
||||||
is_first_person = not is_first_person
|
#is_first_person = not is_first_person
|
||||||
_apply_perspective()
|
#_apply_perspective()
|
||||||
perspective_changed.emit(is_first_person)
|
#perspective_changed.emit(is_first_person)
|
||||||
get_viewport().set_input_as_handled()
|
#get_viewport().set_input_as_handled()
|
||||||
return
|
#return
|
||||||
|
|
||||||
if not (event is InputEventMouseMotion):
|
if not (event is InputEventMouseMotion):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://3i8mgirdlpli
|
||||||
Reference in New Issue
Block a user