# 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)/ CreateIndexedFly(NORMAL / # FIRE_CRACKER / AUTO_FIRE 三种服务端索引飞行) # REF/GameLib/FlyingInstance.cpp CFlyingInstance::Create → __SetDataPointer(m_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_v3Position;m_v3Velocity+=m_v3Accel*dt;m_v3Velocity.z+=m_fGravity*dt; # v3Movement=m_v3Velocity*dt;_fMoveDistance=|v3Movement|;m_fRemainRange-=_fMoveDistance; # m_v3Position+=v3Movement;m_fRemainRange<0 → OnExplodingOutOfRange + __Explode(false); # 对象目标且未命中 && square_distance_between_linesegment_and_point(pos,last,targetPos) < # m_fBombRange² → m_bTargetHitted=TRUE;m_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 return;m_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 IFlyEventHandler:OnExplodingOutOfRange / OnExplodingAtBackground / # OnExplodingAtAnotherTarget(skill,vid) / OnExplodingAtTarget(skill) extends Node # CFlyingManager::EIndexFlyType(REF/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=200(cm/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=500(cm)→ 5 m var bomb_range := 0.1 # m_fBombRange=10(cm)→ 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)