Tutorials‎ > ‎

Create Endless Runner Game With Cocos2d-X (Part 2 - Game Logic)

posted Apr 20, 2015, 2:44 AM by Hadi Setiawan   [ updated Aug 15, 2016, 11:27 PM by Surya Wang ]

If you haven’t read the first part of this tutorial, then go ahead and read it first, it will be about how to create the skeletal animation of the hero that we’ll use in this part of tutorial.

Setting Up New Cocos2d-x Project

Assuming that you already have some knowledge about Cocos2d-x, then you should be able to create a new project using the console tool. But if you don’t already learnt about it, then you should read more about Cocos2d-x first, whether from this site’s module, or other sources.

The command we’ll use is cocos new –l cpp tutorial

When the project successfully created, go to the resource folder and copy the exported skeletal animation from part one of this tutorial. And finally, open up the proj.win32 folder and double click the solution file.

First, we’ll have to add two additional library to our solution:

1.       libCocosStudio (located in tutorial\cocos2d\cocos\editor-support\cocostudio)

2.       libGUI (located in tutorial\cocos2d\cocos\ui)

And then add the reference for our tutorial project, by right clicking on the tutorial project and choose property, then click on the Add New Reference button.


When finished adding the reference, next we’ll add additional include directory.

 

Coding the Game

We’ll divide our code into three main parts, AppDelegate, Hero, and GameScene. The HelloWorldScene can be safely deleted as we’ll be using GameScene instead.

Hero

The Hero class we’ll code will extends from the Armature class which is a class used for loading the skeletal animation we created in part one of this tutorial. We’ll add several other methods to make the coding easier, such as the jump, run, and several movement flags.

This is the code of our class Hero.

class Hero : public Armature {
public:
    CREATE_FUNC(Hero);

    virtual bool init() {
        ArmatureDataManager::getInstance()->addArmatureFileInfo("tutorial/tutorial.ExportJson");
        auto size = Director::getInstance()->getVisibleSize();

        setScale(0.2);
        setPositionX(45);
        setPositionY(size.height / 2);

        PhysicsMaterial mat = PHYSICSBODY_MATERIAL_DEFAULT;
        mat.restitution = 0;
        auto body = PhysicsBody::createBox(Size(64, 100), mat);

        body->setRotationEnable(false);
        body->setPositionOffset(Vec2(0, 8));
        body->setContactTestBitmask(1);
        setPhysicsBody(body);

        Armature::init("tutorial");
        getAnimation()->play("Run");

        return true;
    }

    virtual void update(float dt) {
        Armature::update(dt);

        auto body = getPhysicsBody();
        auto anim = getAnimation();
        auto animId = anim->getCurrentMovementID();
 
        auto size = Director::getInstance()->getVisibleSize();
        Vec2 force = body->getVelocity();
 
        if (running) {
            force.x = 400;
        }
 
        body->setVelocity(force);
 
        if (body->getVelocity().x == 0 && animId == "Run")
            anim->play("Idle");
        else if (body->getVelocity().x != 0 && animId == "Idle")
            anim->play("Run");
    }

    void jump() {
        if (!isJumping()) {
            auto vel = getPhysicsBody()->getVelocity();
            vel.y = 600;
            getPhysicsBody()->setVelocity(vel);
            getAnimation()->play("JumpStart");
            jumping = true;
        }
    }

    bool isJumping() {
        return jumping;
    }

    void setJumping(bool jumping) {
        this->jumping = jumping;
    }

    bool isRunning() {
        return running;
    }

    void setRunning(bool running) {
        this->running = running;
    }

private:
    int movementFlag = 0;
    bool jumping = false;
    bool running = false;
};
 

The code is pretty self-explanatory, but do notice the init part. We set the physics body to fixed rotation, so that our hero won’t wildly somersault around.

GameScene

The GameScene is where the real game logic is. In GameScene, we’ll code it so the camera will appear as if it’s following our hero around the game, an endless parallax background that will show as if there’s a different distance between the backgrounds.

Here’s the content of our GameScene class, and also the tags that we’ll use to identify nodes in our scene.

const int TAG_DEFAULT = 0;
const int TAG_HERO = 1;
const int TAG_REWARD = 2;
const int TAG_FLOOR = 3;
const int TAG_HINT = 4;
const int TAG_BG = 100;
const int TAG_BGa = 110;

class GameScene : public Layer {
public:
    CREATE_FUNC(GameScene);

    static Scene* createScene() {
        auto scene = Scene::createWithPhysics();
        scene->addChild(GameScene::create());

        auto world = scene->getPhysicsWorld();
        world->setGravity(Vec2(0, -980));

        return scene;
    }

    virtual bool init() {
        if (!Layer::init()) {
            return false;
        }

        guiNode = Node::create();
        addChild(guiNode);

        hero = Hero::create();
        hero->setTag(TAG_HERO);
        addChild(hero);

        initBackground();
        initFloor();
        initHUD();

        hero->getAnimation()->setMovementEventCallFunc([](Armature* a, MovementEventType type, const std::string& movementId) {
            switch (type)
            {
            case cocostudio::COMPLETE:
                if (movementId == "JumpStart") {
                    a->getAnimation()->play("JumpEnd");
                }
                else if (movementId == "JumpEnd") {
                    a->getAnimation()->play("Run");
                }
                break;
            }
        });

        auto keyboard = EventListenerKeyboard::create();
        keyboard->onKeyPressed = [this](EventKeyboard::KeyCode kc, Event* e) {
            if (kc == EventKeyboard::KeyCode::KEY_SPACE) {
                if (!hero->isRunning()) {
                    hero->setRunning(true);
                    this->getGuiNode()->getChildByTag(TAG_HINT)->removeFromParent();
                }
                else {
                    hero->jump();
                }
            }
        };

        auto listener = EventListenerPhysicsContact::create();
        listener->onContactBegin = [this](PhysicsContact &con) {
            auto tagA = con.getShapeA()->getBody()->getNode()->getTag();
            auto tagB = con.getShapeB()->getBody()->getNode()->getTag();

            if ((tagA == TAG_HERO && tagB == TAG_FLOOR) || (tagB == TAG_HERO || tagA == TAG_FLOOR)) {
                hero->setJumping(false);
            }

            return true;
        };

        this->_eventDispatcher->addEventListenerWithSceneGraphPriority(keyboard, this);
        this->_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);        
        this->scheduleUpdate();

        return true;
    }

    virtual void update(float delta){
        auto size = Director::getInstance()->getVisibleSize();
        static auto heroPos = hero->getPosition();

        // only update chase camera position if not losing
        if (!lose) {
            heroPos = hero->getPosition();
        }

        // simulate chase camera
        Vec2 heroOffset(size.width / 3, size.height / 2);

        this->getScene()->setPosition(-heroPos + heroOffset);
        this->guiNode->setPosition(heroPos - heroOffset);

        // parallax background
        for (int i = 0; i < 3; i++)
        {
            int vel = (0.5f * i) + 1;
            int mult = heroPos.x / vel / bgSize.width;

            Vec2 bgOffset(bgSize.width * 0.5f + mult * bgSize.width, bgSize.height * 0.75f);
            this->guiNode->getChildByTag(TAG_BG + i)->setPosition(bgOffset + (heroPos / -vel));

            bgOffset.x += bgSize.width;
            this->guiNode->getChildByTag(TAG_BGa + i)->setPosition(bgOffset + (heroPos / -vel));
        }

        // update floor position
        auto bound = size.width * 2;
        auto gap = size.width / 2 + 500;

        for each (auto var in this->getChildren())
        {
            if (var->getTag() != TAG_FLOOR) continue;

            auto floorPosX = var->getPositionX();

            if (floorPosX < heroPos.x - bound) {
                auto newPos = floorPosX + gap * 4;
                var->setPositionX(newPos);
            }
        }

        // update score
        char buffer[32];
        sprintf(buffer, "Score: %.0f", hero->getPositionX() / 100);
        labelScore->setString(buffer);

        // check whether the hero falls
        if (hero->getPositionY() < 0) {
            MessageBox("You lose!", "Game Over");
            Director::getInstance()->end();
        }
        else if (hero->getPositionY() < size.height / 4) {
            lose = true;
            hero->getPhysicsBody()->setCollisionBitmask(0);
        }
    }

    void initBackground() {
        for (int i = 2; i >= 0; i--)
        {
            char filename[32];
            sprintf(filename, "bg%d.png", i + 1);

            auto s = Sprite::create(filename);
            s->setTag(TAG_BG + i);
            s->setScale(1.2f);
            this->guiNode->addChild(s);

            s = Sprite::create(filename);
            s->setTag(TAG_BGa + i);
            s->setScale(1.2f);
            this->guiNode->addChild(s);
        }

        bgSize = this->guiNode->getChildByTag(100)->getContentSize() * 1.2f;
    }

    void initFloor() {
        auto size = Director::getInstance()->getVisibleSize();
        auto gap = size.width / 2 + 500;
        auto floorSize = Rect(0, 0, size.width / 2, size.height / 3);

        spawnFloor(Vec2(gap *-1, 0), floorSize);
        spawnFloor(Vec2(gap * 0, 0), floorSize);
        spawnFloor(Vec2(gap * 1, 0), floorSize);
        spawnFloor(Vec2(gap * 2, 0), floorSize);
    }

    void initHUD() {
        auto size = Director::getInstance()->getVisibleSize();

        labelScore = LabelTTF::create("Score: 0", "Calibri", 32);
        labelScore->setColor(ccc3(0, 0, 0));
        labelScore->setPosition(size);
        labelScore->setHorizontalAlignment(TextHAlignment::RIGHT);
        labelScore->setAnchorPoint(Vec2(1.2f, 1.2f));
        guiNode->addChild(labelScore, 1000);
        
        auto temp = LabelTTF::create("Press space to start running", "Calibri", 64);
        temp->setHorizontalAlignment(TextHAlignment::CENTER);
        temp->setColor(ccc3(0, 0, 0));
        temp->setTag(TAG_HINT);
        temp->setPosition(Vec2(size.width / 2, size.height / 4 * 3));
        this->guiNode->addChild(temp, 1001);
    }

    void spawnFloor(Vec2 pos, Rect rect) {
        pos.x += rect.size.width * 0.5f;
        pos.y += rect.size.height * 0.5f;

        Texture2D::TexParams texparam;
        texparam.magFilter = GL_LINEAR;
        texparam.minFilter = GL_LINEAR;
        texparam.wrapS = GL_REPEAT;
        texparam.wrapT = GL_REPEAT;

        auto sprite = Sprite::create("tile_64.png");
        sprite->getTexture()->setTexParameters(texparam);
        sprite->setTextureRect(rect);
        sprite->setTag(TAG_FLOOR);
        sprite->setPosition(pos);

        auto body = PhysicsBody::createBox(rect.size);
        body->setDynamic(false);
        body->setContactTestBitmask(1);
        sprite->setPhysicsBody(body);

        this->addChild(sprite);
    }

    Hero* getHero() {
        return this->hero;
    }

    Node* getGuiNode() {
        return this->guiNode;
    }

protected:
    Hero *hero;
    Node *guiNode;
    LabelTTF* labelScore;
    Size bgSize;
    bool playing = false;
    bool lose = false;
};

There’re some tricky part in the GameScene. Notice that we’ve created a node called guiNode. The reason behind that is because in the current version of Cocos2d-x, when we move the parent node of a physics body, the physics body won’t move along with its parent, but it will be relative to the scene position instead. That’s why to achieve a chase camera effect, we divide the scene into two parts, the gui and the rest of it. When we want it to chase the main character then, we move the scene to the opposing side of the hero, and guiNode along with the hero.

AppDelegate

We basically do not change the AppDelegate much, we only tells it to load the GameScene instead of the default HelloWorldScene.

bool AppDelegate::applicationDidFinishLaunching() {
    /* ... ommited for brevity */

    auto scene = GameScene::createScene();
    director->runWithScene(scene);

    return true;
}

Basically we're done here. You just need to run the code and it should run good. You can also download the source code and resources used in this tutorial below.
ċ
Resources.zip
(8830k)
Hadi Setiawan,
Apr 20, 2015, 2:52 AM
ċ
source code.zip
(7k)
Hadi Setiawan,
Apr 20, 2015, 2:53 AM