Got questions? Leave a comment below. You can also subscribe to be notified when we release new chapters.
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/