How to Make a Platformer Game with Cocos2D-X

Powerups and Items

In a side-scrolling platformer game, it can be really fun to add items, powerups, trampolines and other objects that interact with the player.

Trampolines

If you've ever played Sonic the Hedgehog, you'll know what I mean by trampolines. Trampolines are anything that launches the player in a certain direction. In the case of the exploding barrels, they launch the player directly upwards.

To detect whether the player is making contact with an item or other object, there's generally one of two ways: 1. Set a contact listener for your b2World object, then sort through the BeginContact and EndContact messages looking for meaningful collisions. This will work with solid objects that the player cannot pass through. 2. Create a sensor fixture that the player can pass through, but will trigger BeginContact and EndContact messages. These are good for non-solid objects.

b2ContactListener

Both of the above mentioned items are solid fixtures, so let's explore how to create a Box2d contact listener. First, you'll need a class that inherits from b2ContactListener and overrides BeginContact and EndContact:

class Level : public Node, public b2ContactListener
{
  public:
    // ...
    
    void BeginContact(b2Contact* contact);
    void EndContact(b2Contact* contact);

    // ...
};

In a previous chapter, we discussed creating dynamic bodies like the player. When the body is created, a user data pointer is set to the containing LevelObject:

void LevelObject::addBodyToWorld(b2World* world)
{
  b2BodyDef bodyDef;
  bodyDef.type = b2_dynamicBody;
  bodyDef.userData = this;
  this->body = world->CreateBody(&bodyDef);
}

Having that user data pointer makes for convenient access to the LevelObject when contact occurs:

void Level::BeginContact(b2Contact* contact)
{
  // call the appropriate LevelObject's beginContact function
  if (contact && contact->IsTouching())
  {
    // turn fixtures into level objects
    LevelObject* A = static_cast<LevelObject*>
      (contact->GetFixtureA()->GetBody()->GetUserData());
    LevelObject* B = static_cast<LevelObject*>
      (contact->GetFixtureB()->GetBody()->GetUserData());
    
    // turn fixtures into player
    Player* player = dynamic_cast<Player*>(A);
    LevelObject* other = B;
    if (player == nullptr)
    {
      player = dynamic_cast<Player*>(B);
      other = A;
    }
    
    // call the begin contact
    if (A && B)
    {
      // always prefer to call the player's version of beginContact
      if (player && other)
        player->beginContact(other, contact);
      else
        A->beginContact(B, contact);
    }
  }
}

void Level::EndContact(b2Contact* contact)
{
  // nothing to do here for the current implementation
}

Essentially, all the above code does is convert the b2World's BeginContact message into a LevelObject::beginContact() that can be overridden. It prefers to call the Player object's version, taking the other level object as a parameter.

Attack!

Player::beginContact searches through its body's current contacts, looking for the object that triggered the contact. This reveals the point of collision, which can be used to see which object is higher. If the player is higher than the object, the contact is considered to be the player's attack.

void Player::beginContact(LevelObject* otherObject,b2Contact* contact)
{
  const float kPixelsPerMeter = 32.0f;
  
  // search contact list for this object
  b2ContactEdge* edge = body->GetContactList();
  for ( ; edge; edge = edge->next)
  {
    if (otherObject == static_cast<LevelObject*>(edge->other->GetUserData()))
    {
      // are we landing on top of this object?
      float heightFactor = this->sprite->getContentSize().height
        / kPixelsPerMeter / 4.0f;
      b2Vec2 othersPosition = edge->other->GetPosition();
      
      // if so, attack the object
      if (othersPosition.y < (body->GetPosition().y - heightFactor))
        otherObject->attackedBy(this);
      
      // else, we are being attacked else
      this->attackedBy(otherObject);
    }
  }
}

Landing on an enemy does damage. Landing on a barrel launches the player into the air. Here's the Barrel class' attackedBy function, which optionally can destroy or re-use the barrel:

void Barrel::attackedBy(LevelObject* o)
{
  Player* player = dynamic_cast<Player*>(o);
  if (!this->open && player)
  {
    bool destroyable = false;
    this->profile->playSound("open");
    
    // bounce the player in the air
    player->setVelocity(Point(0.0f,0.0f));
    player->applyImpulse(Point(0.0f,50.0f));
    
    // flip in the air
    if (player->isAlive())
      player->animateFlip(player->getVelocity().x < 0.0f);
    
    // destroy the barrel
    if (destroyable)
    {
      this->open = true;
      this->setIsSolid(false);
      this->animate("animation", kActionOpen, false);
      this->sprite->runAction(Sequence::create(
        DelayTime::create(1.0f),
        CallFunc::create(this, callfunc_selector(self::removeFromLevel)),
        nullptr
      ));
    }
    
    // re-use the barrel
    else
    {
      Animation* anim = this->profile->animationForKey("animation");
      if (anim)
      {
        this->sprite->stopAllActions();
        this->sprite->runAction(Sequence::create(
          Animate::create(anim),
          Animate::create(anim)->reverse(),
          CallFunc::create(this, callfunc_selector(self::setFirstFrame)),
          nullptr
        ));
      }
    }
  }
}

The Stone

Okay, so there's really nothing special about the Stone object. All it does is create a dynamic, circular fixture with a high restitution and attach a sprite.

Box2d does everything from then on, allowing the player to move the stone around. Because the stone is a LevelObject it will receive the attackedBy call, essentially absorbing the wizard's magical attacks.

Sensor Fixtures

So what if you wanted to create a non-solid item like a coin that can be picked up on contact? If it's not solid then there are no fixtures, so how would a collision occur to trigger the BeginContact call?

For this situation, a special type of fixture called a sensor can be used. You still create a Box2d body. You just attach a sensor fixture which other objects can pass through, triggering a contact:

enum
{
  kFilterCategoryLevel = 0x01,
  kFilterCategorySolidObject = 0x02,
  kFilterCategoryNonSolidObject = 0x04
};

// create a circular sensor fixture
b2FixtureDef fixtureDef;
b2CircleShape circleShape;
circleShape.m_radius = 16;
fixtureDef.shape = &circleShape;
fixtureDef.isSensor = true;
fixtureDef.filter.categoryBits = kFilterCategoryNonSolidObject;
fixtureDef.filter.maskBits = 0xffff;
body->CreateFixture(&fixtureDef);

Conclusion

There are many other ways to use sensor fixtures and the Begin/EndContact messages. Play around with the code and you'll learn how to create items that interact with the player in your game.

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

Next Chapter >

Comments


Comments
  1. James Matthew

    Thanks man,really appreciate it. Waiting for the nest one:)