Files
pixelheros/assets/script/game/skill/SkillView.ts
panw 5634b49fee fix(技能): 修复瞬时技能碰撞检测关闭时机
移除基于 pendingClose 的延迟关闭逻辑,改为在攻击帧中立即调度关闭碰撞检测。
这避免了同一帧内对同一目标造成多次伤害的问题,并简化了时间类型技能的处理逻辑。
2026-03-16 09:10:03 +08:00

201 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { _decorator, Animation, CCInteger, Collider2D, Contact2DType, UITransform, v3, Vec3 } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { HeroViewComp } from "../hero/HeroViewComp";
import { BuffsList, DTType, EType, RType, SkillConfig, SkillSet } from "../common/config/SkillSet";
import { SDataCom } from "./SDataCom";
import { Attrs } from "../common/config/HeroAttrs";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { DamageQueueHelper } from "../hero/DamageQueueComp";
import { mLogger } from "../common/Logger";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@ccclass('SkillView')
@ecs.register('SkillView', false)
export class SkillView extends CCComp {
/** 视图层逻辑代码分离演示 */
@property({ type: CCInteger })
atk_x: number = 0
@property({ type: CCInteger })
atk_y: number = 0
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = true;
anim:Animation=null;
group:number=0;
SConf:SkillConfig=null;
sData:SDataCom=null;
s_uuid:number=1001
private collider: Collider2D = null; // 缓存碰撞体引用
private pendingDisableCollider: boolean = false;
private isDisposing: boolean = false;
private attackFrameCount: number = 0; // 攻击帧计数器
private maxAttackFrames: number = 1; // 最大攻击帧数,可配置
// 已命中目标追踪,防止重复伤害
init() {
this.SConf = SkillSet[this.s_uuid]
this.sData = this.ent.get(SDataCom)
this.anim = this.node.getComponent(Animation)
this.node.active = true;
this.pendingDisableCollider = false;
this.isDisposing = false;
this.collider = this.getComponent(Collider2D);
if(this.collider) {
this.collider.group = this.group;
this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.enabled = this.SConf?.EType === EType.collision;
}
if(this.node.getComponent(Animation)){
let anim = this.node.getComponent(Animation);
mLogger.log(this.debugMode, 'SkillView', "[SkillCom]:has anim",anim)
anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this);
anim.on(Animation.EventType.FINISHED, this.onAnimationFinished, this);
// 对象池复用时,需要手动播放默认动画(因为 Play On Load 只在首次生效)
if (anim.defaultClip) {
anim.play(anim.defaultClip.name);
}
}
this.attackFrameCount = 0; // 重置攻击帧计数
}
onBeginContact (seCol: Collider2D, oCol: Collider2D) {
if (!this.sData || !this.SConf) {
mLogger.warn(this.debugMode, 'SkillView', '[SkillView] onBeginContact 缺少 sData 或 SConf忽略此次碰撞');
return;
}
if (this.isDisposing) return;
if (!this.node || !this.node.activeInHierarchy) return;
// 安全获取双方信息用于日志
const casterName = this.sData.caster?.ent?.get(HeroAttrsComp)?.hero_name ?? '未知施法者';
const casterEid = this.sData.casterEid;
const targetView = oCol.getComponent(HeroViewComp);
const targetName = targetView?.ent?.get(HeroAttrsComp)?.hero_name ?? '非英雄对象';
const targetEid = targetView?.ent?.eid ?? '未知EID';
mLogger.log(this.debugMode, 'SkillView', `[skillView] 碰撞1 [${this.sData.caster.box_group}][${casterName}][${casterEid}]的[${seCol.group}]:[${this.SConf.name}][${this.ent.eid}]碰撞了 [${oCol.group}]:[ ${targetName}][${targetEid}]`);
if (oCol.group === seCol.group) return;
if (this.pendingDisableCollider) return;
// 不是 HeroViewComp直接忽略
if (!targetView) return;
// 🔥 方案A防御性检查 - 在获取model前强制检查ent是否存在
if (!targetView.ent) {
mLogger.warn(this.debugMode, 'SkillView', '[SkillView] onBeginContact targetView.ent为空实体已销毁忽略此次碰撞');
return;
}
let model = targetView.ent.get(HeroAttrsComp);
mLogger.log(this.debugMode, 'SkillView', `[skillView] 碰撞3`, oCol.group, seCol.group, model);
if (!model) return;
if (model.is_dead) return;
if (this.sData.fac == model.fac) return;
// 检查是否已经命中过这个目标(日志安全输出)
this.apply_damage(targetView)
}
onAnimationFinished(){
if(this.SConf.EType==EType.animationEnd){
this.disable_collider_now();
this.ent.destroy()
}
}
// //动画帧事件 atk 触发
public atk(args:any){
this.attackFrameCount++;
if (this.enable_collider_safely()) {
mLogger.log(this.debugMode, 'SkillView', `[SkillView] [${this.SConf?.name}] 第${this.attackFrameCount}次攻击帧开启碰撞检测`);
this.scheduleOnce(() => {
if (!this.node || !this.node.isValid || this.isDisposing) return;
this.close_collider();
mLogger.log(this.debugMode, 'SkillView', `[SkillView] [${this.SConf?.name}] 第${this.attackFrameCount}次攻击帧关闭碰撞检测`);
}, 0);
}
}
//伤害应用
apply_damage(target:HeroViewComp,is_range:boolean=false){
if(target == null) return;
// 安全检查:如果目标实体已不存在,直接返回
if (!target.ent) return;
if (!this.SConf) return;
// 检查技能是否应该销毁
const max_hit_count=this.SConf.hit + this.sData.Attrs[Attrs.puncture]
if ( this.sData.hit_count >= max_hit_count ) {
this.close_collider()
return
}
// 安全获取名称,防止实体销毁导致的空指针异常
const casterName = this.sData.caster?.ent?.get(HeroAttrsComp)?.hero_name ?? "未知施法者";
const targetName = target.ent.get(HeroAttrsComp)?.hero_name ?? "未知目标";
mLogger.log(this.debugMode, 'SkillView', `[skillView] 伤害 [${this.group}][${casterName}][${this.sData.casterEid}]的 [${this.SConf.name}]对 [${target.box_group}][ ${targetName}][${target.ent.eid}]`);
// 使用伤害队列系统处理伤害
DamageQueueHelper.addDamageToEntity(
target.ent,
this.sData.Attrs,
this.sData.casterEid,
this.sData.s_uuid,
this.sData.ext_dmg,
this.sData.dmg_ratio,
);
// 更新技能命中次数
this.sData.hit_count++
if (
(this.SConf.DTType != DTType.range) &&
(this.SConf.EType != EType.animationEnd) &&
(this.SConf.EType != EType.timeEnd)
) {
// 修复:物理回调中不能直接销毁刚体,需延迟到下一帧
this.close_collider();
this.scheduleOnce(() => {
if (this.ent) {
this.ent.destroy();
}
}, 0);
}
}
close_collider(){
if (!this.collider) return;
if (this.pendingDisableCollider && !this.collider.enabled) return;
this.pendingDisableCollider = true;
if (this.collider.isValid) {
this.collider.enabled = false;
}
this.pendingDisableCollider = false;
}
private disable_collider_now() {
this.isDisposing = true;
this.close_collider();
}
private enable_collider_safely(): boolean {
if (!this.collider || !this.collider.isValid) return false;
if (this.isDisposing) return false;
if (!this.node || !this.node.isValid || !this.node.activeInHierarchy) return false;
this.pendingDisableCollider = false;
this.collider.group = this.group;
this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.enabled = true;
return true;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
// 清理碰撞体事件监听
if (this.collider) {
this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.enabled = false;
}
this.pendingDisableCollider = false;
this.isDisposing = false;
if (this.anim) {
this.anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this);
}
// 取消所有定时器
this.unscheduleAllCallbacks();
if (this.node && this.node.isValid) {
this.node.active = false;
}
}
}