Got questions? Leave a comment below. You can also subscribe to be notified when we release new chapters.
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
(contact->GetFixtureA()->GetBody()->GetUserData());
LevelObject* B = static_cast
(contact->GetFixtureB()->GetBody()->GetUserData());
// turn fixtures into player
Player* player = dynamic_cast(A);
LevelObject* other = B;
if (player == nullptr)
{
player = dynamic_cast(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(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(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.
[1]: /book/cocos2d-x/artificial-intelligence/