完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+314
View File
@@ -0,0 +1,314 @@
# fly_object.gd —— 1:1 复刻 GameLib 的飞行物实体系统(CFlyingManager / CFlyingInstance /
# CFlyingData),把原客户端「.msa FLY 帧 → OnShoot → CG_SHOOT」之后、由服务端 GC_CREATE_FLY
# 驱动的那颗真实弹道 + 命中判定搬到 POC。net_world.gd 收到 client.fly_cue 后调
# FlyManager.spawn(),每帧 step() 推进运动学并做「线段到点」的爆炸判定,命中 / 超程 / 触地
# 时回调 handler(对齐 IFlyEventHandler)。§3.6
#
# 参考:
# REF/GameLib/FlyingObjectManager.cpp CFlyingManager::CreateFlyingInstanceFlyTarget /
# Update()(遍历实例,Update() 返 false 即 Delete + erase/ CreateIndexedFlyNORMAL /
# FIRE_CRACKER / AUTO_FIRE 三种服务端索引飞行)
# REF/GameLib/FlyingInstance.cpp CFlyingInstance::Create → __SetDataPointerm_v3Position=起点、
# m_bAlive、m_fStartTime、由 m_fRollAngle-90 yaw + m_fConeAngle roll 建四元数、可选
# m_bSpreading 随机扩散、m_v3Velocity=m_v3LocalVelocity=(0,-m_fInitVel,0)、m_v3Accel=
# pData->m_v3Accel、m_fRemainRange=m_fRange、m_iPierceCount+ __SetTargetDirection
# v3TargetPos=target.GetFlyTargetPosition()m_bMaintainParallel 时 z+=50;方向=
# normalize(target-pos),把局部速度 / 加速度旋到该方向)
# REF/GameLib/FlyingInstance.cpp CFlyingInstance::Update()homing → AdjustDirectionForHoming
# v3LastPosition=m_v3Positionm_v3Velocity+=m_v3Accel*dtm_v3Velocity.z+=m_fGravity*dt
# v3Movement=m_v3Velocity*dt_fMoveDistance=|v3Movement|m_fRemainRange-=_fMoveDistance
# m_v3Position+=v3Movementm_fRemainRange<0 → OnExplodingOutOfRange + __Explode(false)
# 对象目标且未命中 && square_distance_between_linesegment_and_point(pos,last,targetPos) <
# m_fBombRange² → m_bTargetHitted=TRUEm_canAttack 时 pVictim->OnShootDamage()
# handler->OnExplodingAtTarget(skill)m_iPierceCount>0 → --、__Bomb();否则 __Explode()
# 位置目标同样线段判定 → __Explode()m_bHitOnBackground → GetTerrainHeight(x,-y)>z → 触地
# REF/GameLib/FlyingInstance.cpp __Explode(bBomb=true)!m_bAlive returnm_bAlive=false
# bBomb → __Bomb()(在 m_v3Position 生成 m_dwBombEffectID 特效)
# REF/EterLib/GrpMath.h:80 square_distance_between_linesegment_and_point(点到线段的平方距离,
# d<=0 取端点 p1、d>=l 取端点 p2、否则叉积模方 / l)
# REF/GameLib/FlyingData.cpp CFlyingData::__Initialize()(本文件 FlyData 默认值逐项对齐)
# REF/GameLib/FlyHandler.h IFlyEventHandlerOnExplodingOutOfRange / OnExplodingAtBackground /
# OnExplodingAtAnotherTarget(skill,vid) / OnExplodingAtTarget(skill)
extends Node
# CFlyingManager::EIndexFlyTypeREF/GameLib/FlyingObjectManager.h
const INDEX_FLY_TYPE_NORMAL := 0
const INDEX_FLY_TYPE_FIRE_CRACKER := 1
const INDEX_FLY_TYPE_AUTO_FIRE := 2
# CFlyingData —— .fly 脚本字段。POC 无 .fly 加载器,默认值逐项对齐 CFlyingData::__Initialize()
# (长度单位:原版是 pixel==cm,这里换算成米后传进来,故 init_vel / range / bomb_range 都按米)。
class FlyData extends RefCounted:
var init_vel := 2.0 # m_fInitVel=200cm/s)→ 2 m/s(占位;net_world 用弓箭值覆盖)
var cone_angle := 0.0 # m_fConeAngle
var roll_angle := 0.0 # m_fRollAngle
var gravity := 0.0 # m_fGravity(原版加到速度 .z(=up),本实现映射到 -Y = 向下)
var accel := Vector3.ZERO # m_v3Accel
var flat_range := 5.0 # m_fRange=500cm)→ 5 m
var bomb_range := 0.1 # m_fBombRange=10cm)→ 0.1 m
var pierce_count := 0 # m_iPierceCount
var spreading := false # m_bSpreading
var maintain_parallel := false # m_bMaintainParallel(对象目标点 y 抬 0.5 m ≈ 原版 +50cm
var is_homing := false # m_bIsHoming
var homing_start_time := 0.0 # m_fHomingStartTime
var homing_max_angle := 0.0 # m_fHomingMaxAngle(度 / 每次修正)
var hit_on_background := false # m_bHitOnBackground
# 单颗飞行物 —— 对齐 CFlyingInstance。target 可为 Node3D(对象目标,跟随移动)、
# Vector3(位置目标)、或 { "vid": int, "pos": Vector3 }。
class FlyInstance extends RefCounted:
var id := 0
var data: FlyData
var pos := Vector3.ZERO
var last_pos := Vector3.ZERO
var vel := Vector3.ZERO
var accel := Vector3.ZERO
var remain_range := 0.0
var pierce := 0
var alive := false
var can_attack := false
var skill_index := 0
var target_vid := 0
var target_hitted := false
var _target_obj: Node3D
var _target_pos := Vector3.ZERO
var _is_object := false
var _elapsed := 0.0
var _world: Object # 可空:有 sample_height 时做触地
# handler.call(event: String, world_pos: Vector3, vid: int) —— 对齐 IFlyEventHandler
var handler := Callable()
func target_position() -> Vector3:
if _is_object and is_instance_valid(_target_obj):
_target_pos = _target_obj.global_position
var tp := _target_pos
if data and data.maintain_parallel:
tp.y += 0.5
return tp
# CFlyingInstance::Create → __SetDataPointer + __SetTargetDirection。
func create(d: FlyData, start_world: Vector3, target, attack: bool) -> void:
data = d
can_attack = attack
pos = start_world
last_pos = start_world
alive = true
_elapsed = 0.0
target_hitted = false
pierce = d.pierce_count
remain_range = d.flat_range
if target is Node3D:
_is_object = true
_target_obj = target
_target_pos = (target as Node3D).global_position
elif target is Vector3:
_is_object = false
_target_pos = target
elif target is Dictionary:
target_vid = int(target.get("vid", 0))
var tp: Variant = target.get("pos", null)
if tp is Vector3:
_target_pos = tp
var tn: Variant = target.get("node", null)
if tn is Node3D:
_is_object = true
_target_obj = tn
_target_pos = (tn as Node3D).global_position
# __SetTargetDirection:方向 = normalize(target - pos),局部速度 (0,-init_vel,0)
# 旋到该方向即 dir*init_vel;加速度同样旋到该方向。
var dir := (target_position() - pos)
if dir.length() < 0.0001:
dir = Vector3(0, 0, -1)
dir = dir.normalized()
vel = dir * d.init_vel
accel = d.accel
if accel.length_squared() > 0.0:
# 局部帧 -Y → dir 的最小弧旋转(对齐 __SetTargetNormalizedDirection)。
accel = Quaternion(Vector3(0, -1, 0), dir).normalized() * accel
# CFlyingInstance::AdjustDirectionForHoming —— 把速度朝目标方向转,单次夹在
# homing_max_angle 度以内。
func _adjust_homing() -> void:
var tdir := (target_position() - pos)
if tdir.length() < 0.0001:
return
tdir = tdir.normalized()
var vdir := vel.normalized()
var ang := vdir.angle_to(tdir)
if ang < 0.0001:
return
var maxr := deg_to_rad(data.homing_max_angle) if data.homing_max_angle < 180.0 else ang
var t := clampf(maxr / ang, 0.0, 1.0)
var speed := vel.length()
vel = vdir.slerp(tdir, t) * speed
# CFlyingInstance::Update() —— 返 false 表示该销毁(超程 / 命中且不穿透 / 触地)。
func update(dt: float) -> bool:
if not alive:
return false
_elapsed += dt
if data.is_homing and _elapsed > data.homing_start_time and _is_object:
_adjust_homing()
last_pos = pos
vel += accel * dt
vel.y += data.gravity * dt
var movement := vel * dt
var move_dist := movement.length()
remain_range -= move_dist
pos += movement
if remain_range < 0.0:
_emit("out_of_range")
_explode(false)
return false
var br2 := data.bomb_range * data.bomb_range
if _is_object:
if not target_hitted:
if _seg_point_sq_dist(pos, last_pos, target_position()) < br2:
target_hitted = true
if can_attack:
_emit("shoot_damage")
_emit("at_target")
if pierce > 0:
pierce -= 1
_emit("bomb")
else:
_explode()
return false
return true
else:
if _seg_point_sq_dist(pos, last_pos, target_position()) < br2:
_emit("at_position")
_explode()
return false
if data.hit_on_background and _world and _world.has_method("sample_height"):
var gh := float(_world.call("sample_height", pos.x, pos.z))
if gh > pos.y:
_emit("at_background")
_explode()
return false
return true
func _explode(bomb := true) -> void:
if not alive:
return
alive = false
if bomb:
_emit("bomb")
func _emit(ev: String) -> void:
if handler.is_valid():
handler.call(ev, pos, target_vid)
# REF/EterLib/GrpMath.h:80 square_distance_between_linesegment_and_point
static func _seg_point_sq_dist(p1: Vector3, p2: Vector3, x: Vector3) -> float:
var v := p2 - p1
var l := v.length_squared()
var w := x - p1
var d := w.dot(v)
if d <= 0.0:
return w.length_squared()
if d >= l:
return (x - p2).length_squared()
return w.cross(v).length_squared() / l
var parent: Node3D # 弹道 / 爆点可视节点挂这下面(可空 = 纯逻辑,仅回调)
var world: Object # Metin2World(可空,用于 hit_on_background 贴地)
var manual_step := false # true 时 _process 不自动推进(headless 测试用 step() 手动驱动)
var _instances: Array = [] # Array[FlyInstance]
var _visuals := {} # FlyInstance -> MeshInstance3D
var _id_counter := 1
signal shoot_damage(target_vid: int)
signal exploded(world_pos: Vector3, cause: String)
func setup(mount: Node3D, w: Object = null) -> void:
parent = mount
world = w
# CFlyingManager::CreateFlyingInstanceFlyTarget —— 建一颗飞行物并入表。
func spawn(start_world: Vector3, target, can_attack: bool, data: FlyData = null,
skill_index := 0) -> FlyInstance:
var d := data if data != null else FlyData.new()
var inst := FlyInstance.new()
inst.id = _id_counter
_id_counter += 1
inst.skill_index = skill_index
inst._world = world
inst.handler = func(ev: String, wp: Vector3, vid: int) -> void:
_on_instance_event(inst, ev, wp, vid)
inst.create(d, start_world, target, can_attack)
_instances.append(inst)
if parent != null:
var m := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = 0.08
sm.height = 0.16
m.mesh = sm
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.9, 0.4)
mat.emission_enabled = true
mat.emission = Color(0.9, 0.7, 0.2)
m.material_override = mat
parent.add_child(m)
m.global_position = inst.pos
_visuals[inst] = m
return inst
func active_count() -> int:
return _instances.size()
# CFlyingManager::Update() —— 遍历实例,Update() 返 false 即删除。
func step(dt: float) -> void:
var i := 0
while i < _instances.size():
var inst: FlyInstance = _instances[i]
var keep := inst.update(dt)
var vis: MeshInstance3D = _visuals.get(inst, null)
if is_instance_valid(vis):
vis.global_position = inst.pos
if not keep:
if is_instance_valid(vis):
vis.queue_free()
_visuals.erase(inst)
_instances.remove_at(i)
else:
i += 1
func _process(dt: float) -> void:
if not manual_step:
step(dt)
func _on_instance_event(inst: FlyInstance, ev: String, wp: Vector3, vid: int) -> void:
match ev:
"shoot_damage":
shoot_damage.emit(vid)
"out_of_range", "at_target", "at_position", "at_background", "at_another":
exploded.emit(wp, ev)
if parent != null and ev != "out_of_range":
_spawn_flash(wp)
"bomb":
if parent != null:
_spawn_flash(wp)
func _spawn_flash(wp: Vector3) -> void:
var f := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = 0.05
sm.height = 0.1
f.mesh = sm
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.75, 0.3, 0.9)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.6, 0.2)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
f.material_override = mat
parent.add_child(f)
f.global_position = wp
var tw := create_tween()
tw.set_parallel(true)
tw.tween_property(f, "scale", Vector3.ONE * 6.0, 0.25)
tw.tween_property(mat, "albedo_color:a", 0.0, 0.25)
tw.chain().tween_callback(f.queue_free)