Wednesday, October 1, 2014

Cocos2d-x: Làm game đầu tiên - Air Hockey (Phần cuối)

1.1 Định nghĩa lớp InGameScene

- Màn hình game trong Cocos2d-x là các scene, do vậy chúng ta phải định nghĩa một scene cho game của mình.

- InGameScene, đây là lớp mang theo scene để chứa các đối tượng của game. Thông thường, chúng ta không cần phải định nghĩa một lớp scene riêng biệt mà ta sẽ chú trọng vào định nghĩa một lớp Layer (là vùng chứa thực sự) và trong lớp này có một hàm tĩnh khởi tạo ra scene chứa Layer này.

- Trong file InGameScene.h
#include <stdio.h>

#include "cocos2d.h"

using namespace cocos2d;

class InGameScene : Layer
{
public:
    static Scene* createScene();
   
public:
    virtual bool init();
   
    CREATE_FUNC(InGameScene);

};



- InGameScene thực chất là một Layer, là Layer mà chúng ta nói đến trong phần phân tích ở trên. Trong lớp này, hàm tĩnh createScene() sẽ có nhiệm vụ khởi tạo scene cho Layer này.

- Ở trong file InGameScene.cpp
#include "InGameScene.h"
Scene* InGameScene::createScene()
{
    auto scene = Scene::create();
    auto layer = InGameScene::create();
   
    scene->addChild(layer);
    return scene;
}

- Khi đó ở AppDelegate, chúng ta sẽ chọn InGameScene để hiển thị màn hình game này như sau

// create a scene. it's an autorelease object

auto scene = InGameScene::createScene();

// run

director->runWithScene(scene);

1.2 Hiển thị các đối tượng sprite trong game

- Việc hiển thị các đối tượng game trong hàm init() của lớp InGameScene

Trong file InGameScene.h
#include <stdio.h>

#include "cocos2d.h"

using namespace cocos2d;

class GameSprite;

class InGameScene : Layer
{
public:
    static Scene* createScene();
   
public:
    virtual bool init();
   
    CREATE_FUNC(InGameScene);

    virtual void update(float fDelta);
private:
    virtual void onEnter();
   
    virtual void onTouchesBegan(const std::vector<Touch*>& touches, Event *unused_event);
    virtual void onTouchesMoved(const std::vector<Touch*>& touches, Event *unused_event);
    virtual void onTouchesEnded(const std::vector<Touch*>& touches, Event *unused_event);
    virtual void onTouchesCancelled(const std::vector<Touch*>&touches, Event *unused_event);
   
    void playerScore(int team);
private:
    GameSprite  *_whitePlayer;
    GameSprite  *_bluePlayer;
    GameSprite  *_ball;
   
    Label       *_lblWhiteScore;
    Label       *_lblBlueScore;
   
    Vector<Sprite *> _players; //Should use Sprite instead of GameSprite, if not it will warning in Vector class
    int         _whiteScore;
    int         _blueScore;
    Size        _screenSize;
};

Trong file InGameScene.cpp

bool InGameScene::init()
{
    if (!Layer::init())
    {
        return false;
    }
   
    _whiteScore = 0;
    _blueScore  = 0;
   
    _screenSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
   
    Sprite *sprtBg = Sprite::create("court.png");
    sprtBg->setPosition(Vec2(_screenSize.width / 2 + origin.x, _screenSize.height / 2 + origin.y));
   
    this->addChild(sprtBg, ZODER_BG);
   
    _whitePlayer = GameSprite::createGameSprite(GSWhiteMallet);
    _whitePlayer->setPosition(Vec2(_screenSize.width / 2, _whitePlayer->radius() * 2));
    _whitePlayer->setNextPosition(_whitePlayer->getPosition());
    this->addChild(_whitePlayer, ZODER_PLAYER);
   
    _bluePlayer = GameSprite::createGameSprite(GSBlueMallet);
    _bluePlayer->setPosition(Vec2(_screenSize.width / 2, _screenSize.height - _bluePlayer->radius() * 2));
    _bluePlayer->setNextPosition(_bluePlayer->getPosition());
    this->addChild(_bluePlayer, ZODER_PLAYER);
   
    _players = Vector<Sprite *>(2);
    _players.pushBack(_whitePlayer);
    _players.pushBack(_bluePlayer);
   
    _ball = GameSprite::createGameSprite(GSPuck);
    _ball->setPosition(Vec2(_screenSize.width / 2, _screenSize.height / 2 - 2 * _ball->radius()));
    _ball->setNextPosition(_ball->getPosition());
    this->addChild(_ball, ZODER_BALL);
   
    _lblWhiteScore = Label::createWithSystemFont("W: 0", "Arial", 50);
    _lblWhiteScore->setTextColor(Color4B(255, 255, 255, 255));
    _lblWhiteScore->setPosition(_screenSize.width - 60, _screenSize.height / 2 - 80);
    _lblWhiteScore->setRotation(90);
    this->addChild(_lblWhiteScore, ZODER_LABEL);
   
    _lblBlueScore = Label::createWithSystemFont("B: 0", "Arial", 50);
    _lblBlueScore->setTextColor(Color4B(0, 0, 255, 255));
    _lblBlueScore->setPosition(_screenSize.width - 60, _screenSize.height / 2 + 80);
    _lblBlueScore->setRotation(90);
    this->addChild(_lblBlueScore, ZODER_LABEL);
   
    this->schedule(schedule_selector(InGameScene::update));
   
    AudioUtils::play(ATBackgroundMusic);
   
    return true;
}

- Hai mallet chúng ta đặt tên là _whitePlayer và _bluePlayer, sau đó chúng ta add chúng vào 1 vector _players để tiện cho quá trình xử lý touch tiếp theo

1.3 Thiết lập Multi-touch, di chuyển Mallet

- Để thiết lập multi-touch, lưu ý trên iOS chúng ta cần phải gọi hàm sau trong lớp AppController.mm
[eaglView setMultipleTouchEnabled:YES];

- Trong file InGameScene.h
 virtual void onEnter();
   
 virtual void onTouchesBegan(const std::vector<Touch*>& touches, Event *unused_event);
 virtual void onTouchesMoved(const std::vector<Touch*>& touches, Event *unused_event);
 virtual void onTouchesEnded(const std::vector<Touch*>& touches, Event *unused_event);
 virtual void onTouchesCancelled(const std::vector<Touch*>&touches, Event *unused_event);

- Trong file InGameScene.cpp, hàm onEnter(), chúng ta sẽ add Listener để bắt các sự kiện touch của người chơi

void InGameScene::onEnter()
{
    Layer::onEnter();
   
    auto listener = EventListenerTouchAllAtOnce::create();
    listener->onTouchesBegan = CC_CALLBACK_2(InGameScene::onTouchesBegan, this);
    listener->onTouchesMoved = CC_CALLBACK_2(InGameScene::onTouchesMoved, this);
    listener->onTouchesEnded = CC_CALLBACK_2(InGameScene::onTouchesEnded, this);
    listener->onTouchesCancelled = CC_CALLBACK_2(InGameScene::onTouchesCancelled, this);
   
    auto dispatcher = this->getEventDispatcher();
    dispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}

- Và chúng ta tiếp tục định nghĩa các hàm bắt các sự kiện touch trên Layer

void InGameScene::onTouchesBegan(const std::vector<Touch*>& touches, Event *unused_event)
{
//    cocos2d::log("You touched");

    for(auto touch : touches) {
        if (touch) {
            Vec2 location = touch->getLocation();
            for (int j = 0; j < _players.size(); j++) {
                GameSprite *player = (GameSprite *)_players.at(j);
                if (player->getBoundingBox().containsPoint(location)) {
                    touch->retain();
                    player->setTouch(touch);
                    break;
                }
            }
        }
    }
}

- Ở hàm TouchesBegan, chúng ta sẽ kiểm tra các touch nào trong vùng biên của 2 Mallet, các touch thuộc vào vùng biên của Mallet sẽ được gán vào thuộc tính touch của Mallet đó thông qua hàm player->setTouch(). Mục đích là ở các sự kiện moveTouch, endTouch, chúng ta sẽ biến touch nào thuộc Mallet nào để tiếp tục xử lý trên Mallet đó.

void InGameScene::onTouchesMoved(const std::vector<Touch*>& touches, Event *unused_event)
{
//    cocos2d::log("You moved");
    for(auto touch : touches)
    {
        if (touch != NULL)
        {
            Vec2 location = touch->getLocation();
            for (int j = 0; j < _players.size(); j++)
            {
                GameSprite *player = (GameSprite *)_players.at(j);
                Touch *t = player->getTouch();
                if (t != NULL && t == touch)
                {
                    Vec2 next = location;
                    float radius = player->radius();
                    if (next.x < radius) {
                        next.x = radius;
                    }
                   
                    if (next.x + radius > _screenSize.width) {
                        next.x = _screenSize.width - radius;
                    }
                   
                    if (next.y < radius) {
                        next.y = radius;
                    }
                   
                    if (next.y + radius > _screenSize.height) {
                        next.y = _screenSize.height - radius;
                    }
                   
                    if (player->getPositionY() < _screenSize.height / 2.f) {
                        if (next.y > _screenSize.height / 2.f - radius) {
                            next.y = _screenSize.height / 2.f - radius;
                        }
                    } else {
                        if (next.y < _screenSize.height / 2.f + radius) {
                            next.y = _screenSize.height / 2.f + radius;
                        }
                    }
                   
                    player->setNextPosition(next);
                    player->setVelocity(Vec2(location.x - player->getPositionX(), location.y - player->getPositionY()));
                   
                    break;
                }
            }
        }
    }
}

- Trong hàm touchesMove, tùy vào mỗi touch xử lý trên Mallet nào thì chúng ta sẽ làm việc trên Mallet đó. Trong hàm này chúng ta sẽ xác định điểm di chuyển đến tiếp theo của Mallet là tọa độ của touch. Tuy nhiên, các trường hợp mà điểm đến mới vượt giới hạn của vùng sân thì sẽ bị giới hạn lại. Cuối cùng, điểm kế đến sẽ được thiết lập cho Mallet qua hàm setNexPosition, vector vận tốc sẽ được tính tại thời điểm đó cho Mallet.

void InGameScene::onTouchesEnded(const std::vector<Touch*>& touches, Event *unused_event)
{
//    cocos2d::log("You ended");
    for(auto touch : touches) {
        if (touch != NULL) {
            for (int j = 0; j < _players.size(); j++) {
                GameSprite *player = (GameSprite *)_players.at(j);
                Touch *t = player->getTouch();
                if (t != NULL && t == touch) {
                    player->setTouch(NULL);
                    player->setVelocity(Vec2(0, 0));
                   
                    break;
                }
            }
        }
    }
}

- Hàm touchesEnd sẽ giải phóng giá trị các thuộc tính cho các Mallet liên quan đến touch

1.4 Vòng lặp Main-loop

- Qua phần xử lý touch ở trên, nếu chạy thử game chúng ta sẽ thấy không có hiện tượng các Mallet di chuyển mặc dù các sự kiến touches đều được bắt. Lý do ở vấn đề là chúng ta chưa hề di chuyển vị trí của các Mallet thông qua hàm setPosition, chúng ta chỉ có tương tác đến thuộc tính _nextPosition của Mallet mà thôi. Tại sao vậy?

- Chúng ta nhận xét rằng, trong game AirHockey này, các chuyển động của Mallet, của Puck là liên tục, tùy thuộc vào hành động của người chơi và vận tốc do tương tác giữa các đối tượng với nhau. Đây có thể xem là 1 game Action.

- Đối với thể loại game như thế này, thường chúng ta sẽ sử dụng vòng lặp game, Main Loop, để liên tục cập nhật trạng thái cho các đối tượng trong game: Mallet, Puck, Label… Việc cập nhật thông qua Main Loop sẽ giúp đồng bộ tất cả các đối tượng trong game. Ví dụ
  • Hai Mallet, thay đổi tùy theo tương tác của người chơi. Giả sử như vậy ta có thể cho Mallet thay đổi vị trí tức thời tại thời điểm có tương tác của người chơi.
  • Puck thay đổi do va chạm với Mallet và thay đổi vị trí dựa vào vận tốc, và việc này sẽ cần cập nhật liên tục qua Main Loop. 
  • Nếu chúng ta tách 2 quá trình xử lý này thì thỉnh thoảng sẽ có sự không đồng bộ, ví dụ, tại thời điểm Mallet va chạm Puck, Mallet đổi vị trí nhưng chưa đến thời điểm lặp nên Puck chưa thể thay đổi vị trí dẫn đến tình trạng không đồng nhất.
- Và như ở các bước trên, trong các hàm xử lý touch, chúng ta chỉ mới thiết lập các thuộc tính, trạng thái còn việc cập nhật lại các trạng thái này, chúng ta sẽ làm trong Main Loop
void InGameScene::update(float fDelta)

{…}

- Để hàm này được gọi liên tục, ta sẽ phải kích hoạt nó thông qua câu lệnh bên dưới ở trong hàm init()
this->schedule(schedule_selector(InGameScene::update));

- Trong Main Loop, chúng ta sẽ thực hiện các công việc sau
  • Kiểm tra tại mỗi thời điểm lặp, Puck có va chạm với các Mallet hay không. Nếu có xảy ra va chạm thì sẽ tính lại vận tốc và điểm đến mới của Mallet.
  • Sau khi tính toán vị trí, tiếp tục kiểm tra va chạm của Puck với các đường biên và vùng ghi điểm ở khung thành. 
  • Nếu Puck vào khung thành thì thực hiện nâng điểm tỉ số và reset lại trận đấu

1.5 Xác định va chạm, tính toán lại vận tốc, di chuyển



void InGameScene::update(float fDelta)
{
    Vec2 ballPosition       = _ball->getPosition();
    Vec2 ballNextPosition   = _ball->getNextPosition();
    Vec2 ballVelocity       = _ball->getVelocity();
    ballVelocity            = ballVelocity * 0.98f;
    ballNextPosition.x      += ballVelocity.x;
    ballNextPosition.y      += ballVelocity.y;
   
    //Check collision of ball and mallet
    float distance          = _ball->radius() + _whitePlayer->radius();
   
    GameSprite *player;
    Vec2 playerPosition;
    Vec2 playerNextPosition;
    Vec2 playerVelocity;
    for (int j = 0; j < _players.size(); j++) {
        player = (GameSprite *)_players.at(j);
        playerPosition = player->getPosition();
        playerNextPosition = player->getNextPosition();
        playerVelocity = player->getVelocity();
       
        Vec2 vBallPlayer1 = Vec2(ballNextPosition.x - playerPosition.x, ballNextPosition.y - playerPosition.y);
        Vec2 vBallPlayer2 = Vec2(ballPosition.x - playerNextPosition.x, ballPosition.y - playerNextPosition.y);
       
        if (vBallPlayer1.length() <= distance ||
            vBallPlayer2.length() <= distance) {
            //Collision between Ball and Player
            float angle = vBallPlayer1.getAngle();
           
            Vec2 vecByX     = Vec2(ballVelocity.x, playerVelocity.x);
            Vec2 vecByY     = Vec2(ballVelocity.y, playerVelocity.y);
           
            float angleX    = vecByX.getAngle();
            float angleY    = vecByY.getAngle();
            float vx1       = playerVelocity.length() * cosf(angleY - angle);
            float vy1       = ballVelocity.length() * sinf(angleX - angle);
           
            float vx        = cosf(angle) * vx1 + cosf(angle + MATH_PI / 2) * vy1;
            float vy        = sinf(angle) * vx1 + sinf(angle + MATH_PI / 2) * vy1;
           
            ballVelocity    = Vec2(vx, vy);
            ballNextPosition.x += ballVelocity.x;
            ballNextPosition.y += ballVelocity.y;
           
            if (vx > 5 && vy > 5) {
                AudioUtils::play(ATHit);
            }
        }
    }
    //check collision of ball and sides
    if (ballNextPosition.x < _ball->radius()) {
        ballNextPosition.x = _ball->radius();
        ballVelocity.x *= -0.8f;
        AudioUtils::play(ATHit);
    }
   
    if (ballNextPosition.x > _screenSize.width - _ball->radius()) {
        ballNextPosition.x = _screenSize.width - _ball->radius();
        ballVelocity.x *= -0.8f;
        AudioUtils::play(ATHit);
    }
   
    //ball and top of the court
    if (ballNextPosition.y > _screenSize.height - _ball->radius()) {
        if (_ball->getPosition().x < (_screenSize.width - GOAL_WIDTH) / 2 ||
            _ball->getPosition().x > (_screenSize.width + GOAL_WIDTH) / 2) {
            ballNextPosition.y = _screenSize.height - _ball->radius();
            ballVelocity.y *= -0.8f;
            AudioUtils::play(ATHit);
        }
    }
    //ball and bottom of the court
    if (ballNextPosition.y < _ball->radius() ) {
        if (_ball->getPosition().x < (_screenSize.width - GOAL_WIDTH) / 2 ||
            _ball->getPosition().x > (_screenSize.width + GOAL_WIDTH) / 2) {
            ballNextPosition.y = _ball->radius();
            ballVelocity.y *= -0.8f;
            AudioUtils::play(ATHit);
        }
    }
   
    //finally, after all checks, update ball's vector and next position
    _ball->setVelocity(ballVelocity);
    _ball->setNextPosition(ballNextPosition);
   
   
    //check for goals!
    if (ballNextPosition.y  < -_ball->radius() * 2) {
        this->playerScore(1);
       
    }
   
    if (ballNextPosition.y > _screenSize.height + _ball->radius() * 2) {
        this->playerScore(2);
    }
   
    ///
    _ball->setPosition(_ball->getNextPosition());
    _ball->setNextPosition(_ball->getPosition());
   
    _whitePlayer->setPosition(_whitePlayer->getNextPosition());
    _whitePlayer->setNextPosition(_whitePlayer->getPosition());
   
    _bluePlayer->setPosition(_bluePlayer->getNextPosition());
    _bluePlayer->setNextPosition(_bluePlayer->getPosition());
}

1.6 Cập nhật Score cho các đội

void InGameScene::playerScore(int team)
{
    AudioUtils::play(ATGoal);
   
    _ball->setVelocity(Vec2(0, 0));
    if (team == 1) {
        _blueScore += 1;
        std::string str = ("B :") + std::to_string(_blueScore);
        _lblBlueScore->setString(str);
        _ball->setNextPosition(Vec2(_screenSize.width / 2, _screenSize.height / 2 - 2 * _ball->radius()));
    } else {
        _whiteScore += 1;
        std::string str = ("W :") + std::to_string(_whiteScore);
        _lblWhiteScore->setString(str);
        _ball->setNextPosition(Vec2(_screenSize.width / 2, _screenSize.height / 2 + 2 * _ball->radius()));
    }
   
    _bluePlayer->setPosition(Vec2(_screenSize.width / 2, _screenSize.height - _bluePlayer->radius() * 2));
    _bluePlayer->setNextPosition(_bluePlayer->getPosition());
   
    _whitePlayer->setPosition(Vec2(_screenSize.width / 2, _whitePlayer->radius() * 2));
    _whitePlayer->setNextPosition(_whitePlayer->getPosition());
}

Kết quả 

 

Download mã nguồn và Resource của game AirHockey 

No comments:

Post a Comment