2025-06-25 17:50:40 +08:00
|
|
|
|
import { _decorator, Component, Node, Vec3, MeshRenderer, Color, tween } from 'cc';
|
2025-06-24 19:34:37 +08:00
|
|
|
|
import { BehaviorTreeManager } from './BehaviorTreeManager';
|
2025-06-24 23:51:59 +08:00
|
|
|
|
import { RTSBehaviorHandler } from './RTSBehaviorHandler';
|
2025-06-24 19:34:37 +08:00
|
|
|
|
|
|
|
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 单位配置接口
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface UnitConfig {
|
|
|
|
|
|
unitType: string;
|
|
|
|
|
|
behaviorTreeName: string;
|
|
|
|
|
|
maxHealth: number;
|
|
|
|
|
|
moveSpeed: number;
|
|
|
|
|
|
attackRange: number;
|
|
|
|
|
|
attackDamage: number;
|
|
|
|
|
|
color: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-25 17:50:40 +08:00
|
|
|
|
* 单位控制器
|
2025-06-24 19:34:37 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@ccclass('UnitController')
|
|
|
|
|
|
export class UnitController extends Component {
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
showDebugInfo: boolean = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 单位属性
|
|
|
|
|
|
public unitType: string = '';
|
|
|
|
|
|
public maxHealth: number = 100;
|
|
|
|
|
|
public currentHealth: number = 100;
|
2025-06-24 23:51:59 +08:00
|
|
|
|
public moveSpeed: number = 1.5;
|
2025-06-24 19:34:37 +08:00
|
|
|
|
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';
|
|
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
// 体力系统属性
|
|
|
|
|
|
public maxStamina: number = 100;
|
|
|
|
|
|
public currentStamina: number = 100;
|
|
|
|
|
|
public homePosition: Vec3 = Vec3.ZERO.clone();
|
|
|
|
|
|
public staminaRecoveryRate: number = 20; // 每秒恢复的体力
|
|
|
|
|
|
public staminaCostPerMining: number = 15; // 每次挖矿消耗的体力
|
|
|
|
|
|
|
2025-06-24 23:51:59 +08:00
|
|
|
|
// 移动状态管理
|
|
|
|
|
|
private isMoving: boolean = false;
|
|
|
|
|
|
private moveStartTime: number = 0;
|
|
|
|
|
|
private lastTargetUpdateTime: number = 0;
|
|
|
|
|
|
|
2025-06-24 19:34:37 +08:00
|
|
|
|
private behaviorTreeManager: BehaviorTreeManager | null = null;
|
2025-06-24 23:51:59 +08:00
|
|
|
|
private behaviorHandler: Component | null = null;
|
2025-06-24 19:34:37 +08:00
|
|
|
|
private meshRenderer: MeshRenderer | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
onLoad() {
|
|
|
|
|
|
this.meshRenderer = this.getComponent(MeshRenderer);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建行为树管理器
|
|
|
|
|
|
this.behaviorTreeManager = this.addComponent(BehaviorTreeManager);
|
2025-06-24 23:51:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加RTS行为处理器
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 添加RTSBehaviorHandler组件
|
|
|
|
|
|
this.behaviorHandler = this.addComponent(RTSBehaviorHandler);
|
|
|
|
|
|
} catch (error) {
|
2025-06-25 17:50:40 +08:00
|
|
|
|
console.warn('RTSBehaviorHandler组件添加失败', error);
|
2025-06-24 23:51:59 +08:00
|
|
|
|
}
|
2025-06-24 19:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置单位配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2025-06-24 23:51:59 +08:00
|
|
|
|
// 设置节点名称显示单位类型
|
|
|
|
|
|
this.node.name = `${config.unitType.toUpperCase()}_${this.node.name}`;
|
|
|
|
|
|
|
2025-06-24 19:34:37 +08:00
|
|
|
|
// 初始化行为树
|
|
|
|
|
|
if (this.behaviorTreeManager) {
|
|
|
|
|
|
this.behaviorTreeManager.initializeBehaviorTree(config.behaviorTreeName, this);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置单位颜色
|
|
|
|
|
|
*/
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 23:51:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 设置黑板变量值
|
|
|
|
|
|
*/
|
|
|
|
|
|
setBlackboardValue(key: string, value: any) {
|
|
|
|
|
|
if (this.behaviorTreeManager) {
|
|
|
|
|
|
this.behaviorTreeManager.updateBlackboardValue(key, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取黑板变量值
|
|
|
|
|
|
*/
|
|
|
|
|
|
getBlackboardValue(key: string): any {
|
|
|
|
|
|
return this.behaviorTreeManager?.getBlackboardValue(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 23:51:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 设置移动目标
|
|
|
|
|
|
*/
|
|
|
|
|
|
setTarget(position: Vec3) {
|
|
|
|
|
|
this.targetPosition = position.clone();
|
2025-06-25 17:50:40 +08:00
|
|
|
|
this.isMoving = true;
|
|
|
|
|
|
this.moveStartTime = Date.now();
|
2025-06-24 23:51:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清除移动目标
|
|
|
|
|
|
*/
|
|
|
|
|
|
clearTarget() {
|
|
|
|
|
|
this.targetPosition = Vec3.ZERO.clone();
|
2025-06-25 17:50:40 +08:00
|
|
|
|
this.isMoving = false;
|
2025-06-24 23:51:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 19:34:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 受到伤害
|
|
|
|
|
|
*/
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-24 23:51:59 +08:00
|
|
|
|
* 移动到目标位置(只在水平面移动,不改变Y轴)
|
2025-06-24 19:34:37 +08:00
|
|
|
|
*/
|
2025-06-24 23:51:59 +08:00
|
|
|
|
moveToTarget(targetPos: Vec3, speed?: number, deltaTime?: number): boolean {
|
2025-06-24 19:34:37 +08:00
|
|
|
|
const currentPos = this.node.worldPosition;
|
2025-06-25 17:50:40 +08:00
|
|
|
|
const distance = Vec3.distance(currentPos, targetPos);
|
2025-06-24 19:34:37 +08:00
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
if (distance < 0.5) {
|
2025-06-24 23:51:59 +08:00
|
|
|
|
this.isMoving = false;
|
2025-06-25 17:50:40 +08:00
|
|
|
|
return true;
|
2025-06-24 19:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
const actualSpeed = speed || this.moveSpeed;
|
|
|
|
|
|
const actualDeltaTime = deltaTime || 0.016;
|
|
|
|
|
|
const direction = new Vec3();
|
|
|
|
|
|
Vec3.subtract(direction, targetPos, currentPos);
|
|
|
|
|
|
direction.normalize();
|
2025-06-24 23:51:59 +08:00
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
const moveDistance = actualSpeed * actualDeltaTime;
|
|
|
|
|
|
const newPosition = new Vec3();
|
|
|
|
|
|
Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance);
|
2025-06-24 19:34:37 +08:00
|
|
|
|
|
|
|
|
|
|
this.node.setWorldPosition(newPosition);
|
2025-06-24 23:51:59 +08:00
|
|
|
|
this.isMoving = true;
|
|
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
return false;
|
2025-06-24 19:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 攻击目标
|
|
|
|
|
|
*/
|
|
|
|
|
|
attackTarget(): boolean {
|
2025-06-25 17:50:40 +08:00
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
if (currentTime - this.lastAttackTime < this.attackCooldown * 1000) {
|
|
|
|
|
|
return false;
|
2025-06-24 19:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
if (this.targetNode && this.targetNode.isValid) {
|
|
|
|
|
|
const distance = Vec3.distance(this.node.worldPosition, this.targetNode.worldPosition);
|
|
|
|
|
|
if (distance <= this.attackRange) {
|
|
|
|
|
|
this.lastAttackTime = currentTime;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-06-24 19:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
return false;
|
2025-06-24 19:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
update(deltaTime: number) {
|
2025-06-25 17:50:40 +08:00
|
|
|
|
if (this.behaviorTreeManager) {
|
|
|
|
|
|
this.behaviorTreeManager.update(deltaTime);
|
2025-06-24 23:51:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 17:50:40 +08:00
|
|
|
|
if (this.isMoving && !this.targetPosition.equals(Vec3.ZERO)) {
|
|
|
|
|
|
const reached = this.moveToTarget(this.targetPosition, this.moveSpeed, deltaTime);
|
|
|
|
|
|
if (reached) {
|
|
|
|
|
|
this.clearTarget();
|
2025-06-24 19:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调试信息显示
|
|
|
|
|
|
if (this.showDebugInfo) {
|
|
|
|
|
|
this.updateDebugInfo();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新调试信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
private updateDebugInfo() {
|
|
|
|
|
|
// 可以在这里添加调试信息的显示逻辑
|
|
|
|
|
|
// 比如在单位上方显示状态文本等
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onDestroy() {
|
|
|
|
|
|
// 停止所有动画
|
|
|
|
|
|
tween(this.node).stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|