How to Make a Platformer Game with Cocos2D-X

Touch Tutorial & Player Movement

Now that you've got a physical world going with Box2d, you're only a step away from moving the player. In this chapter, we'll look at how to accept player input, move the player left and right, jump and even wall jump. ### Input Events Accepting input with Cocos2d-X is simple. You create a `Layer` object (or subclass) that implements onTouchesBegan / Moved / Ended / Cancelled functions: class LevelLayer : public Layer { public: typedef Layer super; typedef LevelLayer self; LevelLayer(); virtual ~LevelLayer(); virtual void onTouchesBegan(const vector& touches, Event* event); virtual void onTouchesMoved(const vector& touches, Event* event); virtual void onTouchesEnded(const vector& touches, Event* event); virtual void onTouchesCancelled(const vector& touches, Event* event); }; Then listen for touch events: LevelLayer::LevelLayer() { auto listener = EventListenerTouchAllAtOnce::create(); listener->onTouchesBegan = CC_CALLBACK_2(self::onTouchesBegan, this); listener->onTouchesMoved = CC_CALLBACK_2(self::onTouchesMoved, this); listener->onTouchesEnded = CC_CALLBACK_2(self::onTouchesEnded, this); listener->onTouchesCancelled = CC_CALLBACK_2(self::onTouchesCancelled, this); auto dispatcher = this->getEventDispatcher(); dispatcher->addEventListenerWithSceneGraphPriority(listener, this); } Since multi-touch being enabled is more versatile, make sure your iOS project's EAGLView has it set: [myCCEAGLView setMultipleTouchEnabled:YES]; Having enabled touches, you will begin to receive the `onTouches` callbacks whenever a touch event happens. You'll get `onTouchesBegan` when the input starts, `onTouchesMoved` repeatedly as the touch moves, and `onTouchesEnded` when the touch ends. You'll also receive `onTouchesCancelled` if the player's finger moves off-screen. It's okay to treat `onTouchesCancelled` like `onTouchesEnded`. On Mac, Windows and Linux, you'll actually be receiving mouse input. However, it will still come to your `Layer` as `onTouchesBegan`, etc. This makes it easy to program your game cross-platform. You only have to write one set of input receiving functions. ### Quadrants You can respond to the touch events as you see fit. Like, if the bottom-left quadrant is tapped, the player walks left. Tapping the bottom-right quadrant walks the player right. Tapping the upper quadrants makes the player jump. Landing on an enemy is how the player attacks.
In the following code, the touches are iterated and the `Player` class is informed of the latest touch position. inline Point locationInGLFromTouch(Touch& touch) { auto director = Director::getInstance(); return director->convertToGL(touch.getLocationInView()); } void LevelLayer::onTouchesEnded(const vector& touches, Event* event) { for (auto& touch : touches) { if (touch) player->touch(locationInGLFromTouch(*touch)); } } With the input hooked up, it's as simple as applying impulses to the `Player`'s Box2d body depending on which quadrant was tapped. void Player::touch(const Point& location) { auto winSize = Director::getInstance()->getWinSize(); b2Vec2 currentVelocity = body->GetLinearVelocity(); b2Vec2 impulse(0.0f,0.0f); // walk if (location.y < (winSize.height * 0.5f)) { // apply impulse if x velocity is getting low if (fabsf(currentVelocity.x) < 5.0f) { impulse.y = 0.0f; impulse.x = 200.0f * delta; if (location.x < (winSize.width * 0.5f)) impulse.x = -impulse.x; this->body->ApplyLinearImpulse(impulse, body->GetWorldCenter(), true); } } // jump else { // apply impulse impulse.y = 1300.0f * delta; impulse.x = 30.0f * delta; if (location.x < (winSize.width * 0.5f)) impulse.x = -impulse.x; this->body->ApplyLinearImpulse(impulse, body->GetWorldCenter(), true); } } This gives you the ability to move the player around, but there's a few problems. First, the player can walk left or right even while they are in the air. Second, the player can continue to jump while airborne. While double jumping is pretty cool, we want to be able to limit the amount of jumps or constrain the player to only jumping while having contact with the level below. ### Detecting Contacts We need a way to detect if the player currently has contact with a fixture. Box2d makes this easy for us by giving us a method to detect the body's contacts. Iterate over the contacts with `Body->GetContactList()` and check the contact's position versus the player's body position. Set some boolean flags so when your `Player` class receives input, it knows whether there is contact below, to the side, etc. void LevelObject::updateContacts() { const float kPixelsPerMeter = 32.0f; float heightFactor = this->sprite->getContentSize().height; heightFactor /= kPixelsPerMeter / 4.0f; // reset the flags hasContactBelow = hasContactAbove = hasContactToTheSide = hasContactWithObject = false; // iterate over the Box2d body's contacts b2ContactEdge* edge = nullptr; for (edge = body->GetContactList(); edge; edge = edge->next) { if (!edge->contact->IsEnabled() || !edge->contact->IsTouching()) continue; // get vector from the body's position to the contact's position b2Vec2 v = edge->other->GetPosition() - body->GetPosition(); // set flags based on vector if (fabsf(v.x) > heightFactor) hasContactToTheSide = true; else if (v.y > heightFactor) hasContactAbove = true; else if (v.y < -heightFactor) hasContactBelow = true; // check if it's an object void* userData = edge->other->GetUserData(); if (userData) { auto o = static_cast(userData); if (o) hasContactWithObject = true; } } } In your `Player` class' `tick` method, you call `updateContacts()` and from then on you will always have these flags set correctly when a touch event happens. You can now buff up the `Player::touch()` method with the constraints: // walk if( location.y < (winSize.height * 0.5f) && hasContactBelow ) { // ... } // jump else if( hasContactBelow ) { // ... } ### Double Jumping & Wall Jumps If you want to implement a finite amount of double jumps, just create a member variable for how many consecutive jumps have occurred, reset it whenever the player has contact below, increment when the player jumps, and constrain the jumps by a finite number. If you want to implement wall jumps, just use the `hasContactToTheSide` flag as an additional constraint for jumping. [1]: /book/cocos2d-x/box2d-begincontact-sensor-fixtures/

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

Next Chapter >