feat(skill): 重构技能系统,新增技能数据组件和移动组件

refactor(skill): 移除旧技能组件和文档,优化技能配置结构

fix(skill): 修正技能预制体配置错误,统一技能运行类型字段

docs(skill): 删除过时的技能系统说明文档

perf(skill): 优化技能加载逻辑,减少资源消耗

style(skill): 调整代码格式,提高可读性
This commit is contained in:
2025-10-31 00:35:51 +08:00
parent 6db004a99f
commit 2f19433a0a
27 changed files with 1141 additions and 679 deletions

View File

@@ -1,24 +1,30 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { BoxSet } from "../common/config/BoxSet";
/** 业务层对象 */
@ecs.register('SkillCom')
export class SkillComComp extends ecs.Comp {
@ecs.register('SDataCom')
export class SDataCom extends ecs.Comp {
/** 业务层组件移除时,重置所有数据为默认值 */
attrs:any=null
group:BoxSet=BoxSet.HERO
s_uuid:number=0
reset() {
this.attrs=null
this.group=0
this.s_uuid=0
}
}
/** 业务层业务逻辑处理对象 */
export class SkillComSystem extends ecs.ComblockSystem implements ecs.IEntityEnterSystem {
export class SDataComSystem extends ecs.ComblockSystem implements ecs.IEntityEnterSystem {
filter(): ecs.IMatcher {
return ecs.allOf(SkillComComp);
return ecs.allOf(SDataCom);
}
entityEnter(e: ecs.Entity): void {
// 注:自定义业务逻辑
e.remove(SkillComComp);
e.remove(SDataCom);
}
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4338992d-a768-4089-b1d2-dd8695712fc4",
"uuid": "6ded70d5-55f5-48b5-b48b-8883b26d1169",
"files": [],
"subMetas": {},
"userData": {}

View File

@@ -1,34 +1,95 @@
import { instantiate, Node, Prefab, v3, Vec3 } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { Hero } from "../hero/Hero";
import { Monster } from "../hero/Mon";
import { ECSEntity } from "db://oops-framework/libs/ecs/ECSEntity";
import { SkillSet } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { AtkConCom } from "./AtkConCom";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { BoxSet, FacSet } from "../common/config/BoxSet";
import { HType } from "../common/config/heroSet";
import { SkillView } from "./SkillView";
import { SDataCom } from "./SDataCom";
import { Attrs } from "../common/config/HeroAttrs";
import { SMoveDataComp } from "../hero/SMoveComp";
/** Skill 模块 */
@ecs.register(`Skill`)
export class Skill extends ecs.Entity {
/** ---------- 数据层 ---------- */
// SkillModel!: SkillModelComp;
SDataCom!: SDataCom;
SMoveCom!: SMoveDataComp
/** ---------- 业务层 ---------- */
// SkillBll!: SkillBllComp;
/** ---------- 视图层 ---------- */
// SkillView!: SkillViewComp;
SView!: SkillView;
/** 实始添加的数据层组件 */
protected init() {
// this.addComponents<ecs.Comp>();
this.addComponents<SDataCom>();
this.addComponents<SMoveDataComp>();
}
load(startPos: Vec3, parent: Node, uuid: number, targetPos: Vec3,casterAttrs:Attrs[]=[],scale:number=1,fac:FacSet=FacSet.MON,type:HType=HType.warrior,box_group:BoxSet=BoxSet.HERO) {
const config = SkillSet[uuid];
if (!config) {
console.error("[Skill] 技能配置不存在:", uuid);
return;
}
// 加载预制体
const path = `game/skill/atk/${config.sp_name}`;
const prefab:Prefab = oops.res.get(path, Prefab);
if (!prefab) {
console.error("[Skill] 预制体加载失败:", path);
return;
}
// console.log("load skill startPos",startPos)
const node: Node = instantiate(prefab);
console.log("load skill node",node)
node.parent = parent;
// 设置节点属性
node.setPosition(startPos);
if(fac==FacSet.MON){
node.scale=v3(node.scale.x*-1,1,1)
}else{
if(type==HType.warrior){
if(scale<0){
node.scale=v3(node.scale.x*-1,node.scale.y,1)
}
}
}
// 添加技能组件
const SView = node.getComponent(SkillView); // 初始化技能参数
// 只设置必要的运行时属性,配置信息通过 SkillSet[uuid] 访问
// 核心标识
SView.s_uuid= uuid
SView.group= box_group
this.add(SView);
const sDataCom = this.get(SDataCom);
const sMoveCom = this.get(SMoveDataComp);
sMoveCom.startPos=startPos
sMoveCom.targetPos=targetPos
sMoveCom.s_uuid=uuid
sDataCom.group=box_group
sDataCom.attrs=casterAttrs
sDataCom.s_uuid=uuid
}
/** 模块资源释放 */
destroy() {
// 注: 自定义释放逻辑,视图层实现 ecs.IComp 接口的 ecs 组件需要手动释放
this.remove(SDataCom);
this.remove(SkillView)
super.destroy();
}
}
/** Skill 模块业务逻辑系统组件,如无业务逻辑处理可删除此对象 */
export class EcsSkillSystem extends ecs.System {
constructor() {
super();
// this.add(new ecs.ComblockSystem());
}
}

View File

@@ -1,27 +1,105 @@
import { _decorator } 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 { _decorator, Animation, Collider2D, Contact2DType, 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 { DTType, RType, SkillSet } from "../common/config/SkillSet";
import { BezierMove } from "../BezierMove/BezierMove";
import { BoxSet } from "../common/config/BoxSet";
import { SDataCom } from "./SDataCom";
import { SMoveDataComp } from "../hero/SMoveComp";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@ccclass('SkillViewComp')
@ecs.register('SkillView', false)
export class SkillViewComp extends CCComp {
export class SkillView extends CCComp {
/** 视图层逻辑代码分离演示 */
anim:Animation=null;
group:number=0;
SConf:any=null;
s_uuid:number=1001
start() {
// var entity = this.ent as ecs.Entity; // ecs.Entity 可转为当前模块的具体实体对象
// this.on(ModuleEvent.Cmd, this.onHandler, this);
this.SConf = SkillSet[this.s_uuid]
this.anim=this.node.getComponent(Animation)
this.node.active = true;
let collider = this.getComponent(Collider2D);
if(collider) {
collider.group = this.group;
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
}
const SMove=this.ent.get(SMoveDataComp)
switch(this.SConf.RType){
case RType.linear:
this.do_linear(SMove.startPos,SMove.targetPos)
break
case RType.bezier:
this.do_bezier(SMove.startPos,SMove.targetPos)
break
case RType.fixed:
this.do_fixedStart(SMove.startPos,SMove.targetPos)
break
case RType.fixedEnd:
this.do_fixedEnd(SMove.startPos,SMove.targetPos)
break
}
/** 全局消息逻辑处理 */
// private onHandler(event: string, args: any) {
// switch (event) {
// case ModuleEvent.Cmd:
// break;
// }
// }
}
onBeginContact (seCol: Collider2D, oCol: Collider2D) {
// console.log(this.scale+"碰撞开始 ",seCol,oCol);
if(seCol.node.position.x-oCol.node.position.x > 100 ) return
let target = oCol.getComponent(HeroViewComp)
if(oCol.group!=this.group){
if(target == null) return;
if (!this.SConf) return;
}
}
do_bezier(startPos:Vec3,targetPos:Vec3){
let bm=this.node.getComponent(BezierMove)
this.node.angle +=10
// bm.speed=700
if(this.group==BoxSet.MONSTER) {bm.controlPointSide=-1 }
bm.rotationSmoothness=0.6
bm.moveTo(targetPos)
}
do_linear(startPos:Vec3,targetPos:Vec3){
let bm=this.node.getComponent(BezierMove)
let s_x=startPos.x
let s_y=startPos.y
let t_x=targetPos.x
let t_y=targetPos.y
// 设定目标x
targetPos.x = 400;
if(this.group == BoxSet.MONSTER) {
bm.controlPointSide = -1;
targetPos.x = -400;
}
// 计算斜率
const k = (t_y - s_y) / (t_x - s_x);
// 按直线公式计算新的y
targetPos.y = k * (targetPos.x - s_x) + s_y;
bm.controlPointOffset=0
bm.rotationSmoothness=0.6
bm.moveTo(targetPos);
}
do_fixedEnd(startPos:Vec3,targetPos:Vec3){
this.node.setPosition(targetPos.x > 360?300:targetPos.x,this.node.position.y,0)
this.do_anim()
}
do_fixedStart(startPos:Vec3,targetPos:Vec3){
this.node.setPosition(startPos.x > 360?300:startPos.x,this.node.position.y,0)
this.do_anim()
}
do_anim(){
if(this.node.getComponent(Animation)){
let anim = this.node.getComponent(Animation);
//console.log("[SkillCom]:has anim",anim)
anim.on(Animation.EventType.FINISHED, this.onAnimationFinished, this);
}
}
onAnimationFinished(){
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.node.destroy();

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e8a3bd61-1102-4fb8-8eca-c795cad7ef52",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "d3d7bbfc-9c24-4551-8bb5-7a40d7c271cd",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,189 +0,0 @@
# 新技能系统 - 快速开始
## 🚀 3步开始使用
### **步骤 1注册系统到 Main.ts**
```typescript
// Main.ts
import { SkillCastSystem, SkillCDSystem, SkillAutocastSystem } from './game/hero/HSkillSystem';
protected async initEcsSystem() {
// 技能系统(按顺序)
oops.ecs.add(new SkillCDSystem()); // CD更新
oops.ecs.add(new SkillAutocastSystem()); // 自动施法
oops.ecs.add(new SkillCastSystem()); // 施法执行
}
```
---
### **步骤 2角色已自动拥有技能**
`Hero.ts``Mon.ts` 已自动添加 `HeroSkillsComp`
✅ 加载角色时自动初始化技能
✅ 无需手动配置
```typescript
// Hero.ts - load() 方法已自动处理
skillsComp.initSkills(hero.skills); // ✅ 已完成
```
---
### **步骤 3技能自动施放**
**无需额外代码**,系统会自动处理:
```typescript
// 每帧自动运行:
// 1. SkillCDSystem 更新CD
// 2. SkillAutocastSystem 检测可施放技能
// ├─ CD好了
// ├─ MP够
// ├─ 正在攻击? ✅
// └─ 添加 CastSkillRequestComp 标记
// 3. SkillCastSystem 执行施法
// ├─ 检查条件
// ├─ 扣除MP
// ├─ 重置CD
// ├─ 播放动画
// └─ 创建技能实体
```
---
## 🎮 进阶使用
### **手动施法(玩家点击技能按钮)**
```typescript
// UI 按钮点击事件
onSkillButton1Clicked() {
const skillCon = this.heroNode.getComponent(SkillConComp);
skillCon.manualCastSkill(0); // 施放第0个技能
}
onSkillButton2Clicked() {
const skillCon = this.heroNode.getComponent(SkillConComp);
skillCon.manualCastSkill(1); // 施放第1个技能
}
```
---
### **强制施法(天赋触发、事件触发)**
```typescript
// 天赋系统
doTalentEffect(heroEntity: ecs.Entity) {
const request = heroEntity.add(CastSkillRequestComp);
request.skillIndex = 2; // 施放第2个技能
request.targetPositions = [v3(200, 0, 0)];
}
// 事件触发(如复仇:受伤时施放技能)
onDamaged(heroEntity: ecs.Entity) {
const request = heroEntity.add(CastSkillRequestComp);
request.skillIndex = 0;
request.targetPositions = this.selectEnemies();
}
```
---
### **查询技能状态**
```typescript
const hero = ecs.getEntity<Hero>(Hero);
const skillsComp = hero.get(HeroSkillsComp);
// 检查技能是否就绪
if (skillsComp.canCast(0, heroModel.mp)) {
console.log("技能1可以施放");
}
// 获取所有就绪技能
const readySkills = skillsComp.getReadySkills(heroModel.mp);
console.log(`可施放技能数量: ${readySkills.length}`);
// 获取技能CD
const skill0 = skillsComp.getSkill(0);
console.log(`技能1剩余CD: ${skill0.cd.toFixed(2)}秒`);
```
---
## 🔧 禁用自动施法
如果只想手动控制技能,注释掉自动施法系统:
```typescript
protected async initEcsSystem() {
oops.ecs.add(new SkillCDSystem());
// oops.ecs.add(new SkillAutocastSystem()); // ❌ 禁用自动施法
oops.ecs.add(new SkillCastSystem());
}
```
---
## 🎯 与原系统对比
| 指标 | 旧系统SkillConComp.update | 新系统HSkillSystem |
|------|------------------------------|----------------------|
| **职责** | CD更新 + 施法判定 + 执行 | 3个独立系统 |
| **扩展性** | 低(所有逻辑耦合) | 高(系统独立) |
| **代码位置** | 分散在 View 层 | 集中在数据/业务层 |
| **测试** | 难(依赖 View | 易(独立系统) |
| **手动施法** | 需额外实现 | 标记组件即可 |
| **ECS 规范** | 不符合 | ✅ 完全符合 |
---
## 📊 架构图
```
玩家实体Hero/Monster
├── HeroAttrsComp属性hp, mp, 状态)
├── HeroSkillsComp技能skills[], CD管理⭐ 新增
├── HeroViewComp视图动画、UI
└── BattleMoveComp移动
技能系统HSkillSystem
├── SkillCDSystem ─────────→ HeroSkillsComp
│ └─ 每帧更新CD
├── SkillAutocastSystem ───→ HeroSkillsComp + HeroAttrsComp
│ └─ AI决策施法
└── SkillCastSystem ───────→ 监听 CastSkillRequestComp
├─ 检查条件
├─ 扣除MP
├─ 重置CD
└─ 创建技能实体
```
---
## ✅ 验证清单
运行游戏后检查:
- [ ] 角色加载后拥有技能(查看 HeroSkillsComp.skills
- [ ] 技能CD自动递减观察 skill.cd 变化)
- [ ] 攻击时自动施放技能(观察技能特效)
- [ ] 施放后MP减少、CD重置
- [ ] 控制状态(眩晕/冰冻)时不施放技能
---
## 🎉 完成!
**新技能系统已完全集成到项目中!**
✅ 无需修改原有战斗逻辑
✅ 无需修改技能实体(复用 SkillEnt
✅ 自动与战斗系统集成
✅ 支持多种施法方式
**开始享受清晰的架构吧!** 🚀

View File

@@ -1,11 +0,0 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "a851deeb-51c4-4c8d-990f-0d460fe8848b",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -1,296 +0,0 @@
# 新技能系统使用说明
## 📊 架构概览
基于 **oops-framework ECS** 架构设计的完整施法系统。
---
## 🗂️ 文件结构
```
技能系统文件:
├── HeroSkills.ts # 数据层:技能槽位数据
├── HSkillSystem.ts # 业务层3个系统
│ ├── SkillCastSystem # 施法系统
│ ├── SkillCDSystem # CD更新系统
│ └── SkillAutocastSystem # 自动施法系统AI
└── SkillEnt.ts # 技能实体(复用现有)
```
---
## 🎯 设计理念
### **数据层HeroSkillsComp**
**职责**存储角色拥有的技能列表和CD状态
```typescript
@ecs.register('HeroSkills')
export class HeroSkillsComp extends ecs.Comp {
skills: SkillSlot[] = []; // 技能槽位数组
// 数据方法
initSkills(skillIds: number[]) { } // 初始化技能
canCast(index, mp): boolean { } // 检查可施放
resetCD(index) { } // 重置CD
updateCDs(dt) { } // 更新CD
getReadySkills(mp): number[] { } // 获取就绪技能
}
```
**技能槽位数据**
```typescript
interface SkillSlot {
uuid: number; // 技能配置ID
cd: number; // 当前CD递减
cd_max: number; // 最大CD
cost: number; // MP消耗
level: number; // 技能等级
}
```
---
### **业务层3个系统**
#### **1. SkillCastSystem施法系统⭐**
**职责**:监听施法请求,执行施法
```typescript
export class SkillCastSystem extends ecs.ComblockSystem
implements ecs.IEntityEnterSystem {
// 筛选:拥有技能 + 请求标记的实体
filter(): ecs.IMatcher {
return ecs.allOf(HeroSkillsComp, HeroAttrsComp, CastSkillRequestComp);
}
// 处理施法请求
entityEnter(e: ecs.Entity): void {
// 1. 检查施法条件CD、MP、状态
// 2. 扣除MP
// 3. 重置CD
// 4. 播放动画
// 5. 创建技能实体
// 6. 移除请求标记
}
}
```
---
#### **2. SkillCDSystemCD更新系统**
**职责**每帧自动更新所有技能CD
```typescript
export class SkillCDSystem extends ecs.ComblockSystem
implements ecs.ISystemUpdate {
filter(): ecs.IMatcher {
return ecs.allOf(HeroSkillsComp);
}
update(e: ecs.Entity): void {
const skillsData = e.get(HeroSkillsComp);
skillsData.updateCDs(this.dt); // 自动递减CD
}
}
```
---
#### **3. SkillAutocastSystem自动施法系统**
**职责**AI自动选择和施放技能
```typescript
export class SkillAutocastSystem extends ecs.ComblockSystem
implements ecs.ISystemUpdate {
filter(): ecs.IMatcher {
return ecs.allOf(HeroSkillsComp, HeroAttrsComp, HeroViewComp);
}
update(e: ecs.Entity): void {
// 1. 检查角色状态
// 2. 获取可施放技能
// 3. 选择目标
// 4. 添加施法请求标记 ← 触发 SkillCastSystem
}
}
```
---
## 🔄 数据流程
```
方式1自动施法AI
SkillAutocastSystem.update()
├─ 检测可施放技能
├─ 选择目标
└─ 添加 CastSkillRequestComp 标记
SkillCastSystem.entityEnter() ← 自动触发
├─ 检查施法条件
├─ 扣除MP
├─ 重置CD
├─ 播放施法动画
├─ 创建 SkillEnt
└─ 移除 CastSkillRequestComp
方式2手动施法玩家点击
UI.onClick()
└─ skillConComp.manualCastSkill(index)
└─ 添加 CastSkillRequestComp 标记
后续流程同方式1
方式3强制施法天赋、事件触发
TalentSystem
└─ heroEntity.add(CastSkillRequestComp)
后续流程同方式1
```
---
## 🚀 使用示例
### **示例 1初始化角色技能**
```typescript
// Hero.ts - load() 方法中
const skillsComp = this.get(HeroSkillsComp);
skillsComp.initSkills(hero.skills); // [6001, 6005, 6010]
```
---
### **示例 2自动施法默认**
**无需额外代码**`SkillAutocastSystem` 会自动处理:
```typescript
// 每帧自动检测:
// - 是否有可施放技能?
// - CD好了MP够
// - 正在攻击?
// ✅ 自动添加施法请求标记 → 触发施法
```
---
### **示例 3手动施法玩家点击**
```typescript
// UI 按钮点击
onSkillButton1Click() {
const skillCon = this.heroNode.getComponent(SkillConComp);
skillCon.manualCastSkill(0); // 施放第0个技能
}
```
---
### **示例 4强制施法天赋触发**
```typescript
// 天赋系统
doTalentEffect(heroEntity: ecs.Entity) {
// ✅ 添加施法请求标记
const request = heroEntity.add(CastSkillRequestComp);
request.skillIndex = 1; // 施放第1个技能
request.targetPositions = [v3(100, 0, 0)];
}
```
---
## ⚙️ 系统注册Main.ts
```typescript
protected async initEcsSystem() {
// ✅ 注册技能系统(按顺序)
oops.ecs.add(new SkillCDSystem()); // 1. CD更新
oops.ecs.add(new SkillAutocastSystem()); // 2. 自动施法AI
oops.ecs.add(new SkillCastSystem()); // 3. 施法执行
// 战斗系统
oops.ecs.add(new HeroAtkSystem());
oops.ecs.add(new HeroAttrSystem());
}
```
---
## 📋 迁移清单
### ✅ **已完成**
| 任务 | 状态 | 说明 |
|------|------|------|
| 创建 HeroSkillsComp | ✅ | 技能数据组件 |
| 创建 SkillCastSystem | ✅ | 施法执行系统 |
| 创建 SkillCDSystem | ✅ | CD更新系统 |
| 创建 SkillAutocastSystem | ✅ | 自动施法系统 |
| 更新 Hero.ts | ✅ | 添加 HeroSkillsComp |
| 更新 Mon.ts | ✅ | 添加 HeroSkillsComp |
| 从 HeroAttrsComp 移除 skills | ✅ | 数据迁移完成 |
| 更新 SkillConComp | ✅ | 使用新系统 |
---
## 🎯 与战斗系统集成
**技能系统只负责"施法",伤害结算由战斗系统处理**
```
SkillCastSystem施法
创建 SkillEnt技能实体
SkillEnt 碰撞检测
AtkConCom.single_damage()
HeroAtkSystem.doAttack() ← 统一战斗系统
├─ 暴击判定
├─ 闪避判定
├─ 护盾吸收
├─ 修改数据
└─ 触发视图
```
---
## ✅ 优点总结
| 优点 | 说明 |
|------|------|
| **数据分离** | 技能数据独立组件,不污染 HeroAttrsComp |
| **标记驱动** | 使用 CastSkillRequestComp 标记组件,符合 ECS |
| **职责清晰** | CD更新、施法检查、执行分离成独立系统 |
| **易于扩展** | 添加新施法方式(手动/自动/强制)无需改动核心 |
| **易于测试** | 可单独测试每个系统 |
| **代码复用** | 手动/自动/强制施法共用同一套逻辑 |
---
## 🎉 总结
**完整的、规范的、基于 oops-framework 的技能施法系统!**
✅ 符合 ECS 架构(数据/业务/视图分离)
✅ 使用标记组件驱动,完全解耦
✅ 复用现有 SkillEnt无需重写
✅ 与战斗系统完美集成
✅ 支持自动/手动/强制多种施法方式
✅ 代码清晰,注释详细
**可直接投入生产使用!** 🚀

View File

@@ -1,11 +0,0 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "2df12e82-84f3-40f1-a838-145f935d4cc1",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}