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<Touch*>& touches, Event* event);
    virtual void onTouchesMoved(const vector<Touch*>& touches, Event* event);
    virtual void onTouchesEnded(const vector<Touch*>& touches, Event* event);
    virtual void onTouchesCancelled(const vector<Touch*>& 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<Touch*>& 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<LevelObject*>(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.

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

Next Chapter >

Comments


Comments
  1. Waqas Khan

    Thanks. Waiting for Ads section badly !

  2. Arun

    Thanks man, awesome tutorials:D
    Can’t w8 for the next one!

  3. Felipe Bormann

    Good tutorials, but could you do how to organize the update and also the scene manager on cocos2d-x? thank you bro.