How to Make a Platformer Game with Cocos2D-X

Artificial Intelligence

Creating artificial intelligence (AI) for your game can be one of the most rewarding parts of development. It is the icing on the cake, the cherry on top of the pie. Seeing your creations come to life, and being able to guide them is pure satisfaction.

In the screenshot above, we see the hero Billy running away from an evil wizard. The wizards can fire large, red, damaging, magic orbs which fly slowly towards their destination. The wizards themselves wink in and out of existence, warping from one place in the level to the next, making them enigmatic and difficult to defeat. Another type of AI is the bat. The bat hangs upside down from ceilings, waiting for the hero to approach. When close enough, the bat wakes up and swoops down toward the hero.

In this chapter, we'll take a look at how to develop simple AI like these, so you can bring your own game to life.

Moods

To begin coding AI, think of the different possible moods your enemy will experience. This approach helps to compartmentalize the task at hand. Essentially it's the good ol' divide and conquer strategy.

For example, the bat has three different moods: 1. Sleep - Wait for the player to get close 2. Seek - Awake and get ready to swoop 3. Attack - Swoop down towards the player

enum
{
  kMoodSleep,
  kMoodSeek,
  kMoodAttack
};

We'll create a basic AI superclass and then subclass out the individual enemies. Each mood will have its own self-contained function which runs the AI while in that particular mood.

Timers

Each mood has a timer. When a mood function is first called, the timer is set the amount of time the AI will need in that mood.

When the mood timer counts down to zero, the AI calls the chooseMood function, which decides which mood to set next.

Now that we have an outline on how to proceed, let's begin coding.

class AI : public Character
{
  private:
    typedef Character super;
    typedef AI self;
  
  protected:
    /// The current mood of this AI
    int mood;
    
    /// A timer used in mood functions
    float timer;
    
    /// When the player is close enough, this object will wake up
    float wakeDistanceSQ;
    
    /// Return the amount of time this AI will sleep for
    virtual float sleepTime();

    /// Set the AI's mood
    virtual void setMood(int newMood);

    /// Automatically choose a new mood
    virtual void chooseMood();

    /// Returns true if the AI can wake up
    virtual bool canWakeup();

  public:
    AI();
    virtual ~AI();
    
    /// Allow the AI to think & move
    virtual void update(float delta);
    
    /// Run the sleep mood
    virtual void moodSleep(float delta);

    /// Run the seek mood (find the player)
    virtual void moodSeek(float delta);

    /// Run the attack mood
    virtual void moodAttack(float delta);
};
const float kTimerUp = -9999.0f;

const float kTimerUp = -9999.0f;

AI::AI()
{
  mood = kMoodSleep;
  timer = kTimerUp;
  wakeDistanceSQ = 400.0f * 300.0f;
}

AI::~AI()
{
}

float AI::sleepTime()
{
  // return the amount of time this AI will sleep for;
  // this function is in place so different types of sub classes
  // can sleep for different amounts of time
  return 0.5f;
}

void AI::setMood(int newMood)
{
  // set new Mood
  mood = newMood;
  
  // reset the timer so the mood initializes itself
  timer = kTimerUp;
}

void AI::chooseMood()
{
  // this is the default chooseMood method
  // override it to implement different enemy personalities
  
  // choose between seeking and sleeping
  this->setMood(this->canWakeup() ? kMoodSeek : kMoodSleep);
}

bool AI::canWakeup()
{
  // can wake if player is alive and is close enough
  Player& player = this->getPlayer(); // inherited from LevelObject
  
  // note that player distance is saved for later use
  float playerDistanceSQ = this->distanceSQ(player.getPosition());
  
  return (player.isAlive() && playerDistanceSQ < wakeDistanceSQ);
}

void AI::update(float delta)
{
  super::update(delta);
  
  if (this->isAlive())
  {
    // count down the timer
    if (timer != kTimerUp)
      timer -= delta;

    // perform current mood
    switch (mood)
    {
      case kMoodSleep: this->moodSleep(delta); break;
      case kMoodSeek: this->moodSeek(delta); break;
      case kMoodAttack: this->moodAttack(delta); break;
    }
  }
}

void AI::moodSleep(float delta)
{
  // starting sleep mood...
  if (timer == kTimerUp)
  {
    timer = this->sleepTime() + (this->sleepTime() * CCRANDOM_0_1());
    this->sprite->stopAllActions();
    this->animate("sleep", kActionSleep);
  }
  
  // wake up?
  else if (timer <= 0.0f)
    this->chooseMood();
}

void AI::moodSeek(float delta)
{
  // starting seek mood...
  if (timer == kTimerUp)
    timer = 1.0f;
  
  // timer up
  else if (timer <= 0.0f)
    this->chooseMood();
}

void AI::moodAttack(float delta)
{
  // starting attack mood
  if (timer == kTimerUp)
    timer = 1.0f;
  
  // timer up
  else if (timer <= 0.0f)
    this->chooseMood();
}

The first bit to grasp in the above code is the update function. If the AI is alive, it counts down the timer and calls the current mood function.

Within each mood function, it sets the initial timer if necessary. If the timer has expired, it calls the chooseMood function which sleeps if the player is too far away, or wakes up if not.

The Bat Enemy AI

Now let's take a look at the specific AI for the bat. Simply subclass AI and override the mood functions to make the bat unique.

In the sleep mood, the bat simply hangs on the ceiling. Apply a linear impulse which is greater than gravity to keep the bat hanging:

void Bat::moodSleep(float delta)
{
  super::moodSleep(delta);

  // counteract gravity
  body->ApplyLinearImpulse(
    b2Vec2(0.0f, 100.0f * delta),
    body->GetWorldCenter(),
    true
  );
}

In the seek mood, the bat simply opens its eyes in warning, prepared to swoop down. A timer of one second is used, an animation is played and the same linear impulse is applied to keep the bat on the ceiling:

void Bat::moodSeek(float delta)
{
  if (timer == kTimerUp)
  {
    // open up the Bat's eyes and get ready to swoop
    timer = 1.0f;
    this->sprite->stopAllActions();
    this->animate("rest", kActionRest);
  }
  else if (timer <= 0.0f)
    this->setMood(kMoodAttack);
  
  // counteract gravity
  body->ApplyLinearImpulse(
    b2Vec2(0.0f, 100.0f * delta),
    body->GetWorldCenter(),
    true
  );
}

Now for the attack mood. The bat animates flapping wings, plays a sound, then decides whether to swoop left or right depending on the player position.

For one second, the bat swoops down and for another, the bat swoops back up. Both of these motions are controlled by linear impulses which are either slightly stronger or weaker than gravity. This turns the swoop into a nice curve:

void Bat::moodAttack(float delta)
{
  if (timer == kTimerUp)
  {
    timer = 2.0f;
    
    // animate
    this->sprite->stopAllActions();
    this->animate("fly", kActionAttack);
    this->profile->playSound("fly");

    // determine swoop direction
    auto& player = this->getPlayer();
    if (player.getPositionX() > this->getPositionX())
    {
      swoopX = 3.0f;
      this->sprite->setFlipX(false);
    }
    else
    {
      swoopX = -3.0f;
      this->sprite->setFlipX(true);
    }
  }
  
  // mood finished
  else if (timer <= 0.0f)
  {
    this->setMood(kMoodSleep);
  }

  // swooping up
  else if (timer <= 1.0f)
  {
    body->ApplyLinearImpulse(
      b2Vec2((swoopX / 2.0f) * delta, 55.0f * delta),
      body->GetWorldCenter(),
      true
    );
    
    // keep swooping until we hit the ceiling
    if (!hasContactAbove)
      timer = 1.0f;
  }

  // swooping down
  else if(timer <= 2.0f)
  {
    body->ApplyLinearImpulse(
      b2Vec2(swoopX * delta, 40.0f * delta),
      body->GetWorldCenter(),
      true
    );
    
    if (hasContactBelow)
      timer = 1.0f;
  }
}

That's it for the bat AI.

Conclusion

The moods for the wizard are quite similar:

  1. Sleep - Wait for the player to get close enough
  2. Seek - Find a random position near the player and warp there
  3. Attack - Launch a magic orb at the player, then go back to sleep

Imagine the AI -- whether it be an enemy or non-playing-character (NPC) -- that you'd like to create for your game. What moods will it have?

The easiest way to start is with simple sleeping and seeking. Add the attack mood when you've got that mastered. Good luck and leave a comment when you've brought your first AI to life!

Got questions? Leave a comment below. You can also subscribe to be notified when we release new chapters.

Next Chapter >

Comments


Comments
  1. MD

    One comment — I wouldn’t use floats for time-related variables in a game; better use doubles instead. Read “Don’t Store That in a Float” by Bruce Dawson to see why (can’t post the URL, but it’s easy to find).

    • Nat Weiss

      Yeah, and if you’re writing a multiplayer game, stick to ints. I’ve seen two devices return different results adding 0.1 to a double (float failed too) over time.

  2. hungnt

    can I help you?
    a problem abount sensor in box2d? for example about it.
    thanks so much.