@tool class_name Carryable extends RigidBody3D ## 可抱起的物品。**只有带这个脚本(并进了 carryable 组)的东西才抱得起来** —— 这就是 ## 「仅特定物品可抱」的实现方式:门槛不在玩家那边,而在物品自己身上。 ## ## 和 Zipline 一样,手感参数放物品这一侧:不同物品可以有不同的抱持偏移、不同的投掷速度, ## 玩家只负责「找到它、把它摆到手上、扔出去」,不定义手感。 ## ## 用法:把这个脚本挂在物品根节点(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_group("投掷") ## 长按蓄满力后的出手速度(米/秒) @export var throw_speed_max: float = 14.0 ## 轻点时最低出手速度(米/秒) @export var throw_speed_min: float = 5.0 ## 谁抱着它(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 func _ready() -> void: add_to_group(GROUP) _saved_collision_layer = collision_layer _update_simulation() func can_be_carried() -> bool: return carrier_peer == 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 apply_hold_transform(socket: Node3D) -> void: var socket_basis := socket.global_basis global_position = socket.global_position + socket_basis * hold_offset global_basis = socket_basis * Basis.from_euler(hold_rotation_deg * PI / 180.0) ## 按蓄力比例算出手速度(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