制作横版游戏KillBear第6课:添加敌人 简单AI实现

2015年03月22日 09:18 0 点赞 0 评论 更新于 2025-11-21 18:03

在上一课中,我们学习了为英雄添加血条和攻击功能,具体是在状态层加入血条,并添加了一个攻击按键。本篇将在前面内容的基础上添加敌人,并通过有限状态机(FSM)实现简单的AI。

开发环境

  • Win64 : vs2010
  • Cocos2d-x v3.4Final
  • TexturePackerGUI
  • MapEdit

代码实现

角色类设计

创建一个继承自基础 Role 类的 Enemy 类作为敌人。

Enemy.h 文件

typedef enum {
AI_IDLE = 0,
AI_PATROL,
AI_ATTACK,
AI_PURSUIT
} AiState;

class Enemy : public Role {
public:
Enemy();
~Enemy();
bool init();
void updateSelf();
CREATE_FUNC(Enemy);
CC_SYNTHESIZE(cocos2d::Vec2, m_moveDirection, MoveDirection);
CC_SYNTHESIZE(float, m_eyeArea, EyeArea);
CC_SYNTHESIZE(float, m_attackArea, AttackArea);
CC_SYNTHESIZE(AiState, m_aiState, AiState);

private:
void decide(const cocos2d::Vec2& target, float targetBodyWidth);
void execute(const cocos2d::Vec2& target, float targetBodyWidth);
unsigned int m_nextDecisionTime;
};

这里定义了 AiState 枚举类型作为AI状态类型。由于敌人是AI,我们需要为其设定一些区域,如视野、最大攻击判定区、AI状态等。其他代码与 Hero 类大同小异。

Enemy.cpp 文件

bool Enemy::init() {
if (!Role::init()) {
return false;
}
Animation *idleAnim = this->createNomalAnimation("bear_idle_d.png", 3, 6);
this->setIdleAction(RepeatForever::create(Animate::create(idleAnim)));
// 其他动画和初始化代码...
return true;
}

void Enemy::updateSelf() {
this->execute(global->hero->getPosition(), global->hero->getBodyBox().actual.size.width);

if (this->getCurrActionState() == ACTION_STATE_WALK) {
Vec2 location = this->getPosition();
Vec2 direction = this->getMoveDirection();
Vec2 expectP = location + direction;
float maptileHeight = global->tileMap->getTileSize().height;

if (expectP.y < 0 || expectP.y > maptileHeight * 3) {
direction.y = 0;
}
this->setFlippedX(direction.x < 0 ? true : false);
this->setPosition(location + direction);
this->updateBoxes();
this->setLocalZOrder(this->getPositionY());
}

if (this->getCurrActionState() == ACTION_STATE_NOMAL_ATTACK_A) {
this->runNomalAttackA();
}
}

void Enemy::execute(const Vec2& target, float targetBodyWidth) {
if (m_nextDecisionTime == 0) {
this->decide(target, targetBodyWidth);
} else {
--m_nextDecisionTime;
}
}

void Enemy::decide(const Vec2& target, float targetBodyWidth) {
Vec2 location = this->getPosition();
float distance = location.getDistance(target);
distance = distance - targetBodyWidth / 2;
bool isFlippedX = this->isFlippedX();
bool isOnTargetLeft = (location.x < target.x ? true : false);

if ((isFlippedX && isOnTargetLeft) || (!isFlippedX && !isOnTargetLeft)) {
this->m_aiState = CCRANDOM_0_1() > 0.5f ? AI_PATROL : AI_IDLE;
} else {
if (distance < m_eyeArea) {
this->m_aiState = (distance < m_attackArea) && (fabsf(location.y - target.y) < 15) ? AI_ATTACK : AI_PURSUIT;
} else {
this->m_aiState = CCRANDOM_0_1() > 0.5f ? AI_PATROL : AI_IDLE;
}
}

switch (m_aiState) {
case AI_ATTACK: {
this->runNomalAttackA();
this->m_nextDecisionTime = 50;
break;
}
case AI_IDLE: {
this->runIdleAction();
this->m_nextDecisionTime = CCRANDOM_0_1() * 100;
break;
}
case AI_PATROL: {
this->runWalkAction();
this->m_moveDirection.x = CCRANDOM_MINUS1_1();
this->m_moveDirection.y = CCRANDOM_MINUS1_1();
m_moveDirection.x = m_moveDirection.x > 0 ? (m_moveDirection.x + velocity.x) : (m_moveDirection.x - velocity.x);
m_moveDirection.y = m_moveDirection.y > 0 ? (m_moveDirection.y + velocity.y) : (m_moveDirection.y - velocity.y);
this->m_nextDecisionTime = CCRANDOM_0_1() * 100;
break;
}
case AI_PURSUIT: {
this->runWalkAction();
this->m_moveDirection = (target - location).getNormalized();
this->setFlippedX(m_moveDirection.x < 0 ? true : false);
m_moveDirection.x = m_moveDirection.x > 0 ? (m_moveDirection.x + velocity.x) : (m_moveDirection.x - velocity.x);
m_moveDirection.y = m_moveDirection.y > 0 ? (m_moveDirection.y + velocity.y) : (m_moveDirection.y - velocity.y);
this->m_nextDecisionTime = 10;
break;
}
}
}

distance 用于判断敌人和目标之间的距离。代码中使用了几个随机宏,目的是更真实地表现敌人的行为。AI的几种状态及对应延时说明如下:

  • Attack(攻击):每次攻击延时50。
  • Idle(发呆):延时为随机一个0 - 1的数字乘以100。
  • Patrol(巡逻):延时时间也是随机生成的。
  • Pursuit(追击):当发现Hero时进行追击的判断。

主要的AI逻辑为:先判断目标是否出现在正前方(根据视野范围),若出现则选择发呆或巡逻;再判断是否在攻击范围内(根据攻击范围),若在则选择追击或攻击。

在GameLayer中加入敌人

为了方便管理多个敌人,我们使用数组(链表会更好)来实现。

GameLayer.h 文件

#include "Enemy.h"
// 其他头文件...

class GameLayer {
public:
void addEnemies(int number);
void updateEnemies(float dt);
private:
__Array *m_pEnemies;
};

GameLayer.cpp 文件

void GameLayer::addEnemies(int number) {
m_pEnemies = __Array::createWithCapacity(number);
m_pEnemies->retain();
for (int i = 0; i < number; i++) {
Enemy *pEnemy = Enemy::create();
pEnemy->setPosition(Vec2(random(_visibleSize.width / 2, _visibleSize.width), 70));
pEnemy->runIdleAction();
pEnemy->setLocalZOrder(_visibleSize.height - pEnemy->getPositionY());
// 属性设置
pEnemy->setVelocity(Vec2(0.5f, 0.5f));
pEnemy->setEyeArea(300);
pEnemy->setAttackArea(80);
pEnemy->setDamageStrenth(5);
pEnemy->setSumLifeValue(100);
pEnemy->setCurtLifeValue(m_pHero->getSumLifeValue());
m_pEnemies->addObject(pEnemy);
this->addChild(pEnemy, 0);
}
global->enemies = m_pEnemies;
}

void GameLayer::updateEnemies(float dt) {
Ref *Obj = NULL;
CCARRAY_FOREACH(m_pEnemies, Obj) {
Enemy *pEnemy = (Enemy*)Obj;
pEnemy->updateSelf();
if (pEnemy->getDeadAction()->isDone()) {
m_pEnemies->removeObject(pEnemy);
}
}
}

void GameLayer::update(float dt) {
this->updateHero(dt);
this->updateEnemies(dt);
}

addEnemies 函数中,我们一次创建多个敌人并添加到数组中,最后将该数组注册到 Global 中,以便下一章做攻击判断使用。

总结

本文实现了添加多个敌人,并为敌人设定了简单的AI,使其能够自动随机巡逻、追击或攻击Hero。不过目前仅实现了各种动画,尚未实现攻击判定。下一章我们将通过攻击判定,让Hero或Enemy受伤,生命值归零则会死亡。具体的属性,如生命值、攻击力等,可参考上述代码。

作者信息

boke

boke

共发布了 3994 篇文章