<think>我们需要在怪兽控制器(MonsterController)中添加攻击状态,并在GameManager中检测当怪兽与英雄的距离小于等于怪兽尺寸时,将怪兽状态设置为攻击状态。
同时,在攻击状态下,怪兽应该停止移动,并播放攻击动画。
步骤:
1. 在MonsterController中添加攻击状态和相应的动画名称。
2. 在GameManager的checkMonstersDistance方法中,当距离小于等于怪兽尺寸时,如果怪兽没有死亡、受伤,则设置状态为攻击(同时停止移动)。
3. 在设置攻击状态时,需要确保怪兽停止移动(在MonsterController的setState方法中处理攻击状态时,将isMove设为false,isAttack设为true,并播放攻击动画)。
4. 注意:攻击动画可能不需要循环播放,所以播放攻击动画时,循环参数设置为false,然后我们需要监听攻击动画完成事件,以便在攻击动画结束后回到站立状态(或者根据需求进行其他处理)。但是,由于我们的状态机是外部驱动的(由距离检测驱动),所以我们可以在攻击动画播放后,在下一轮距离检测中根据距离决定状态。如果攻击动画播放完后,距离仍然在攻击范围内,则继续播放攻击动画(或者播放攻击动画的循环部分,如果有的话)。但是,如果攻击动画是非循环的,播放完后会自动停止在最后一帧,所以我们需要考虑在攻击动画播放完后切换回站立或循环播放攻击动画(如果有多段攻击,可能需要更复杂的状态机)。
根据需求,我们暂时设定:当进入攻击状态时,播放一次攻击动画(非循环),然后自动回到站立状态。但是,这样可能会导致在攻击动画播放过程中,如果距离发生变化,状态会被打断。因此,我们需要在攻击动画播放期间锁定状态,直到攻击动画结束。
然而,我们的当前设计是每0.5秒检测一次距离,所以攻击动画可能在一轮检测后开始播放,在下一轮检测时如果距离仍然在攻击范围内,则继续播放攻击动画(再次触发攻击状态)。由于我们之前已经做了状态切换的优化(相同状态不会重复设置),所以攻击动画不会被打断。但是,攻击动画是非循环的,所以播放一次后就会停止在最后一帧。我们需要在攻击动画播放完后,自动切换到站立状态,但这样会与距离检测冲突(因为距离还在攻击范围内,下一轮检测又会设置为攻击状态,从而再次播放攻击动画)。
因此,我们可以这样设计:
- 攻击动画设置为非循环(loop: false)。
- 在攻击动画播放结束时,我们通过事件通知MonsterController,然后MonsterController将状态设置为站立(stand),但这样在下一轮检测时,因为距离还在攻击范围内,又会切换到攻击状态,从而再次播放攻击动画,形成连续攻击的效果。
但是,这样会导致攻击动画每次播放都是重新开始,看起来不连贯。所以,另一种做法是将攻击动画设置为循环,这样在攻击状态下,攻击动画会循环播放,直到距离超出攻击范围。
我们根据需求来决定:如果希望攻击动画连续播放直到目标死亡或脱离攻击范围,则使用循环播放。如果希望每次攻击动作只播放一次,然后等待下一次攻击(即攻击有间隔),则使用非循环,并在动画结束后自动回到站立状态,然后由下一轮检测再次触发攻击(形成间隔)。
这里我们按照循环播放来实现,因为这样更简单,而且符合大多数情况。如果以后需要攻击间隔,我们可以添加攻击冷却时间。
修改如下:
1. 在MonsterController中,为攻击动画设置循环播放(loop: true)。
2. 在GameManager的checkMonstersDistance方法中,当距离小于等于怪兽尺寸时,且不处于死亡、受伤状态,则设置为攻击状态(注意:攻击状态优先级高于移动和站立)。
但是,我们也要注意:在攻击状态下,如果英雄移动离开了攻击范围,那么怪兽应该立即停止攻击,转为移动或站立。
因此,我们不需要在MonsterController中处理攻击动画结束事件,而是由距离检测来驱动状态切换。
具体代码修改:
第一步:修改MonsterController,在setState方法中添加攻击状态的处理(已经存在,但需要确保攻击状态时停止移动)。
第二步:在GameManager的checkMonstersDistance方法中,添加攻击状态的判断条件。
修改后的checkMonstersDistance逻辑:
- 如果距离 <= 怪兽尺寸(注意:我们之前的状态切换缓冲是针对移动和站立的,现在攻击状态没有缓冲,因为攻击需要精确触发),则设置状态为攻击。
- 否则,如果距离大于怪兽尺寸+缓冲值,则移动;在缓冲区内保持原状态。
注意:攻击状态应该打断移动状态,而移动状态不应该打断攻击状态(因为攻击状态在近距离时触发,而移动状态是在远距离触发)。所以,在判断攻击状态时,优先级应该高于移动状态。
因此,我们先判断是否满足攻击条件,再判断移动条件。
修改GameManager.ts中的checkMonstersDistance方法:
```typescript
private checkMonstersDistance() {
const heroPos = this.HeroNode.position;
for (let i = 0; i < this.MonstersGroup.length; i++) {
const monsterNode = this.MonstersGroup[i];
const controller = monsterNode.getComponent(MonsterController);
if (!controller) continue;
const monsterPos = monsterNode.position;
// 计算距离(忽略Z轴)
const distance = Vec3.distance(
v3(heroPos.x, heroPos.y, 0),
v3(monsterPos.x, monsterPos.y, 0)
);
// 获取怪兽尺寸(半径)
const monsterSize = controller.MonsterSize;
// 判断是否在攻击范围内(距离小于等于怪兽尺寸)
if (distance <= monsterSize) {
// 确保怪兽没有死亡或处于其他状态(这里我们允许攻击状态打断移动和站立,但受伤和死亡不能被打断)
if (!controller.isDie && !controller.isHurt) {
// 如果当前不是攻击状态,则设置为攻击状态
if (!controller.isAttack) {
controller.setState('attack');
}
// 注意:在攻击状态下,我们不需要移动,所以跳过移动和站立的判断
// 更新朝向
this.updateMonsterDirection(monsterNode);
continue; // 跳过后续的移动和站立判断
}
}
// 添加状态切换缓冲(针对移动和站立)
const shouldMove = distance > (monsterSize + this.stateChangeBuffer);
const shouldStand = distance < (monsterSize - this.stateChangeBuffer);
// 如果距离大于怪兽尺寸+缓冲值,设置移动状态
if (shouldMove) {
// 确保怪兽没有死亡或处于其他状态
if (!controller.isDie && !controller.isAttack && !controller.isHurt) {
controller.setState('move');
}
}
// 如果距离小于尺寸-缓冲值,停止移动
else if (shouldStand) {
if (controller.isMove) {
controller.setState('stand');
}
}
// 在缓冲区内保持当前状态
// 更新朝向(距离变化后可能需要调整)
this.updateMonsterDirection(monsterNode);
}
}
```
第三步:在MonsterController中,确保攻击动画循环播放(loop: true)。
但是,我们之前已经将攻击动画的循环参数设置为true(在setState中调用playAnimation时,默认循环为true)。所以,在攻击状态下,动画会循环播放。
第四步:在攻击状态下,我们需要让怪兽停止移动。在MonsterController的setState方法中,当状态为攻击时,会设置isMove为false,所以怪兽不会在moveMonsters中被移动。
注意:在攻击状态下,我们仍然每帧更新朝向(在checkMonstersDistance中,当进入攻击状态时,我们更新了一次朝向,然后continue跳过了后续的移动/站立判断。但是,英雄可能在攻击过程中移动,所以我们需要在攻击状态下也更新朝向。因此,在攻击状态的分支中,我们在设置攻击状态后更新了一次朝向,然后continue。但是,在攻击状态下,我们并没有在每一帧都更新朝向(因为checkMonstersDistance是每0.5秒执行一次)。所以,我们需要在攻击状态下也能持续更新朝向。
为了在攻击状态下也能实时更新朝向,我们可以将更新朝向的代码提取出来,并在每帧都执行(在MonsterController的update中更新朝向,或者将updateMonsterDirection方法放到MonsterController中,由怪兽自己每帧更新朝向)。但是,为了简单,我们可以在GameManager的update中,每帧更新所有怪兽的朝向(而不仅仅是距离检测时更新)。但是这样可能会增加计算量。
另一种做法:在攻击状态下,我们也在GameManager的update中更新这些攻击状态怪兽的朝向。但是,目前我们的update中只调用了移动怪兽的方法,而移动怪兽的方法只处理移动状态的怪兽。
我们可以修改:在GameManager的update中,每帧更新所有怪兽的朝向(无论状态)。这样,无论怪兽处于什么状态,只要英雄移动了,怪兽就会实时调整朝向。
修改:在GameManager的update方法中,每帧都调用updateMonsterDirectionForAll方法,该方法遍历所有怪兽更新朝向。
但是,这样可能会影响性能(怪兽很多的时候)。因此,我们可以在攻击状态的分支中不continue,而是继续执行更新朝向(因为更新朝向在循环末尾,所以攻击状态分支中更新朝向后,继续执行循环末尾的更新朝向?)。实际上,在攻击状态分支中,我们已经更新了一次朝向,然后continue跳过了循环末尾的更新朝向。所以,攻击状态下的怪兽只在距离检测时更新朝向(每0.5秒一次)。这通常是可以接受的,因为0.5秒更新一次朝向已经足够。
所以,我们暂时保持这样。
代码修改总结:
1. MonsterController.ts不需要修改,因为已经支持攻击状态(但需要确保攻击动画名称是'm_attack_1_a',如果动画名称不同,需要修改animationNames中的attack字段)。
2. 修改GameManager.ts中的checkMonstersDistance方法,优先判断攻击条件。
3. 在怪兽攻击状态下,设置攻击状态并更新朝向后,跳过后续的移动/站立判断。
另外,注意:在攻击动画中,我们可能需要触发伤害判定。这可以通过Spine动画事件来实现,但这不在本次任务范围内。
最后,我们还需要在MonsterController中,确保攻击动画的名称是'm_attack_1_a'。所以,修改MonsterController中的animationNames:
private animationNames = {
move: "move",
stand: "m_stand_1",
attack: "m_attack_1_a", // 修改为实际的攻击动画名称
hurt: "hurt",
die: "die"
};
如果动画名称不是这个,请根据实际修改。
下面给出修改后的GameManager.ts中的checkMonstersDistance方法(其他部分不变)和MonsterController中的animationNames修改。
注意:由于攻击动画是循环的,所以会一直播放直到状态改变。
如果怪兽有多个攻击动画,可以随机选择一个,但这不在本次任务范围内。
代码实现:
修改MonsterController.ts中的animationNames,将attack改为'm_attack_1_a'。
修改GameManager.ts中的checkMonstersDistance方法(见上面代码)。
但是,我们还需要注意:在攻击状态下,如果英雄移动离开了攻击范围,那么在下一次距离检测时,攻击状态会被取消,然后根据距离切换到移动或站立状态。
因此,我们的设计是合理的。
下面给出修改后的MonsterController.ts(只修改了animationNames)和GameManager.ts(修改了checkMonstersDistance)的代码。
由于代码较长,这里只给出修改的部分。
MonsterController.ts修改部分:
```typescript
// MonsterController.ts
// ... 其他代码 ...
export class MonsterController extends Component {
// ... 其他代码 ...
// 动画名称配置
private animationNames = {
move: "move",
stand: "m_stand_1",
attack: "m_attack_1_a", // 攻击动画名称
hurt: "hurt",
die: "die"
};
// ... 其他代码 ...
}
```
GameManager.ts修改部分(checkMonstersDistance方法):
```typescript
// GameManager.ts
// ... 其他代码 ...
private checkMonstersDistance() {
const heroPos = this.HeroNode.position;
for (let i = 0; i < this.MonstersGroup.length; i++) {
const monsterNode = this.MonstersGroup[i];
const controller = monsterNode.getComponent(MonsterController);
if (!controller) continue;
const monsterPos = monsterNode.position;
// 计算距离(忽略Z轴)
const distance = Vec3.distance(
v3(heroPos.x, heroPos.y, 0),
v3(monsterPos.x, monsterPos.y, 0)
);
// 获取怪兽尺寸(半径)
const monsterSize = controller.MonsterSize;
// 判断是否在攻击范围内(距离小于等于怪兽尺寸)
if (distance <= monsterSize) {
// 确保怪兽没有死亡或处于其他状态(这里我们允许攻击状态打断移动和站立,但受伤和死亡不能被打断)
if (!controller.isDie && !controller.isHurt) {
// 如果当前不是攻击状态,则设置为攻击状态
if (!controller.isAttack) {
controller.setState('attack');
}
// 更新朝向(攻击时也要面向英雄)
this.updateMonsterDirection(monsterNode);
// 跳过后续的移动和站立判断
continue;
}
}
// 添加状态切换缓冲(针对移动和站立)
const shouldMove = distance > (monsterSize + this.stateChangeBuffer);
const shouldStand = distance < (monsterSize - this.stateChangeBuffer);
// 如果距离大于怪兽尺寸+缓冲值,设置移动状态
if (shouldMove) {
// 确保怪兽没有死亡或处于其他状态
if (!controller.isDie && !controller.isAttack && !controller.isHurt) {
controller.setState('move');
}
}
// 如果距离小于尺寸-缓冲值,停止移动
else if (shouldStand) {
if (controller.isMove) {
controller.setState('stand');
}
}
// 在缓冲区内保持当前状态
// 更新朝向(距离变化后可能需要调整)
this.updateMonsterDirection(monsterNode);
}
}
```
注意:在攻击状态下,我们只更新了一次朝向(在continue之前),然后跳过了后续的移动/站立判断和循环末尾的更新朝向。因为攻击状态下怪兽不需要移动,所以这样处理是安全的。而且,在攻击状态下,怪兽的朝向在每次距离检测时都会更新(每0.5秒),所以即使英雄移动,怪兽也会调整朝向。
如果希望攻击状态下每帧都更新朝向,可以将更新朝向的代码移到循环外面,或者单独在update中处理所有怪兽的朝向(无论状态)。这里为了简单,我们暂时采用每0.5秒更新一次攻击状态怪兽的朝向。
最后,我们还需要注意:在攻击状态下,我们设置状态为攻击时,如果当前已经是攻击状态,我们不会重复设置,所以不会重复播放动画(因为setState中已经做了优化,相同状态不会重复设置)。
这样,攻击功能就添加完成了。
但是,我们还需要测试:
- 当怪兽与英雄距离小于等于怪兽尺寸时,是否播放攻击动画,并且停止移动。
- 当英雄移动离开攻击范围时,怪兽是否停止攻击,转为移动或站立。
如果遇到问题,请检查动画名称是否正确,以及状态切换的逻辑。
由于代码较长,请根据上述修改点进行修改。
如果问题仍然存在,我们可以添加更多的调试日志。
注意:在攻击状态下,怪兽的移动状态(isMove)会被设为false,所以不会在moveMonsters方法中被移动。
因此,怪兽在攻击时应该停在原地播放攻击动画。
现在,我们完成了攻击功能的添加。
如果还需要添加攻击伤害判定,可以通过Spine动画事件来触发,这需要修改MonsterController,监听动画事件,并在攻击动画的特定帧触发伤害事件。但这超出了本次任务的范围。
最后,我们给出修改后的完整MonsterController.ts和GameManager.ts(仅checkMonstersDistance方法修改)的代码。
由于整个GameManager.ts很长,这里只提供修改的部分,不重复全部代码。
如果还有其他问题,请提出。
§§相关问题§§:
1. 如何通过Spine动画事件实现攻击伤害判定?
2. 如何设计一个更完善的角色状态机(包括攻击冷却、多段攻击等)?
3. 在多人游戏中,如何同步角色的攻击状态和伤害计算?
4. 如何优化大量怪兽的检测和状态更新性能?</think># 添加怪兽攻击功能实现方案
我将为游戏添加怪兽攻击功能,当怪兽与英雄距离不超过怪兽尺寸时,怪兽将进入攻击状态并播放攻击动画。
## 解决方案
### 1. 修改 MonsterController.ts 添加攻击状态处理
```typescript
// MonsterController.ts
import { _decorator, Component, Node, sp, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('MonsterController')
export class MonsterController extends Component {
// 状态属性
public isMove: boolean = false; // 是否行走
public isAttack: boolean = false; // 是否攻击
public isHurt: boolean = false; // 是否受伤
public isStand: boolean = true; // 是否静止(默认状态)
public isDie: boolean = false; // 是否死亡
// 体型属性(单位:像素)
@property({ type: Number, tooltip: "怪兽的碰撞半径(像素)" })
public MonsterSize: number = 40; // 默认值40像素
// Spine动画组件引用
private spineComponent: sp.Skeleton | null = null;
// 动画名称配置
private animationNames = {
move: "move",
stand: "m_stand_1",
attack: "m_attack_1_a", // 添加攻击动画
hurt: "hurt",
die: "die"
};
// 攻击冷却时间
private attackCooldown: number = 0;
private attackInterval: number = 2.0; // 攻击间隔(秒)
protected onLoad(): void {
// 获取Spine动画组件
this.spineComponent = this.node.getComponent(sp.Skeleton);
if (!this.spineComponent) {
console.warn(`[${this.node.name}] No sp.Skeleton component found!`);
}
// 初始播放站立动画
this.playAnimation(this.animationNames.stand);
// 设置攻击动画结束事件监听
this.setupAttackAnimationEvents();
}
// 设置攻击动画事件监听
private setupAttackAnimationEvents() {
if (!this.spineComponent) return;
// 添加动画事件监听
this.spineComponent.setEventListener((trackIndex: number, event: sp.spine.Event) => {
if (event.data.name === "attack_end") {
this.onAttackEnd();
} else if (event.data.name === "attack_hit") {
this.onAttackHit();
}
});
}
// 攻击结束回调
private onAttackEnd() {
// 如果仍然在攻击范围内,保持攻击状态
if (this.isAttack) {
this.playAnimation(this.animationNames.attack, false);
} else {
// 否则回到站立状态
this.setState('stand');
}
}
// 攻击命中回调(这里可以添加伤害计算逻辑)
private onAttackHit() {
console.log(`[${this.node.name}] attack hit!`);
// TODO: 实现伤害计算逻辑
}
// 切换状态的方法
public setState(newState: 'move' | 'attack' | 'hurt' | 'stand' | 'die') {
// 如果当前状态相同,不重复设置
if ((newState === 'move' && this.isMove) ||
(newState === 'attack' && this.isAttack) ||
(newState === 'stand' && this.isStand)) {
return;
}
// 重置所有状态
this.isMove = false;
this.isAttack = false;
this.isHurt = false;
this.isStand = false;
this.isDie = false;
// 设置新状态
switch(newState) {
case 'move':
this.isMove = true;
this.playAnimation(this.animationNames.move);
break;
case 'attack':
// 检查攻击冷却时间
if (this.attackCooldown <= 0) {
this.isAttack = true;
this.playAnimation(this.animationNames.attack, false); // 攻击动画不循环
this.attackCooldown = this.attackInterval; // 重置冷却时间
}
break;
case 'hurt':
this.isHurt = true;
this.playAnimation(this.animationNames.hurt);
break;
case 'stand':
this.isStand = true;
this.playAnimation(this.animationNames.stand);
break;
case 'die':
this.isDie = true;
this.playAnimation(this.animationNames.die, false);
break;
}
console.log(`[${this.node.name}] state changed to: ${newState}`);
}
/**
* 播放指定动画
* @param animationName 动画名称
* @param loop 是否循环播放
*/
private playAnimation(animationName: string, loop: boolean = true) {
if (!this.spineComponent) return;
// 检查动画是否存在
if (!this.spineComponent.findAnimation(animationName)) {
console.warn(`[${this.node.name}] Animation '${animationName}' not found!`);
return;
}
// 播放动画
this.spineComponent.setAnimation(0, animationName, loop);
console.log(`[${this.node.name}] playing animation: ${animationName}${loop ? " (looping)" : ""}`);
}
// 设置体型大小
public setMonsterSize(size: number) {
this.MonsterSize = size;
console.log(`[${this.node.name}] size set to: ${size}px`);
}
// 更新方法
update(deltaTime: number) {
// 更新攻击冷却时间
if (this.attackCooldown > 0) {
this.attackCooldown -= deltaTime;
}
}
}
```
### 2. 修改 GameManager.ts 添加攻击检测逻辑
```typescript
// GameManager.ts
import { _decorator, Component, instantiate, Node, Prefab, Vec3, v3 } from 'cc';
const { ccclass, property } = _decorator;
import { MonsterController } from './MonsterController';
@ccclass('GameManager')
export class GameManager extends Component {
// ... 其他属性保持不变 ...
@property({type: Number, tooltip: "攻击检测距离缓冲值(像素)"})
private attackBuffer: number = 2; // 攻击检测缓冲值
// ... 其他代码保持不变 ...
/**
* 检测所有怪兽与英雄的距离(添加攻击状态检测)
* 添加状态切换缓冲防止频繁切换
*/
private checkMonstersDistance() {
const heroPos = this.HeroNode.position;
for (let i = 0; i < this.MonstersGroup.length; i++) {
const monsterNode = this.MonstersGroup[i];
const controller = monsterNode.getComponent(MonsterController);
if (!controller) continue;
const monsterPos = monsterNode.position;
// 计算距离(忽略Z轴)
const distance = Vec3.distance(
v3(heroPos.x, heroPos.y, 0),
v3(monsterPos.x, monsterPos.y, 0)
);
// 获取怪兽尺寸(半径)
const monsterSize = controller.MonsterSize;
// 1. 首先检测攻击状态(优先级最高)
// 如果距离在攻击范围内(尺寸 + 缓冲值)
if (distance <= monsterSize + this.attackBuffer) {
// 确保怪兽没有死亡或处于其他状态
if (!controller.isDie && !controller.isHurt) {
controller.setState('attack');
// 更新朝向(攻击时也要面向英雄)
this.updateMonsterDirection(monsterNode);
continue; // 跳过后续的移动和站立判断
}
}
// 2. 移动/站立状态检测
// 添加状态切换缓冲
const shouldMove = distance > (monsterSize + this.stateChangeBuffer);
const shouldStand = distance < (monsterSize - this.stateChangeBuffer);
// 如果距离大于怪兽尺寸+缓冲值,设置移动状态
if (shouldMove) {
// 确保怪兽没有死亡或处于其他状态
if (!controller.isDie && !controller.isAttack && !controller.isHurt) {
controller.setState('move');
}
}
// 如果距离小于尺寸-缓冲值,停止移动
else if (shouldStand) {
if (controller.isMove) {
controller.setState('stand');
}
}
// 在缓冲区内保持当前状态
// 更新朝向(距离变化后可能需要调整)
this.updateMonsterDirection(monsterNode);
}
}
// ... 其他代码保持不变 ...
}
```
## 实现说明
### 1. 攻击状态管理
- **攻击动画处理**:
- 添加了攻击动画名称 `m_attack_1_a`
- 攻击动画设置为非循环播放(`loop: false`)
- 添加了攻击动画事件监听器,处理攻击结束和攻击命中事件
- **攻击冷却机制**:
- 添加了 `attackCooldown` 和 `attackInterval` 属性
- 每次攻击后设置冷却时间,防止连续攻击
- 在 `update` 方法中更新冷却时间
- **动画事件处理**:
- `onAttackEnd`:攻击动画结束时调用,决定后续状态
- `onAttackHit`:攻击命中时调用,用于伤害计算(需后续实现)
### 2. 攻击检测逻辑
- **优先级处理**:
- 攻击状态检测优先级最高,先于移动/站立状态检测
- 当检测到攻击条件时,跳过其他状态检测
- **攻击范围计算**:
- 使用 `monsterSize + attackBuffer` 作为攻击范围
- 添加缓冲值防止在边界处频繁切换状态
- **状态转换**:
- 当怪兽进入攻击范围时,调用 `controller.setState('attack')`
- 攻击状态会强制中断移动状态
### 3. Spine动画事件设置
为了使攻击动画事件正常工作,需要在Spine编辑器中设置事件:
1. 在攻击动画的关键帧添加事件
2. 设置两个关键事件:
- `attack_hit`:攻击命中时刻
- `attack_end`:攻击动画结束时刻
## 完整工作流程
1. **距离检测**:
- GameManager定期检测所有怪兽与英雄的距离
- 当距离 ≤ 怪兽尺寸 + 攻击缓冲值时,进入攻击状态
2. **攻击状态切换**:
- 调用 `MonsterController.setState('attack')`
- 播放攻击动画 `m_attack_1_a`(非循环)
3. **攻击动画处理**:
- 在攻击命中时刻触发 `attack_hit` 事件,调用 `onAttackHit`
- 在攻击结束时刻触发 `attack_end` 事件,调用 `onAttackEnd`
4. **攻击结束处理**:
- 如果仍然在攻击范围内,重新播放攻击动画
- 如果不在攻击范围内,切换到站立状态
5. **攻击冷却**:
- 每次攻击后设置冷却时间
- 冷却时间内无法再次攻击
## 高级功能扩展
### 1. 多段攻击支持
```typescript
// MonsterController.ts
// 添加多段攻击支持
private attackCombo: number = 0;
private maxAttackCombo: number = 3; // 最大连击段数
public setState(newState: 'move' | 'attack' | 'hurt' | 'stand' | 'die') {
// ...
case 'attack':
if (this.attackCooldown <= 0) {
this.isAttack = true;
// 确定要播放的攻击动画
let attackAnim = this.animationNames.attack;
if (this.attackCombo > 0 && this.attackCombo < this.maxAttackCombo) {
attackAnim = `${this.animationNames.attack}_${this.attackCombo + 1}`;
}
this.playAnimation(attackAnim, false);
this.attackCombo = (this.attackCombo + 1) % this.maxAttackCombo;
this.attackCooldown = this.attackInterval;
}
break;
// ...
}
private onAttackEnd() {
// 如果仍然在攻击范围内且连击未结束,继续下一段攻击
if (this.isAttack && this.attackCombo > 0) {
let nextAttackAnim = `${this.animationNames.attack}_${this.attackCombo + 1}`;
this.playAnimation(nextAttackAnim, false);
}
// 否则重置状态
else {
this.attackCombo = 0;
if (!this.isAttack) {
this.setState('stand');
}
}
}
```
### 2. 攻击伤害区域检测
```typescript
// GameManager.ts
/**
* 检测攻击命中
*/
private checkAttackHit() {
const heroPos = this.HeroNode.position;
for (let i = 0; i < this.MonstersGroup.length; i++) {
const monsterNode = this.MonstersGroup[i];
const controller = monsterNode.getComponent(MonsterController);
if (!controller || !controller.isAttack) continue;
const monsterPos = monsterNode.position;
const distance = Vec3.distance(
v3(heroPos.x, heroPos.y, 0),
v3(monsterPos.x, monsterPos.y, 0)
);
// 获取怪兽攻击范围(可能比普通尺寸大)
const attackRange = controller.MonsterSize * 1.2;
// 如果英雄在攻击范围内
if (distance <= attackRange) {
// TODO: 应用伤害
console.log(`[${monsterNode.name}] hit hero!`);
}
}
}
// 在update方法中调用
update(deltaTime: number) {
// ... 其他代码 ...
// 检测攻击命中
this.checkAttackHit();
}
```
### 3. 攻击方向优化
```typescript
// GameManager.ts
// 更新怪物朝向(优化攻击方向)
private updateMonsterDirection(monster: Node) {
const heroPos = this.HeroNode.position;
const monsterPos = monster.position;
// 获取当前缩放
const currentScale = monster.getScale();
const absScaleX = Math.abs(currentScale.x);
// 创建新的缩放向量
const newScale = new Vec3();
// 根据位置关系调整朝向
if (monsterPos.x > heroPos.x) {
// 在英雄右侧,需要向左转
newScale.set(-absScaleX, currentScale.y, currentScale.z);
} else {
// 在英雄左侧,保持向右
newScale.set(absScaleX, currentScale.y, currentScale.z);
}
// 特殊处理:攻击状态下微调方向
const controller = monster.getComponent(MonsterController);
if (controller && controller.isAttack) {
// 计算方向向量
const direction = new Vec3(
heroPos.x - monsterPos.x,
heroPos.y - monsterPos.y,
0
);
Vec3.normalize(direction, direction);
// 根据方向微调旋转(可选)
// const angle = Math.atan2(direction.y, direction.x) * 180 / Math.PI;
// monster.setRotationFromEuler(0, 0, angle);
}
monster.setScale(newScale);
}
```
## 测试建议
1. **基础测试**:
- 生成怪兽并移动到英雄附近
- 验证当距离 ≤ 怪兽尺寸时是否进入攻击状态
- 检查攻击动画是否正确播放
2. **边界测试**:
- 测试在攻击边界处的状态切换
- 验证攻击缓冲值是否生效
3. **冷却测试**:
- 验证攻击冷却机制是否正常工作
- 检查连续攻击时是否遵守冷却时间
4. **动画事件测试**:
- 验证 `attack_hit` 和 `attack_end` 事件是否正确触发
- 检查攻击命中逻辑是否按预期工作