拾起物品同步
This commit is contained in:
+66
-22
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
+12
-12
@@ -86,22 +86,22 @@ func _process(_delta: float) -> void:
|
||||
# ---------------- 对外接口(玩家控制器只读这几个) ----------------
|
||||
|
||||
## 缆上参数 t∈[0,1] 处的世界坐标。
|
||||
## 曲线先在**本地空间**算完再转世界:sag 是本地单位,父节点一有缩放整条缆要跟着一起缩,
|
||||
## 形状才不走样(混用 global 锚点 + 未缩放的 sag,下垂比例会随父节点缩放而变)。
|
||||
func sample(t: float) -> Vector3:
|
||||
var a := anchor_a.global_position
|
||||
var b := anchor_b.global_position
|
||||
return a.lerp(b, t) - Vector3.UP * (sag * 4.0 * t * (1.0 - t))
|
||||
return to_global(_curve_local(t))
|
||||
|
||||
|
||||
## 缆上参数 t 处的世界方向(单位向量,指向 B)。
|
||||
## 用解析导数而不是前后差分:差分在 t=0/1 处要越界取样,端点会偏。
|
||||
func tangent(t: float) -> Vector3:
|
||||
var d := (anchor_b.global_position - anchor_a.global_position) - Vector3.UP * (sag * 4.0 * (1.0 - 2.0 * t))
|
||||
return d.normalized()
|
||||
var d := (anchor_b.position - anchor_a.position) - Vector3.UP * (sag * 4.0 * (1.0 - 2.0 * t))
|
||||
return (global_transform.basis * d).normalized()
|
||||
|
||||
|
||||
## 乘坐时人应该待的世界坐标:缆点再往下挂 hang_offset。
|
||||
func ride_point(t: float) -> Vector3:
|
||||
return sample(t) - Vector3.UP * hang_offset
|
||||
return to_global(_curve_local(t) - Vector3.UP * hang_offset)
|
||||
|
||||
|
||||
## 找出世界坐标 pos 在缆上最近的一点,供玩家「就近吸附」。
|
||||
@@ -134,9 +134,9 @@ func _downhill_dir() -> int:
|
||||
return 1 if anchor_b.global_position.y <= anchor_a.global_position.y else -1
|
||||
|
||||
|
||||
## 世界曲线转成本地坐标:缆绳网格是本节点的子节点,顶点必须落在局部空间里。
|
||||
func _local_sample(t: float) -> Vector3:
|
||||
return to_local(sample(t))
|
||||
## 下垂曲线在**本节点局部空间**的点。sag 是本地单位,所以父节点缩放时整条缆一起缩、形状不变。
|
||||
func _curve_local(t: float) -> Vector3:
|
||||
return anchor_a.position.lerp(anchor_b.position, t) - Vector3.UP * (sag * 4.0 * t * (1.0 - t))
|
||||
|
||||
|
||||
## 把下垂曲线扫成一根管子。用三角形带:每个截面 RING_SEGMENTS+1 个顶点,
|
||||
@@ -146,17 +146,17 @@ func _rebuild_cable() -> void:
|
||||
return
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
|
||||
var prev := _local_sample(0.0)
|
||||
var prev := _curve_local(0.0)
|
||||
length = 0.0
|
||||
for i in range(segments + 1):
|
||||
var t := float(i) / float(segments)
|
||||
var p := _local_sample(t)
|
||||
var p := _curve_local(t)
|
||||
if i > 0:
|
||||
length += p.distance_to(prev)
|
||||
prev = p
|
||||
# 截面参考轴取「切线 × 世界上方向」。切线接近竖直时叉乘会退化成零向量,
|
||||
# 这时换 RIGHT 兜底,否则 normalize() 会吐 NaN 把整根缆搅烂。
|
||||
var tan_dir := (_local_sample(minf(t + 0.01, 1.0)) - _local_sample(maxf(t - 0.01, 0.0))).normalized()
|
||||
var tan_dir := (_curve_local(minf(t + 0.01, 1.0)) - _curve_local(maxf(t - 0.01, 0.0))).normalized()
|
||||
var n := tan_dir.cross(Vector3.UP)
|
||||
if n.length_squared() < 0.0001:
|
||||
n = tan_dir.cross(Vector3.RIGHT)
|
||||
|
||||
Reference in New Issue
Block a user