315 lines
12 KiB
GDScript
315 lines
12 KiB
GDScript
# 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)
|