上一節,我們學習了如何創建一個新工程,并創建自己的游戲場景。2048游戲場景已經創建好,但是還缺少玩家交互,游戲得分,結束等邏輯。下面我們一起來完善。
游戲操作
游戲中需要加入touch事件來處理,上下左右移動數字卡片的判斷。 玩家可以選擇上下左右四個方向,若棋盤內的數字出現位移或合并,視為有效移動;玩家選擇的方向上若有相同的數字則合并,每次有效移動可以同時合并,但不可以連續合并;合并所得的所有新生成數字相加即為該步的有效得分;玩家選擇的方向行或列前方有空格則出現位移;每有效移動一步,棋盤的空位(無數字處)隨機出現一個數字(依然可能為2或4)。 直到棋盤內的數字卡片數值達到2048,玩家勝利。或棋盤填滿數字,無法合并移動。游戲結束。
注冊觸摸事件響應
首先我們需要讓Layer能接收Touch事件。所以我們繼承Layer處理事件的回調虛函數,并重寫,下面是實現過程:
1、在GameScene.h文件中聲明成員函數
//觸摸事件監聽回調函數
virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);
virtual void onTouchMoved(cocos2d::Touch* touch, cocos2d::Event* event);
virtual void onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event);
2、在GameScene.cpp文件的init函數中創建綁定觸摸事件
//設置觸摸事件監聽
auto touchListener = EventListenerTouchOneByOne::create();
touchListener->onTouchBegan = CC_CALLBACK_2(GameScene::onTouchBegan, this);
touchListener->onTouchMoved = CC_CALLBACK_2(GameScene::onTouchMoved, this);
touchListener->onTouchEnded = CC_CALLBACK_2(GameScene::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);
3、實現觸摸事件回調函數
bool GameScene::onTouchBegan(Touch* touch, Event* event)
{
Point beginTouch = touch->getLocation();
recognizer->beginPoint(beginTouch.x, beginTouch.y);
return true;
}
void GameScene::onTouchMoved(Touch* touch, Event* event)
{
Point pos = touch->getLocation();
recognizer->movePoint(pos.x, pos.y);
}
void GameScene::onTouchEnded(Touch* touch, Event* event)
{
SimpleGestures rtn = recognizer->endPoint();
switch (rtn) {
case SimpleGesturesLeft:
doLeft();
doCheck();
setScore(score);
break;
case SimpleGesturesRight:
doRight();
doCheck();
setScore(score);
break;
case SimpleGesturesUp:
doUp();
doCheck();
setScore(score);
break;
case SimpleGesturesDown:
doDown();
doCheck();
setScore(score);
break;
case SimpleGesturesNotSupport:
case SimpleGesturesError:
log("not support or error touch,use geometricRecognizer!!");
break;
default:
break;
}
}
手勢判斷
在此我封裝了一個簡單的手勢識別類SimpleRecognizer,來處理玩家的滑動手勢。代碼中對滑動距離大于50以上的才處理為有效手勢。關鍵代碼如下:
// be called in onTouchBegan
void SimpleRecognizer::beginPoint(cocos2d::Point point)
{
this->result = SimpleGesturesError;
points.clear();
points.push_back(point);
}
void SimpleRecognizer::movePoint(cocos2d::Point point)
{
points.push_back(point);
}
SimpleGestures SimpleRecognizer::endPoint(cocos2d::Point point)
{
points.push_back(point);
if (this->points.size() < 3) {
return SimpleGesturesError;
}
SimpleGestures newRtn = SimpleGesturesError;
int len = this->points.size();
//每當觸點移動時,在當前觸點和之前觸點之間計算不同的x坐標和y坐標
double dx = this->points[len - 1].x - this->points[0].x;
double dy = this->points[len - 1].y - this->points[0].y;
if (abs(dx) > abs(dy)) {
//在這種情況下,運動趨勢的觸點在x軸方向
if (dx > 50) {
newRtn = SimpleGesturesRight;
} else if ( dx < -50 ) {
newRtn = SimpleGesturesLeft;
}
} else {
//在這種情況下,運動趨勢的觸點在y軸方向
if (dy > 50) {
newRtn = SimpleGesturesUp;
} else if ( dy < -50 ) {
newRtn = SimpleGesturesDown;
}
}
// first set result
if (result == SimpleGesturesError) {
result = newRtn;
}
// if diretcory change, not support Recongnizer
if (result != newRtn) {
result = SimpleGesturesNotSupport;
}
return result;
}
根據手勢處理數字卡片的移動或合并
根據手勢方向,判斷數字卡片是否可以移動和合并。更新分數,移動完成后判斷是否在空白處生成新的數字卡片。
游戲中我們并沒有真正的數字卡片移動,如果卡片可以沿著一個方向移動,比如左移,是循環遍歷4*4的矩陣,將card[x+1][y]的數字賦給card[x][y],將card[x+1][y]的數字賦為0實現的。如果相鄰兩數字卡片card[x+1][y]和card[x][y]的數字相同則進行合并操作,card[x][y]的數字為合并后所得的數字。將card[x+1][y]的數字賦為0實現的。得分為合并所得數字。
四個方向的移動處理代碼類似,下面來看左移代碼實現如下:
//左滑動
bool GameScene::doLeft()
{
//判斷有沒有發生移動
bool isMove = false;
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
for (int x1 = x+1; x1<4; x1++)
{
if (cardArr[x1][y]->getNumber() > 0)
{
if (cardArr[x][y]->getNumber() <= 0)
{
cardArr[x][y]->setNumber(cardArr[x1][y]->getNumber());
cardArr[x1][y]->setNumber(0);
x--;
isMove = true;
}
else if(cardArr[x][y]->getNumber() == cardArr[x1][y]->getNumber())
{
cardArr[x][y]->setNumber(cardArr[x][y]->getNumber() * 2);
cardArr[x1][y]->setNumber(0);
//改變分數
score += cardArr[x][y]->getNumber();
isMove = true;
}
break;
}
}
}
}
return isMove;
}
在移動、合并和得分處理完成后,我們需要判斷為空白矩陣位置加入新的數字卡片。實際上是,查找4*4的矩陣區域中數字為0的卡片,隨機賦值為2或4.新的數字卡片加入后,我們進行判斷游戲是否結束。
游戲結束的邊界,4*4的數字卡片中沒有數字0且相鄰數字卡片沒有相等的數字。
void GameScene::doCheck()
{
bool isGameOver = true;
//結束邊界 4*4的card數值>0 且 相鄰card沒有相同數值
//4*4的card數值>0 不能在創建Number
//判斷每一個的上下左右和自己是否相同
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
if (cardArr[x][y]->getNumber() == 0 ||
(x<3 && cardArr[x][y]->getNumber() == cardArr[x+1][y]->getNumber()) ||
(x>0 && cardArr[x][y]->getNumber() == cardArr[x-1][y]->getNumber()) ||
(y<3 && cardArr[x][y]->getNumber() == cardArr[x][y+1]->getNumber()) ||
(y>0 && cardArr[x][y]->getNumber() == cardArr[x][y-1]->getNumber()) )
{
isGameOver = false;
}
}
}
if (isWin()) {
successLayer = LayerColor::create(Color4B(0, 0, 0, 180));
Size winSize = Director::getInstance()->getWinSize();
Point centerPos = Point(winSize.width / 2, winSize.height / 2);
auto gameOverTitle = Label::createWithSystemFont("YOU WIN","Consolas",80);
gameOverTitle->setPosition(centerPos);
successLayer->addChild(gameOverTitle);
addChild(successLayer,1);
scheduleOnce(SEL_SCHEDULE(&GameScene::removeSuccessLayer), 2);
return;
}
//isGameOver = true;
if (isGameOver)
{
log("game over");
UserDefault::getInstance()->setBoolForKey("history", false);
HighScore::getInstance()->setScore(score);
GameOverLayer *gameoverLayer = GameOverLayer::create(Color4B(0, 0, 0, 180));
addChild(gameoverLayer,1);
Director::getInstance()->pause();
}
else
{
if (shouldCreateCardNumber()) {
createCardNumber();
saveStatus();
}
}
}
數字卡片數字達到2048,玩家win的判斷
bool GameScene::isWin()
{
bool win = false;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
if( 2048 == cardArr[i][j]->getNumber() )
{
win = true;
break;
}
}
}
return win;
}
PopLayer
游戲中需要提示玩家勝利或游戲結束,這類的提示都可以使用popLayer實現。新建一個新的Layer對象,add到當前場景上。 下面我們來添加游戲的暫停和game over的popLayer。他們都是LayerColor的子類。
1、game over層顯示GameOver標題和得分、最高分和重玩菜單。關鍵代碼如下:
bool GameOverLayer::initWithColor(const Color4B& color)
{
if (!LayerColor::initWithColor(color)) {
return false;
}
Size winSize = Director::getInstance()->getWinSize();
Point centerPos = Point(winSize.width / 2, winSize.height / 2);
auto gameOverTitle = Label::createWithSystemFont("GAME OVER","Consolas",80);
gameOverTitle->setPosition(Point(centerPos.x, centerPos.y + 150));
addChild(gameOverTitle);
char score[64];
sprintf(score, "%d", HighScore::getInstance()->getScore());
auto scoreTitle = Label::createWithSystemFont(score,"Consolas",60);
scoreTitle->setPosition(Point(centerPos.x, centerPos.y + 50));
addChild(scoreTitle);
char temp[64];
sprintf(temp, "BEST:%d", HighScore::getInstance()->getHighScore());
auto highScoreTitle = Label::createWithSystemFont(temp,"Consolas",40);
highScoreTitle->setPosition(Point(centerPos.x, centerPos.y - 50));
addChild(highScoreTitle);
MenuItemFont::setFontName("Consolas");
MenuItemFont::setFontSize(80);
auto menuItemRestart = MenuItemFont::create("RESTART", CC_CALLBACK_1(GameOverLayer::onRestart, this));
auto menu = Menu::create(menuItemRestart, NULL);
addChild(menu);
menu->setPosition(Point(centerPos.x, centerPos.y - 150));
//設置觸摸事件監聽
auto touchListener = EventListenerTouchOneByOne::create();
touchListener->onTouchBegan = CC_CALLBACK_2(GameOverLayer::onTouchBegan, this);
touchListener->onTouchMoved = CC_CALLBACK_2(GameOverLayer::onTouchMoved, this);
touchListener->onTouchEnded = CC_CALLBACK_2(GameOverLayer::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);
touchListener->setSwallowTouches(true);
return true;
}
2、暫停層顯示繼續、重玩、退出菜單。關鍵代碼如下:
bool PopLayer::initWithColor(const Color4B& color)
{
if (!LayerColor::initWithColor(color)) {
return false;
}
Size winSize = Director::getInstance()->getWinSize();
Point centerPos = Point(winSize.width / 2, winSize.height / 2);
MenuItemFont::setFontName("Consolas");
MenuItemFont::setFontSize(80);
auto menuItemContinue = MenuItemFont::create("CONTINUE", CC_CALLBACK_1(PopLayer::onContinue, this));
auto menuItemRestart = MenuItemFont::create("RESTART", CC_CALLBACK_1(PopLayer::onRestart, this));
auto menuItemExit = MenuItemFont::create("EXIT", CC_CALLBACK_1(PopLayer::onExit, this));
auto menu = Menu::create(menuItemContinue, menuItemRestart, menuItemExit, NULL);
menu->alignItemsVertically();
addChild(menu);
menu->setPosition(centerPos);
return true;
}
對比你會發現他們之間的實現方式都差不多。都是Label/Menu組合而成的。
暫停效果圖:
gameover效果圖:
3、layer屏蔽touch事件
此處有個問題,在popLayer層上觸摸,你會發現底層的GameScene會響應。這就需要對popLayer進行touch事件處理屏蔽,不應該傳遞到底層。
在inin方法中注冊touch事件監聽
//設置觸摸事件監聽
auto touchListener = EventListenerTouchOneByOne::create();
touchListener->onTouchBegan = CC_CALLBACK_2(PopLayer::onTouchBegan, this);
touchListener->onTouchMoved = CC_CALLBACK_2(PopLayer::onTouchMoved, this);
touchListener->onTouchEnded = CC_CALLBACK_2(PopLayer::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);
// 設置是否吞沒事件,在 onTouchBegan 方法返回 true 時吞沒
touchListener->setSwallowTouches(true);
空實現touch事件的監聽函數
bool GameOverLayer::onTouchBegan(Touch* touch, Event* event)
{
return true;
}
void GameOverLayer::onTouchMoved(Touch* touch, Event* event)
{
}
void GameOverLayer::onTouchEnded(Touch* touch, Event* event)
{
}
數據存儲與狀態恢復
游戲中需要對玩家的數據進行存儲。Cocos2d-x為我們提供了UserDefaults來管理數據存儲。數據是通過指定的Key 字串,按鍵值對存儲的。游戲中存儲了最高分和玩家的當前游戲數據,以便能從下次游戲恢復。
我們可以使用如下方法往文件中存儲數據,下面是簡單的以4*4矩陣的行列作為key,存儲該數字卡片的數值。
//存儲最高分
UserDefault::getInstance()->setIntegerForKey(KHIGHSCORE, score);
//
char temp[10];
//4*4
for (int i = 0; i<4; i++) {
for(int j = 0; j<4; j++)
{
sprintf(temp,"%d%d",i,j);
UserDefault::getInstance()->setIntegerForKey(temp, cardArr[i][j]->getNumber());
}
}
通過如下方法從文件中取出數據,以4*4矩陣的行列作為key,取出當前數字卡片的數值。
char temp[10];
//4*4
for (int i = 0; i<4; i++) {
for(int j = 0; j<4; j++)
{
sprintf(temp,"%d%d",i,j);
int number = UserDefault::getInstance()->getIntegerForKey(temp);
cardArr[i][j]->setNumber(number);
}
}
Build && Run
此處,我們是通過xcode作為開發工具進行開發和編譯運行調試。但是xcode不支持Android的開發,那么如何進行Android的編譯與運行呢?
在Cocos2d-x v3.0 加入了新的cocos命令。按照上一章節開始處講的建立cocos命令環境。在命令行窗口執行下命令,查看cocos 相關的命令使用:
IvenYangtekiMacBook-Pro-2:~ zeroyang$ cocos --help
/Users/zeroyang/Documents/work/cocos2d-x-3.0/tools/cocos2d-console/bin/cocos.py 0.2 - cocos console: A command line tool for cocos2d
Available commands:
compile Compiles the current project to binary
new Creates a new project
run Compiles & deploy project and then runs it on the target
jscompile minifies and/or compiles js files
deploy Deploy a project to the target
Example:
/Users/zeroyang/Documents/work/cocos2d-x-3.0/tools/cocos2d-console/bin/cocos.py new --help
/Users/zeroyang/Documents/work/cocos2d-x-3.0/tools/cocos2d-console/bin/cocos.py run --help
按提示,我們直接執行
$ cocos run -p android
你會發現build不通過,仔細檢查錯誤信息,
jni/../../Classes/AppDelegate.cpp:31: error: undefined reference to 'GameScene::createScene()'
collect2: error: ld returned 1 exit status
make: *** [obj/local/armeabi/libcocos2dcpp.so] Error 1
make: Leaving directory `/Users/zeroyang/Documents/2048/proj.android'
Error running command, return code: 2
你會發現,新建的class文件夾下的cpp沒被包含進android的編譯文件,修改proj.android/jni/android.mk 加入。修改如下:
LOCAL_SRC_FILES := hellocpp/main.cpp \
../../Classes/AppDelegate.cpp \
../../Classes/Card.cpp \
../../Classes/GameOverLayer.cpp \
../../Classes/GameScene.cpp \
../../Classes/HighScore.cpp \
../../Classes/PopLayer.cpp \
../../Classes/SimpleRecognizer.cpp
修改后繼續執行cocos run -p android。 運行效果如圖:
總結
在2048游戲的制作中,我們學習了如何創建一個新的工程,創建自己的Scene,并add 背景layer。 如何創建label顯示標題,分數。如何處理touch事件,如何添加菜單處理菜單事件。以及poplayer的添加,數據存儲等。
至此,一個簡單的2048游戲已經完成。游戲甚是簡陋,還需小伙伴們完善。
源碼地址:http://git.oschina.net/ZeroYang/Tutorial-2048/repository/archive?ref=master