制作横版游戏KillBear第6课:添加敌人 简单AI实现
在上一课中,我们学习了为英雄添加血条和攻击功能,具体是在状态层加入血条,并添加了一个攻击按键。本篇将在前面内容的基础上添加敌人,并通过有限状态机(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受伤,生命值归零则会死亡。具体的属性,如生命值、攻击力等,可参考上述代码。