ai实现基础通话功能

This commit is contained in:
2026-09-13 02:09:22 +08:00
parent d48c4f06b7
commit 1bd54e425e
7 changed files with 156 additions and 39 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
class_name Body
#class_name Body
extends Node3D
# 线性插值速度
const LERP_VELOCITY: float = 0.15
@export_category("Objects")
+32 -32
View File
@@ -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():
+12 -6
View File
@@ -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
+55
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://3i8mgirdlpli