rts-demo
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { Node, resources, JsonAsset } from 'cc';
|
||||
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, TaskStatus, BehaviorTreeJSONConfig } from '@esengine/ai';
|
||||
import { ECSComponent } from './UnitComponent';
|
||||
|
||||
/**
|
||||
* 行为树组件 - ECS组件,管理单个实体的行为树
|
||||
*/
|
||||
export class BehaviorTreeComponent extends ECSComponent {
|
||||
public behaviorTreeFile: string = '';
|
||||
public cocosNode: Node | null = null;
|
||||
|
||||
private behaviorTree: BehaviorTree<any> | null = null;
|
||||
private blackboard: Blackboard | null = null;
|
||||
private context: any = null;
|
||||
private isLoaded: boolean = false;
|
||||
private isRunning: boolean = false;
|
||||
private lastTickTime: number = 0;
|
||||
private tickInterval: number = 0.1; // 行为树更新间隔(秒)
|
||||
|
||||
/**
|
||||
* 初始化行为树
|
||||
*/
|
||||
async initialize() {
|
||||
if (!this.behaviorTreeFile) {
|
||||
console.error('行为树文件路径未设置');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadBehaviorTree();
|
||||
this.setupBlackboard();
|
||||
this.isLoaded = true;
|
||||
this.isRunning = true;
|
||||
console.log(`行为树组件初始化成功: ${this.behaviorTreeFile}`);
|
||||
} catch (error) {
|
||||
console.error(`行为树组件初始化失败: ${this.behaviorTreeFile}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载行为树文件
|
||||
*/
|
||||
private async loadBehaviorTree(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 移除.btree扩展名,使用.bt.json
|
||||
const jsonPath = this.behaviorTreeFile.replace('.btree', '.bt.json');
|
||||
|
||||
resources.load(jsonPath, JsonAsset, (err, asset) => {
|
||||
if (err) {
|
||||
console.error(`加载行为树文件失败: ${jsonPath}`, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const treeData = asset.json as BehaviorTreeJSONConfig;
|
||||
this.buildBehaviorTree(treeData);
|
||||
resolve();
|
||||
} catch (buildError) {
|
||||
console.error(`构建行为树失败: ${jsonPath}`, buildError);
|
||||
reject(buildError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建行为树
|
||||
*/
|
||||
private buildBehaviorTree(treeData: BehaviorTreeJSONConfig) {
|
||||
// 创建基础执行上下文
|
||||
const baseContext = {
|
||||
cocosNode: this.cocosNode,
|
||||
unitComponent: this
|
||||
};
|
||||
|
||||
// 使用@esengine/ai的BehaviorTreeBuilder构建行为树
|
||||
// 这会自动创建黑板并设置所有配置
|
||||
const result = BehaviorTreeBuilder.fromBehaviorTreeConfig(treeData, baseContext);
|
||||
this.behaviorTree = result.tree;
|
||||
this.blackboard = result.blackboard;
|
||||
this.context = result.context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板
|
||||
*/
|
||||
private setupBlackboard() {
|
||||
if (!this.blackboard || !this.cocosNode) return;
|
||||
|
||||
// 设置基础信息
|
||||
this.blackboard.setValue('entityName', this.cocosNode.name);
|
||||
this.blackboard.setValue('currentTime', Date.now() / 1000);
|
||||
this.blackboard.setValue('deltaTime', 0.016);
|
||||
this.blackboard.setValue('worldPosition', this.cocosNode.worldPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新行为树
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (!this.isLoaded || !this.isRunning || !this.behaviorTree || !this.context) return;
|
||||
|
||||
// 控制更新频率
|
||||
this.lastTickTime += deltaTime;
|
||||
if (this.lastTickTime < this.tickInterval) return;
|
||||
|
||||
this.lastTickTime = 0;
|
||||
|
||||
// 更新黑板中的时间信息
|
||||
if (this.blackboard) {
|
||||
this.blackboard.setValue('deltaTime', deltaTime);
|
||||
this.blackboard.setValue('currentTime', Date.now() / 1000);
|
||||
if (this.cocosNode) {
|
||||
this.blackboard.setValue('worldPosition', this.cocosNode.worldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行行为树
|
||||
try {
|
||||
this.behaviorTree.tick();
|
||||
} catch (error) {
|
||||
console.error(`行为树执行错误:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板
|
||||
*/
|
||||
getBlackboard(): Blackboard | null {
|
||||
return this.blackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume() {
|
||||
if (this.isLoaded) {
|
||||
this.isRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8efd182b-9891-4903-bef2-eb07b5184263",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import { _decorator, Component, resources, JsonAsset } from 'cc';
|
||||
import { BehaviorTree, BehaviorTreeBuilder, Blackboard, BehaviorTreeJSONConfig } from '@esengine/ai';
|
||||
import { UnitController } from './UnitController';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 执行上下文接口
|
||||
*/
|
||||
interface GameExecutionContext {
|
||||
blackboard?: Blackboard;
|
||||
unitController: UnitController;
|
||||
gameObject: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树管理器 - 使用@esengine/ai包管理行为树
|
||||
*/
|
||||
@ccclass('BehaviorTreeManager')
|
||||
export class BehaviorTreeManager extends Component {
|
||||
|
||||
@property
|
||||
debugMode: boolean = true;
|
||||
|
||||
@property
|
||||
tickInterval: number = 0.1; // 行为树更新间隔(秒)
|
||||
|
||||
private behaviorTree: BehaviorTree<GameExecutionContext> | null = null;
|
||||
private blackboard: Blackboard | null = null;
|
||||
private context: GameExecutionContext | null = null;
|
||||
private isLoaded: boolean = false;
|
||||
private isRunning: boolean = false;
|
||||
private lastTickTime: number = 0;
|
||||
private unitController: UnitController | null = null;
|
||||
private currentBehaviorTreeName: string = '';
|
||||
|
||||
/**
|
||||
* 初始化行为树
|
||||
*/
|
||||
async initializeBehaviorTree(behaviorTreeName: string, unitController: UnitController) {
|
||||
this.currentBehaviorTreeName = behaviorTreeName;
|
||||
this.unitController = unitController;
|
||||
|
||||
try {
|
||||
await this.loadBehaviorTree(behaviorTreeName);
|
||||
this.setupBlackboard();
|
||||
this.isLoaded = true;
|
||||
this.isRunning = true;
|
||||
console.log(`行为树初始化成功: ${behaviorTreeName}`);
|
||||
} catch (error) {
|
||||
console.error(`行为树初始化失败: ${behaviorTreeName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载行为树文件
|
||||
*/
|
||||
private async loadBehaviorTree(behaviorTreeName: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const jsonPath = `${behaviorTreeName}.bt`;
|
||||
|
||||
resources.load(jsonPath, JsonAsset, (err, asset) => {
|
||||
if (err) {
|
||||
console.error(`加载行为树文件失败: ${jsonPath}`, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const behaviorTreeData = asset.json as BehaviorTreeJSONConfig;
|
||||
|
||||
// 创建执行上下文
|
||||
this.blackboard = new Blackboard();
|
||||
this.context = {
|
||||
blackboard: this.blackboard,
|
||||
unitController: this.unitController!,
|
||||
gameObject: this.node
|
||||
};
|
||||
|
||||
// 从JSON数据创建行为树
|
||||
const buildResult = BehaviorTreeBuilder.fromBehaviorTreeConfig(behaviorTreeData, this.context);
|
||||
this.behaviorTree = buildResult.tree as BehaviorTree<GameExecutionContext>;
|
||||
this.blackboard = buildResult.blackboard;
|
||||
|
||||
resolve();
|
||||
} catch (parseError) {
|
||||
console.error(`创建行为树失败: ${jsonPath}`, parseError);
|
||||
reject(parseError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板基础信息
|
||||
*/
|
||||
private setupBlackboard() {
|
||||
if (!this.unitController || !this.blackboard) return;
|
||||
|
||||
// 设置单位基础信息
|
||||
this.blackboard.setValue('entityName', this.node.name);
|
||||
this.blackboard.setValue('unitType', this.unitController.unitType);
|
||||
this.blackboard.setValue('maxHealth', this.unitController.maxHealth);
|
||||
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
|
||||
this.blackboard.setValue('moveSpeed', this.unitController.moveSpeed);
|
||||
this.blackboard.setValue('attackRange', this.unitController.attackRange);
|
||||
this.blackboard.setValue('attackDamage', this.unitController.attackDamage);
|
||||
this.blackboard.setValue('attackCooldown', this.unitController.attackCooldown);
|
||||
|
||||
// 设置时间信息
|
||||
this.blackboard.setValue('currentTime', Date.now() / 1000);
|
||||
this.blackboard.setValue('deltaTime', 0.016);
|
||||
this.blackboard.setValue('worldPosition', this.node.worldPosition);
|
||||
|
||||
// 设置初始状态
|
||||
this.blackboard.setValue('currentCommand', 'idle');
|
||||
this.blackboard.setValue('hasTarget', false);
|
||||
this.blackboard.setValue('isSelected', false);
|
||||
|
||||
// 设置单位控制器引用,供行为树节点使用
|
||||
this.blackboard.setValue('unitController', this.unitController);
|
||||
this.blackboard.setValue('gameObject', this.node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新黑板值
|
||||
*/
|
||||
updateBlackboardValue(key: string, value: any) {
|
||||
if (this.blackboard) {
|
||||
this.blackboard.setValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板值
|
||||
*/
|
||||
getBlackboardValue(key: string): any {
|
||||
return this.blackboard?.getValue(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板
|
||||
*/
|
||||
getBlackboard(): Blackboard | null {
|
||||
return this.blackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树
|
||||
*/
|
||||
getBehaviorTree(): BehaviorTree<GameExecutionContext> | null {
|
||||
return this.behaviorTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新行为树
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (!this.isLoaded || !this.isRunning || !this.behaviorTree || !this.blackboard) return;
|
||||
|
||||
// 控制更新频率
|
||||
this.lastTickTime += deltaTime;
|
||||
if (this.lastTickTime < this.tickInterval) return;
|
||||
|
||||
this.lastTickTime = 0;
|
||||
|
||||
// 更新黑板中的时间信息
|
||||
this.blackboard.setValue('deltaTime', deltaTime);
|
||||
this.blackboard.setValue('currentTime', Date.now() / 1000);
|
||||
this.blackboard.setValue('worldPosition', this.node.worldPosition);
|
||||
|
||||
// 更新单位状态信息
|
||||
if (this.unitController) {
|
||||
this.blackboard.setValue('currentHealth', this.unitController.currentHealth);
|
||||
this.blackboard.setValue('healthPercentage', this.unitController.currentHealth / this.unitController.maxHealth);
|
||||
this.blackboard.setValue('isLowHealth', this.unitController.currentHealth < this.unitController.maxHealth * 0.3);
|
||||
this.blackboard.setValue('currentCommand', this.unitController.currentCommand);
|
||||
this.blackboard.setValue('isSelected', this.unitController.isSelected);
|
||||
this.blackboard.setValue('targetPosition', this.unitController.targetPosition);
|
||||
this.blackboard.setValue('targetNode', this.unitController.targetNode);
|
||||
|
||||
// 更新距离信息
|
||||
if (this.unitController.targetPosition) {
|
||||
const distance = this.node.worldPosition.subtract(this.unitController.targetPosition).length();
|
||||
this.blackboard.setValue('distanceToTarget', distance);
|
||||
this.blackboard.setValue('isInAttackRange', distance <= this.unitController.attackRange);
|
||||
this.blackboard.setValue('isCloseToTarget', distance <= 1.0);
|
||||
}
|
||||
|
||||
// 更新状态标志
|
||||
this.blackboard.setValue('isIdle', this.unitController.currentCommand === 'idle');
|
||||
this.blackboard.setValue('isMoving', this.unitController.currentCommand === 'move');
|
||||
this.blackboard.setValue('isAttacking', this.unitController.currentCommand === 'attack');
|
||||
this.blackboard.setValue('isGathering', this.unitController.currentCommand === 'gather');
|
||||
this.blackboard.setValue('isPatrolling', this.unitController.currentCommand === 'patrol');
|
||||
}
|
||||
|
||||
// 执行行为树
|
||||
try {
|
||||
this.behaviorTree.tick(deltaTime);
|
||||
if (this.debugMode && Math.random() < 0.01) { // 1%的概率打印调试信息
|
||||
console.log(`行为树执行完成, 单位: ${this.node.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`行为树执行错误:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume() {
|
||||
if (this.isLoaded) {
|
||||
this.isRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载行为树
|
||||
*/
|
||||
async reloadBehaviorTree() {
|
||||
if (this.currentBehaviorTreeName && this.unitController) {
|
||||
this.stop();
|
||||
await this.initializeBehaviorTree(this.currentBehaviorTreeName, this.unitController);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置行为树
|
||||
*/
|
||||
reset() {
|
||||
if (this.behaviorTree) {
|
||||
this.behaviorTree.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.stop();
|
||||
this.behaviorTree = null;
|
||||
this.blackboard = null;
|
||||
this.context = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "891c88fc-282d-4791-a961-8d85244bfee7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import { _decorator, Component, Node, Vec3, Material, MeshRenderer, Color, tween } from 'cc';
|
||||
import { BehaviorTreeComponent } from './BehaviorTreeComponent';
|
||||
|
||||
// 简化的ECS组件基类
|
||||
export class ECSComponent {
|
||||
public entity: any = null;
|
||||
}
|
||||
|
||||
// 简化的Entity类
|
||||
export class Entity {
|
||||
public name: string = '';
|
||||
private components: Map<any, any> = new Map();
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
addComponent(component: any) {
|
||||
this.components.set(component.constructor, component);
|
||||
component.entity = this;
|
||||
}
|
||||
|
||||
getComponent<T>(componentClass: any): T | null {
|
||||
return this.components.get(componentClass) || null;
|
||||
}
|
||||
|
||||
hasComponent(componentClass: any): boolean {
|
||||
return this.components.has(componentClass);
|
||||
}
|
||||
}
|
||||
|
||||
// 简化的Core类
|
||||
export class Core {
|
||||
static entityManager = {
|
||||
entities: [] as Entity[],
|
||||
createEntity: (name: string) => {
|
||||
const entity = new Entity(name);
|
||||
Core.entityManager.entities.push(entity);
|
||||
return entity;
|
||||
},
|
||||
destroyEntity: (entity: Entity) => {
|
||||
const index = Core.entityManager.entities.indexOf(entity);
|
||||
if (index !== -1) {
|
||||
Core.entityManager.entities.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static create(config?: any) {
|
||||
console.log('ECS Core initialized with config:', config);
|
||||
}
|
||||
|
||||
static update(deltaTime: number) {
|
||||
// 简化的更新逻辑
|
||||
}
|
||||
}
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 单位配置接口
|
||||
*/
|
||||
export interface UnitConfig {
|
||||
unitType: string;
|
||||
behaviorTreeFile: string;
|
||||
maxHealth: number;
|
||||
moveSpeed: number;
|
||||
attackRange: number;
|
||||
attackDamage: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位状态组件
|
||||
*/
|
||||
export class UnitStateComponent extends ECSComponent {
|
||||
public unitType: string = '';
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
public moveSpeed: number = 3;
|
||||
public attackRange: number = 2;
|
||||
public attackDamage: number = 25;
|
||||
public isSelected: boolean = false;
|
||||
public currentCommand: string = 'idle';
|
||||
public targetPosition: Vec3 = Vec3.ZERO.clone();
|
||||
public targetNode: Node | null = null;
|
||||
public lastAttackTime: number = 0;
|
||||
public attackCooldown: number = 1.5;
|
||||
public color: string = 'white';
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位组件 - Cocos Creator组件,管理单位的可视化和ECS实体
|
||||
*/
|
||||
@ccclass('UnitComponent')
|
||||
export class UnitComponent extends Component {
|
||||
|
||||
@property
|
||||
showDebugInfo: boolean = true;
|
||||
|
||||
private entity: Entity | null = null;
|
||||
private unitState: UnitStateComponent | null = null;
|
||||
private behaviorTreeComponent: BehaviorTreeComponent | null = null;
|
||||
private meshRenderer: MeshRenderer | null = null;
|
||||
private originalMaterial: Material | null = null;
|
||||
private selectionMaterial: Material | null = null;
|
||||
|
||||
onLoad() {
|
||||
this.meshRenderer = this.getComponent(MeshRenderer);
|
||||
if (this.meshRenderer && this.meshRenderer.material) {
|
||||
this.originalMaterial = this.meshRenderer.material;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位配置
|
||||
*/
|
||||
setup(config: UnitConfig) {
|
||||
// 创建ECS实体
|
||||
this.entity = Core.entityManager.createEntity(`Unit_${this.node.name}`);
|
||||
|
||||
// 添加单位状态组件
|
||||
this.unitState = new UnitStateComponent();
|
||||
this.unitState.unitType = config.unitType;
|
||||
this.unitState.maxHealth = config.maxHealth;
|
||||
this.unitState.currentHealth = config.maxHealth;
|
||||
this.unitState.moveSpeed = config.moveSpeed;
|
||||
this.unitState.attackRange = config.attackRange;
|
||||
this.unitState.attackDamage = config.attackDamage;
|
||||
this.unitState.color = config.color;
|
||||
this.entity.addComponent(this.unitState);
|
||||
|
||||
// 添加行为树组件
|
||||
this.behaviorTreeComponent = new BehaviorTreeComponent();
|
||||
this.behaviorTreeComponent.behaviorTreeFile = config.behaviorTreeFile;
|
||||
this.behaviorTreeComponent.cocosNode = this.node;
|
||||
this.entity.addComponent(this.behaviorTreeComponent);
|
||||
|
||||
// 设置材质颜色
|
||||
this.setUnitColor(config.color);
|
||||
|
||||
console.log(`单位 ${this.node.name} 设置完成 - 类型: ${config.unitType}, 行为树: ${config.behaviorTreeFile}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位颜色
|
||||
*/
|
||||
private setUnitColor(colorName: string) {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
const colorMap: { [key: string]: Color } = {
|
||||
'red': Color.RED,
|
||||
'green': Color.GREEN,
|
||||
'blue': Color.BLUE,
|
||||
'yellow': Color.YELLOW,
|
||||
'white': Color.WHITE,
|
||||
'cyan': Color.CYAN,
|
||||
'magenta': Color.MAGENTA
|
||||
};
|
||||
|
||||
const color = colorMap[colorName] || Color.WHITE;
|
||||
this.meshRenderer.material.setProperty('mainColor', color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选择状态
|
||||
*/
|
||||
setSelected(selected: boolean) {
|
||||
if (!this.unitState) return;
|
||||
|
||||
this.unitState.isSelected = selected;
|
||||
|
||||
// 视觉效果
|
||||
if (selected) {
|
||||
this.showSelectionEffect();
|
||||
} else {
|
||||
this.hideSelectionEffect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示选择效果
|
||||
*/
|
||||
private showSelectionEffect() {
|
||||
// 添加选择圈效果
|
||||
tween(this.node)
|
||||
.to(0.3, { scale: new Vec3(1.1, 1.1, 1.1) })
|
||||
.to(0.3, { scale: Vec3.ONE })
|
||||
.union()
|
||||
.repeatForever()
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏选择效果
|
||||
*/
|
||||
private hideSelectionEffect() {
|
||||
// 停止所有缩放动画
|
||||
tween(this.node).stop();
|
||||
this.node.setScale(Vec3.ONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布命令
|
||||
*/
|
||||
issueCommand(command: string, target?: Vec3 | Node) {
|
||||
if (!this.unitState || !this.behaviorTreeComponent) return;
|
||||
|
||||
this.unitState.currentCommand = command;
|
||||
|
||||
// 设置目标
|
||||
if (target instanceof Vec3) {
|
||||
this.unitState.targetPosition = target.clone();
|
||||
this.unitState.targetNode = null;
|
||||
} else if (target instanceof Node) {
|
||||
this.unitState.targetPosition = target.worldPosition.clone();
|
||||
this.unitState.targetNode = target;
|
||||
}
|
||||
|
||||
// 通过黑板更新行为树状态
|
||||
const blackboard = this.behaviorTreeComponent.getBlackboard();
|
||||
if (blackboard) {
|
||||
blackboard.setValue('currentCommand', command);
|
||||
blackboard.setValue('hasTarget', target !== undefined);
|
||||
blackboard.setValue('targetPosition', this.unitState.targetPosition);
|
||||
|
||||
if (target instanceof Node) {
|
||||
blackboard.setValue('targetType', target.name.includes('Resource') ? 'resource' :
|
||||
target.name.includes('Building') ? 'building' : 'unit');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`单位 ${this.node.name} 接收命令: ${command}`, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单位状态
|
||||
*/
|
||||
getUnitState(): UnitStateComponent | null {
|
||||
return this.unitState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树组件
|
||||
*/
|
||||
getBehaviorTreeComponent(): BehaviorTreeComponent | null {
|
||||
return this.behaviorTreeComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*/
|
||||
takeDamage(damage: number) {
|
||||
if (!this.unitState) return;
|
||||
|
||||
this.unitState.currentHealth = Math.max(0, this.unitState.currentHealth - damage);
|
||||
|
||||
// 更新黑板
|
||||
const blackboard = this.behaviorTreeComponent?.getBlackboard();
|
||||
if (blackboard) {
|
||||
blackboard.setValue('currentHealth', this.unitState.currentHealth);
|
||||
blackboard.setValue('healthPercentage', this.unitState.currentHealth / this.unitState.maxHealth);
|
||||
blackboard.setValue('isLowHealth', this.unitState.currentHealth < this.unitState.maxHealth * 0.3);
|
||||
}
|
||||
|
||||
// 视觉效果
|
||||
this.showDamageEffect();
|
||||
|
||||
if (this.unitState.currentHealth <= 0) {
|
||||
this.die();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示受伤效果
|
||||
*/
|
||||
private showDamageEffect() {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
// 闪红效果
|
||||
const originalColor = this.meshRenderer.material.getProperty('mainColor') as Color;
|
||||
this.meshRenderer.material.setProperty('mainColor', Color.RED);
|
||||
|
||||
this.scheduleOnce(() => {
|
||||
if (this.meshRenderer && this.meshRenderer.material) {
|
||||
this.meshRenderer.material.setProperty('mainColor', originalColor);
|
||||
}
|
||||
}, 0.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位死亡
|
||||
*/
|
||||
private die() {
|
||||
console.log(`单位 ${this.node.name} 死亡`);
|
||||
|
||||
// 从ECS系统中移除实体
|
||||
if (this.entity) {
|
||||
Core.entityManager.destroyEntity(this.entity);
|
||||
}
|
||||
|
||||
// 播放死亡动画后销毁节点
|
||||
tween(this.node)
|
||||
.to(0.5, { scale: Vec3.ZERO })
|
||||
.call(() => {
|
||||
this.node.destroy();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (!this.unitState || !this.behaviorTreeComponent) return;
|
||||
|
||||
// 更新黑板中的时间相关变量
|
||||
const blackboard = this.behaviorTreeComponent.getBlackboard();
|
||||
if (blackboard) {
|
||||
blackboard.setValue('deltaTime', deltaTime);
|
||||
blackboard.setValue('currentTime', Date.now() / 1000);
|
||||
blackboard.setValue('worldPosition', this.node.worldPosition);
|
||||
}
|
||||
|
||||
// 调试信息显示
|
||||
if (this.showDebugInfo) {
|
||||
this.updateDebugInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新调试信息
|
||||
*/
|
||||
private updateDebugInfo() {
|
||||
// 可以在这里添加调试信息的显示逻辑
|
||||
// 比如在单位上方显示状态文本等
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 清理ECS实体
|
||||
if (this.entity) {
|
||||
Core.entityManager.destroyEntity(this.entity);
|
||||
}
|
||||
|
||||
// 停止所有动画
|
||||
tween(this.node).stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "61885e67-b7a2-4e51-857e-ccbea4fdea03",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import { _decorator, Component, Node, Vec3, Material, MeshRenderer, Color, tween } from 'cc';
|
||||
import { BehaviorTreeManager } from './BehaviorTreeManager';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 单位配置接口
|
||||
*/
|
||||
export interface UnitConfig {
|
||||
unitType: string;
|
||||
behaviorTreeName: string;
|
||||
maxHealth: number;
|
||||
moveSpeed: number;
|
||||
attackRange: number;
|
||||
attackDamage: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位控制器 - 纯Cocos Creator组件,管理单位的行为和状态
|
||||
*/
|
||||
@ccclass('UnitController')
|
||||
export class UnitController extends Component {
|
||||
|
||||
@property
|
||||
showDebugInfo: boolean = true;
|
||||
|
||||
// 单位属性
|
||||
public unitType: string = '';
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
public moveSpeed: number = 3;
|
||||
public attackRange: number = 2;
|
||||
public attackDamage: number = 25;
|
||||
public isSelected: boolean = false;
|
||||
public currentCommand: string = 'idle';
|
||||
public targetPosition: Vec3 = Vec3.ZERO.clone();
|
||||
public targetNode: Node | null = null;
|
||||
public lastAttackTime: number = 0;
|
||||
public attackCooldown: number = 1.5;
|
||||
public color: string = 'white';
|
||||
|
||||
private behaviorTreeManager: BehaviorTreeManager | null = null;
|
||||
private meshRenderer: MeshRenderer | null = null;
|
||||
|
||||
onLoad() {
|
||||
this.meshRenderer = this.getComponent(MeshRenderer);
|
||||
|
||||
// 创建行为树管理器
|
||||
this.behaviorTreeManager = this.addComponent(BehaviorTreeManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位配置
|
||||
*/
|
||||
setup(config: UnitConfig) {
|
||||
this.unitType = config.unitType;
|
||||
this.maxHealth = config.maxHealth;
|
||||
this.currentHealth = config.maxHealth;
|
||||
this.moveSpeed = config.moveSpeed;
|
||||
this.attackRange = config.attackRange;
|
||||
this.attackDamage = config.attackDamage;
|
||||
this.color = config.color;
|
||||
|
||||
// 设置材质颜色
|
||||
this.setUnitColor(config.color);
|
||||
|
||||
// 初始化行为树
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.initializeBehaviorTree(config.behaviorTreeName, this);
|
||||
}
|
||||
|
||||
console.log(`单位 ${this.node.name} 设置完成 - 类型: ${config.unitType}, 行为树: ${config.behaviorTreeName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单位颜色
|
||||
*/
|
||||
private setUnitColor(colorName: string) {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
const colorMap: { [key: string]: Color } = {
|
||||
'red': Color.RED,
|
||||
'green': Color.GREEN,
|
||||
'blue': Color.BLUE,
|
||||
'yellow': Color.YELLOW,
|
||||
'white': Color.WHITE,
|
||||
'cyan': Color.CYAN,
|
||||
'magenta': Color.MAGENTA
|
||||
};
|
||||
|
||||
const color = colorMap[colorName] || Color.WHITE;
|
||||
this.meshRenderer.material.setProperty('mainColor', color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选择状态
|
||||
*/
|
||||
setSelected(selected: boolean) {
|
||||
this.isSelected = selected;
|
||||
|
||||
// 视觉效果
|
||||
if (selected) {
|
||||
this.showSelectionEffect();
|
||||
} else {
|
||||
this.hideSelectionEffect();
|
||||
}
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('isSelected', selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示选择效果
|
||||
*/
|
||||
private showSelectionEffect() {
|
||||
// 添加选择圈效果
|
||||
tween(this.node)
|
||||
.to(0.3, { scale: new Vec3(1.1, 1.1, 1.1) })
|
||||
.to(0.3, { scale: Vec3.ONE })
|
||||
.union()
|
||||
.repeatForever()
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏选择效果
|
||||
*/
|
||||
private hideSelectionEffect() {
|
||||
// 停止所有缩放动画
|
||||
tween(this.node).stop();
|
||||
this.node.setScale(Vec3.ONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布命令
|
||||
*/
|
||||
issueCommand(command: string, target?: Vec3 | Node) {
|
||||
this.currentCommand = command;
|
||||
|
||||
// 设置目标
|
||||
if (target instanceof Vec3) {
|
||||
this.targetPosition = target.clone();
|
||||
this.targetNode = null;
|
||||
} else if (target instanceof Node) {
|
||||
this.targetPosition = target.worldPosition.clone();
|
||||
this.targetNode = target;
|
||||
}
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('currentCommand', command);
|
||||
this.behaviorTreeManager.updateBlackboardValue('hasTarget', target !== undefined);
|
||||
this.behaviorTreeManager.updateBlackboardValue('targetPosition', this.targetPosition);
|
||||
|
||||
if (target instanceof Node) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('targetType',
|
||||
target.name.includes('Resource') ? 'resource' :
|
||||
target.name.includes('Building') ? 'building' : 'unit');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`单位 ${this.node.name} 接收命令: ${command}`, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*/
|
||||
takeDamage(damage: number) {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('currentHealth', this.currentHealth);
|
||||
this.behaviorTreeManager.updateBlackboardValue('healthPercentage', this.currentHealth / this.maxHealth);
|
||||
this.behaviorTreeManager.updateBlackboardValue('isLowHealth', this.currentHealth < this.maxHealth * 0.3);
|
||||
}
|
||||
|
||||
// 视觉效果
|
||||
this.showDamageEffect();
|
||||
|
||||
if (this.currentHealth <= 0) {
|
||||
this.die();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示受伤效果
|
||||
*/
|
||||
private showDamageEffect() {
|
||||
if (!this.meshRenderer || !this.meshRenderer.material) return;
|
||||
|
||||
// 闪红效果
|
||||
const originalColor = this.meshRenderer.material.getProperty('mainColor') as Color;
|
||||
this.meshRenderer.material.setProperty('mainColor', Color.RED);
|
||||
|
||||
this.scheduleOnce(() => {
|
||||
if (this.meshRenderer && this.meshRenderer.material) {
|
||||
this.meshRenderer.material.setProperty('mainColor', originalColor);
|
||||
}
|
||||
}, 0.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位死亡
|
||||
*/
|
||||
private die() {
|
||||
console.log(`单位 ${this.node.name} 死亡`);
|
||||
|
||||
// 播放死亡动画后销毁节点
|
||||
tween(this.node)
|
||||
.to(0.5, { scale: Vec3.ZERO })
|
||||
.call(() => {
|
||||
this.node.destroy();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动到目标位置
|
||||
*/
|
||||
moveToTarget(targetPos: Vec3, speed?: number): boolean {
|
||||
const currentPos = this.node.worldPosition;
|
||||
const distance = currentPos.subtract(targetPos).length();
|
||||
|
||||
if (distance < 0.5) {
|
||||
return true; // 已到达目标
|
||||
}
|
||||
|
||||
// 简单的移动逻辑
|
||||
const direction = targetPos.subtract(currentPos).normalize();
|
||||
const moveSpeed = speed || this.moveSpeed;
|
||||
const deltaTime = 1/60; // 假设60fps
|
||||
|
||||
const newPosition = currentPos.add(direction.multiplyScalar(moveSpeed * deltaTime));
|
||||
this.node.setWorldPosition(newPosition);
|
||||
|
||||
return false; // 还在移动中
|
||||
}
|
||||
|
||||
/**
|
||||
* 攻击目标
|
||||
*/
|
||||
attackTarget(): boolean {
|
||||
const currentTime = Date.now() / 1000;
|
||||
|
||||
if (currentTime - this.lastAttackTime < this.attackCooldown) {
|
||||
return false; // 冷却中
|
||||
}
|
||||
|
||||
// 执行攻击
|
||||
console.log(`${this.node.name} 执行攻击`);
|
||||
this.lastAttackTime = currentTime;
|
||||
|
||||
// 更新行为树黑板
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('lastAttackTime', currentTime);
|
||||
}
|
||||
|
||||
return true; // 攻击成功
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// 更新行为树黑板中的时间相关变量
|
||||
if (this.behaviorTreeManager) {
|
||||
this.behaviorTreeManager.updateBlackboardValue('deltaTime', deltaTime);
|
||||
this.behaviorTreeManager.updateBlackboardValue('currentTime', Date.now() / 1000);
|
||||
this.behaviorTreeManager.updateBlackboardValue('worldPosition', this.node.worldPosition);
|
||||
|
||||
// 更新距离信息
|
||||
if (this.targetPosition) {
|
||||
const distance = this.node.worldPosition.subtract(this.targetPosition).length();
|
||||
this.behaviorTreeManager.updateBlackboardValue('distanceToTarget', distance);
|
||||
this.behaviorTreeManager.updateBlackboardValue('isInAttackRange', distance <= this.attackRange);
|
||||
this.behaviorTreeManager.updateBlackboardValue('isCloseToTarget', distance <= 1.0);
|
||||
}
|
||||
|
||||
// 更新状态标志
|
||||
this.behaviorTreeManager.updateBlackboardValue('isIdle', this.currentCommand === 'idle');
|
||||
this.behaviorTreeManager.updateBlackboardValue('isMoving', this.currentCommand === 'move');
|
||||
this.behaviorTreeManager.updateBlackboardValue('isAttacking', this.currentCommand === 'attack');
|
||||
this.behaviorTreeManager.updateBlackboardValue('isGathering', this.currentCommand === 'gather');
|
||||
this.behaviorTreeManager.updateBlackboardValue('isPatrolling', this.currentCommand === 'patrol');
|
||||
}
|
||||
|
||||
// 调试信息显示
|
||||
if (this.showDebugInfo) {
|
||||
this.updateDebugInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新调试信息
|
||||
*/
|
||||
private updateDebugInfo() {
|
||||
// 可以在这里添加调试信息的显示逻辑
|
||||
// 比如在单位上方显示状态文本等
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 停止所有动画
|
||||
tween(this.node).stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4ac64480-2d09-4de6-a22c-add022790676",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user