拾起物品同步

This commit is contained in:
2026-10-03 18:48:04 +08:00
parent 0dcfb88bfe
commit 3d23804fa5
10 changed files with 359 additions and 86 deletions
+66 -22
View File
@@ -7,22 +7,30 @@ extends RigidBody3D
## 和 Zipline 一样,手感参数放物品这一侧:不同物品可以有不同的抱持偏移、不同的投掷速度,
## 玩家只负责「找到它、把它摆到手上、扔出去」,不定义手感。
##
## 用法:把这个脚本挂在物品根节点(RigidBody3D)上即可,别的什么都不用配。
## 用法:把这个脚本挂在物品根节点(RigidBody3D)上,再给它加一个 MultiplayerSynchronizer
## (配置见 res://world/items/carryable_sync.tres,同步 position / rotation / carrier_peer)。
## 参考示例见 res://world/items/carryable_box.tscn。
##
## 联机只同步一个整数:`carrier_peer`(谁抱着)。
## **抱着的时候位置根本不走同步** —— 每一端都从「持有者的抱持点」把物品摆出来,
## 结果天然一致,也省掉了「持有者写位置、同步又把位置写回来」的互相打架。
## 空着的时候才靠 position / rotation 同步,而且只有服务器在模拟。
##
## 标了 @tool 是为了能在编辑器里跑测试(非 @tool 脚本在编辑器里是 placeholder,方法调不动);
## 运行时行为完全一样。
## 所有可抱物品都进这个组,玩家靠它找物品
const GROUP: StringName = &"carryable"
## 玩家节点在世界里以 peer id 命名并进了这个组,靠它找持有者的抱持点
const PLAYER_GROUP: StringName = &"net_player"
## 抱持点在玩家场景里的相对路径
const SOCKET_PATH: String = "CameraRig/CarrySocket"
@export_group("抱持")
## 被抱时相对抱持点的偏移(米)。物品大小不同,靠它对准手心
@export var hold_offset: Vector3 = Vector3.ZERO
## 被抱时的朝向(度)
@export var hold_rotation_deg: Vector3 = Vector3.ZERO
## 抱起来时冻结物理、并清掉碰撞层 —— 不清的话物品会把抱着它的人顶飞
@export var freeze_while_carried: bool = true
@export_group("投掷")
## 长按蓄满力后的出手速度(米/秒)
@@ -30,8 +38,14 @@ const GROUP: StringName = &"carryable"
## 轻点时最低出手速度(米/秒)
@export var throw_speed_min: float = 5.0
## 现在被谁抱着。空 = 没人抱
var carried_by: Node3D = null
## 谁抱着它(peer id),0 = 没人抱。
## 由服务器改,通过物品自己的 MultiplayerSynchronizer 复制到各端;各端只读。
var carrier_peer: int = 0:
set(value):
if carrier_peer == value:
return
carrier_peer = value
_update_simulation()
var _saved_collision_layer: int = 1
@@ -39,30 +53,23 @@ var _saved_collision_layer: int = 1
func _ready() -> void:
add_to_group(GROUP)
_saved_collision_layer = collision_layer
_update_simulation()
func can_be_carried() -> bool:
return carried_by == null
return carrier_peer == 0
## 被抱起来:冻结物理、退出碰撞,之后由持有者每帧摆位置
func begin_carry(holder: Node3D) -> void:
carried_by = holder
if freeze_while_carried:
freeze = true
collision_layer = 0
## 抱着的时候跟着持有者的抱持点走;空着的时候什么都不做(交给物理 / 同步)
func _physics_process(_delta: float) -> void:
if carrier_peer == 0:
return
var socket := _find_carrier_socket()
if socket != null:
apply_hold_transform(socket)
## 放下 / 扔出:恢复物理并继承一个初速度
func end_carry(velocity: Vector3) -> void:
carried_by = null
if freeze_while_carried:
freeze = false
collision_layer = _saved_collision_layer
linear_velocity = velocity
## 把自己摆到抱持点上。持有者每物理帧调一次 —— 偏移和朝向由物品自己决定,玩家不用管。
## 把自己摆到抱持点上。偏移和朝向由物品自己决定,玩家不用管。
func apply_hold_transform(socket: Node3D) -> void:
var socket_basis := socket.global_basis
global_position = socket.global_position + socket_basis * hold_offset
@@ -72,3 +79,40 @@ func apply_hold_transform(socket: Node3D) -> void:
## 按蓄力比例算出手速度(0 = 轻点,1 = 蓄满)
func throw_speed(charge: float) -> float:
return lerpf(throw_speed_min, throw_speed_max, clampf(charge, 0.0, 1.0))
## 请求抱起。客户端用 rpc_id(1, ...) 发给服务器仲裁(先到先得),单机时服务器直接执行。
## 服务器只改 `carrier_peer` 这一个值,其余交给同步 —— 各端看到的状态因此必然一致。
@rpc("any_peer", "call_remote", "reliable")
func request_pickup(peer_id: int) -> void:
if not multiplayer.is_server() or carrier_peer != 0:
return
carrier_peer = peer_id
## 放下 / 扔出。持有者算好初速度发给服务器:服务器收回模拟权,让物品带着这个速度接着飞。
@rpc("any_peer", "call_remote", "reliable")
func request_release(velocity: Vector3) -> void:
if not multiplayer.is_server() or carrier_peer == 0:
return
carrier_peer = 0
linear_velocity = velocity
## 抱着时冻结物理、清掉碰撞层(不清会把抱着它的人顶飞);
## 空着时只有服务器模拟,其余端冻住跟着同步过来的位置走,免得各端各算一套。
func _update_simulation() -> void:
if carrier_peer != 0:
freeze = true
collision_layer = 0
return
collision_layer = _saved_collision_layer
freeze = not multiplayer.is_server()
## 找持有者的抱持点。找不到就什么都不做 —— 远端玩家还没生成时会出现这种情况。
func _find_carrier_socket() -> Node3D:
for node in get_tree().get_nodes_in_group(PLAYER_GROUP):
if str(node.name) == str(carrier_peer):
return node.get_node_or_null(SOCKET_PATH) as Node3D
return null
+5 -1
View File
@@ -1,6 +1,7 @@
[gd_scene load_steps=5 format=3]
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://world/items/carryable.gd" id="1_carryable"]
[ext_resource type="SceneReplicationConfig" path="res://world/items/carryable_sync.tres" id="2_sync"]
[sub_resource type="BoxMesh" id="BoxMesh_body"]
size = Vector3(0.6, 0.6, 0.6)
@@ -22,3 +23,6 @@ surface_material_override/0 = SubResource("StandardMaterial3D_box")
[node name="Collision" type="CollisionShape3D" parent="."]
shape = SubResource("BoxShape3D_body")
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
replication_config = ExtResource("2_sync")
+12
View File
@@ -0,0 +1,12 @@
[gd_resource type="SceneReplicationConfig" format=3]
[resource]
properties/0/path = NodePath(".:position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:rotation")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath(".:carrier_peer")
properties/2/spawn = true
properties/2/replication_mode = 1