消除類遊戲案例:Sushi Crush(二)
在上一節《使用Cocos2d-x製作三消類遊戲Sushi Crush——第一部分》中,我們完成了解析度的適配和壽司精靈的建立、佈局、下落。按照三消遊戲的遊戲製作流程,接下來我們實現壽司精靈的消除操作。本節中我們將設計整個遊戲的主要邏輯演算法,其中包含以下內容:
- 消除壽司的條件檢測
- 消除壽司
- 消除壽司時的特效
- 填補消除壽司後留下的空位
效果圖:
實現之前先來整理整理邏輯!壽司精靈下落後,同行或同列如果有三個或三個以上相同的壽司精靈,那麼應該首先判斷並將其消除。而按照常理,在壽司下落過程中是不能進行消除操作的,所以我們要先定義一個變數,判斷是否有精靈處於下落狀態(即runAction是否完成)。如全部都落到了指定位置,則開始檢測是否有滿足消除條件的壽司,如有,則將其消除並“播放”爆炸特效,同時新建壽司精靈填補空位。遊戲流程如下:
整理好邏輯後,現在就開始我們的工作吧。
遊戲主邏輯
對於一個遊戲而言,只存在場景、層、精靈等實際的元素往往是不夠的,我們需要對這些元素進行必要的邏輯檢測和處理,例如精靈的移動、碰撞、動作等等。而且這些邏輯檢測一般情況下是需要及時做出反饋的,所以我們需要在某個方法中不停地迴圈執行這些邏輯。
Cocos2dx遊戲中,遊戲的每一幀都會呼叫Scheduler的update來排程定時器;然後遍歷渲染樹,對遊戲畫面進行繪製。所以我們可以重寫update方法實現自己遊戲的邏輯檢測和處理,下面就是update的程式碼部分:
void PlayLayer::update(float dt) { // 檢測遊戲場景中是否有Actions if (m_isAnimationing) { // 初始化m_isAnimationing m_isAnimationing = false; //依次檢測每個壽司,看它們是否正在執行某個Action for (int i = 0; i < m_height * m_width; i++) { SushiSprite *sushi = m_matrix[i]; if (sushi && sushi->getNumberOfRunningActions() > 0) { m_isAnimationing = true;//如果sushi正在執行Action,那m_isAnimationing就為真。 break; } } } //當沒有任何壽司執行Action時,檢測和刪除滿足消除條件的壽司 if (!m_isAnimationing) { checkAndRemoveChain(); } }
m_isAnimationing 變數好比一把程式鎖,當精靈們都乖乖的不動時,程式就會開啟它,讓你可以安心地執行消除檢測工作;當有精靈調皮的時候,程式便會鎖上它,避免你在這過程中對精靈”胡作非為”。
getNumberOfRunningActions()方法可以獲取sushi正在執行中的動作個數,如果值大於0,那證明此刻的sushi正處於“調皮”狀態。
update完成了遊戲中最重要的邏輯部分,即下圖粉色部分的內容,checkAndRemoveChain方法中實現藍色部分的內容:
如果要呼叫update,必須在程式中呼叫scheduleUpdate()定時器。
首先,什麼是定時器呢?從字面上我們可以把它理解為能在特定的時間段裡控制處理某項任務的裝置。然而事實上,在Cocos2dx中它的功能也的確如此。當你想讓某個函式不斷的執行,或每隔幾秒執行一次,或只執行一次時,那麼這些都可以統統交給定時器來完成。
cocos2dx中有三種定時器:schedule,scheduleUpdate,scheduleOnce。瞭解了其功能你便會發現定時器真是很簡單,很方便,下面是它們的異同:
-
scheduleUpdate(); 此函式是Node的成員函式,每個Node只要呼叫scheduleUpdate(),那麼這個Node就會自動重新整理當前類的update(float dt)函式體。scheduleUpdate()預設每一幀都會呼叫update函式。
-
schedule的作用與scheduleUpdate()函式相似,但是scheduleUpdate()預設每一幀都會呼叫update函式,而schedule則可以自定義重新整理的函式體和時間間隔。
-
[1]schedule(selector); 引數:目標函式,即自定義的更新函式。該函式等同於scheduleUpdate,預設每一幀都呼叫目標函式。
-
[2]schedule(selector, interval); 引數:目標函式,更新時間。
-
[3]schedule(selector, interval, repeat, delay); 引數:目標函式,更新時間,更新次數,每次等待時間。
-
-
scheduleOnce(selector, delay); 引數:目標函式,等待時間。只執行一次,可以指定重新整理的函式體。
停用定時器的方法:
-
停止預設的update更新函式。
unscheduleUpdate(); -
停止自定義更新函式。
unschedule(selector); 引數:自定義的更新函式。 -
停止所有更新函式。
unscheduleAllSelectors()
檢測
檢測是否有符合消除條件的精靈物件是三消類遊戲必不可少的過程,也是最重要的邏輯演算法部分之一。
在遊戲中,我們檢測壽司能不能被消除可以這樣做——從壽司矩陣中選取一個壽司,分別查詢它的上下、左右是否有與它相鄰並且圖示相同的壽司存在,如存在,則把它們插入到某個列表中,等待進一步的消除操作。
以橫軸上的壽司為例,其原理可用下圖所示圖形來作描述:
在水平方向上以sushi所在的列數為界點,分別向前向後依次檢查是否有精靈的圖示與sushi的圖示相同,如果有相同的,這把它插入chainList列表中。
其演算法如下:
//橫向chain檢查 void PlayLayer::getColChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList) { chainList.push_back(sushi);// 插入第一個壽司精靈 //向前檢測相同圖示值(ImgIndex)的壽司 int neighborCol = sushi->getCol() - 1;//sushi前一列壽司所在列數值 while (neighborCol >= 0) { SushiSprite *neighborSushi = m_matrix[sushi->getRow() * m_width + neighborCol]; if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) { chainList.push_back(neighborSushi);//插入該“鄰居”壽司 neighborCol--;//繼續向前 } else { break; } } //向後檢測相同圖示值(ImgIndex)的壽司 neighborCol = sushi->getCol() + 1; while (neighborCol < m_width) { SushiSprite *neighborSushi = m_matrix[sushi->getRow() * m_width + neighborCol]; if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) { chainList.push_back(neighborSushi); neighborCol++; } else { break; } } }
在getColChain(sushi, chainList)方法中,sushi是傳入的待檢測壽司精靈,chainList是存放與sushi相鄰並擁有相同圖示值(ImgIndex,它決定精靈的種類)的壽司的壽司列表。
縱軸上檢測原理相似,這裡就不做過多描述。
消除壽司
消除壽司之前,需要先判斷並取得可被消除壽司的壽司列表,然後依次取出壽司列表的值把它置為空。
消除演算法如下:
void PlayLayer::removeSushi(std::list<SushiSprite *> &sushiList) { m_isAnimationing = true; std::list<SushiSprite *>::iterator itList; //依次取出sushiList的值 for (itList = sushiList.begin(); itList != sushiList.end(); itList++) { SushiSprite *sushi = (SushiSprite *)*itList; // 從m_matrix中移除sushi m_matrix[sushi->getRow() * m_width + sushi->getCol()] = NULL; explodeSushi(sushi); } // 填補空位,它根據NULL的m_matrix值計算填補位置 fillVacancies(); }
消除特效
在消除類遊戲中,消除精靈的同時一般都會伴有炫麗的爆炸特效,這樣才能吸引玩家的眼球,所以我們也仿照CandyCrush的消除特效製作了一個簡單的特效。特效由三部分組成:逐漸變小並消失的壽司,逐漸增大並消失的光圈,隨機的粒子效果。
第一二部分特效都可以通過讓壽司精靈執行Action來實現,具體方法如下:
// 1. 壽司Action-逐漸變小 sushi->runAction(Sequence::create( ScaleTo::create(time, 0.0), CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, sushi)), NULL));//執行完ScaleTo動作後將sushi從父節點移除 // 2. 圓圈特效 auto circleSprite = Sprite::create("circle.png"); addChild(circleSprite, 10); circleSprite->setPosition(sushi->getPosition()); circleSprite->setScale(0);// start size circleSprite->runAction(Sequence::create(ScaleTo::create(time, 1.0), CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, circleSprite)), NULL));//執行完ScaleTo動作後將circleSprite從父節點移除
這裡CallFunc方法是一個動作回撥,它派生自ActionInstant類,其實就屬於瞬時動作的一種,作用是回撥某個類中的方法。Cocos2d-x內部使用大量的回撥函式來進行訊息傳遞(或者說事件呼叫)。例如:Menu的事件觸發,定時器的觸發,Action結束時的回撥等等。CallFunc就是屬於Action結束時的回撥。
當一個Node執行完某個Action後,我們可能需要做一些其他的工作(比如從場景移除這個Node),這時就可以使用動作回撥函式來完成這項功能。在SushiCrush特效中,當Sprite執行完ScaleTo動作後,需要將該Sprite從父節點中移除,所以這裡我們需要用到動作回撥。
CC_CALLBACK_0是一個巨集定義,其定義使用來C++11的特性,定義如下:
#define CC_CALLBACK_0(__selector__,__target__, …) std::bind(&__selector__,__target__, ##__VA_ARGS__)
粒子特效
Cocos2d-x引擎提供了強大的粒子系統,它在模仿自然現象、物理現象及空間扭曲上具備得天獨厚的優勢,為我們實現一些真實自然而又帶有隨機性的特效(如爆炸、煙花、水流)提供了方便。所以SushiCrush爆炸特效的第三部分我們可以利用粒子系統來實現,程式碼如下:
// 3. 粒子特效 auto particleStars = ParticleSystemQuad::create("stars.plist"); particleStars->setAutoRemoveOnFinish(true); particleStars->setBlendAdditive(false); particleStars->setPosition(sushi->getPosition()); particleStars->setScale(0.3); addChild(particleStars, 20);
ParticleSystemQuad是用於建立粒子系統的類,我們可以通過呼叫它的create方法來建立一個粒子系統。
Cocos2d-x中粒子系統是一個很強大的功能,但因為它有非常多的屬性需要設定和調節,所以使用起來還是有些複雜。
不過你也不用擔心,俗話說兵來將擋,水來土掩,程式設計師也不是省油的燈。為了能偷懶,我們偉大的程式設計師開發了粒子編輯器,它可以很方便的編輯出漂亮的粒子效果,讓你勉去手動設定粒子屬性的過程。程式中stars.plist檔案就是由粒子編輯器編輯生成的。
常用的粒子編輯器有兩種,一種是ParticleDesigner,另一種是ParticleEditor。儘管ParticleDesigner編輯器要比ParticleEditor美觀的多,但就我個人而言,我還試覺得ParticleEditor更好用一些,它比較適合新手。之前我也寫過一篇關於如何使用ParticleEditor編輯器相關的文章,大家可以參考一下。不過不管怎樣,粒子編輯器的使用一般都大同小異,比較簡單,所以這裡就不做贅述了。
教程中我們所用的粒子編輯器是ParticleDesigner,如下圖所示:
提示:ParticleDesigner不支援Windows系統,所以如果你是Windows系統,最好還是選擇使用ParticleEditor吧。
填補空位
消除壽司後,會留下很多空位,所以接下來我們要把空缺的位置給填補上。
填補空缺的壽司矩陣位置可以分為兩個過程:1、讓空缺處上面的壽司精靈依次“落”到空缺處 2、建立新的壽司精靈並讓它“落”到矩陣最上方空缺的位置。
原理如下所示:
下面是程式碼部分:
void PlayLayer::fillVacancies() { Size size = CCDirector::getInstance()->getWinSize(); int *colEmptyInfo = (int *)malloc(sizeof(int) * m_width); memset((void *)colEmptyInfo, 0, sizeof(int) * m_width); // 1. 讓空缺處上面的壽司精靈向下落 SushiSprite *sushi = NULL; for (int col = 0; col < m_width; col++) { int removedSushiOfCol = 0;//記錄一列中空缺的精靈數 // 從下向上 for (int row = 0; row < m_height; row++) { sushi = m_matrix[row * m_width + col]; if (NULL == sushi) { removedSushiOfCol++; } else { if (removedSushiOfCol > 0) { //計算壽司精靈的新行數 int newRow = row - removedSushiOfCol; // 轉換 m_matrix[newRow * m_width + col] = sushi; m_matrix[row * m_width + col] = NULL; // 移動到新位置 Point startPosition = sushi->getPosition(); Point endPosition = positionOfItem(newRow, col); float speed = (startPosition.y - endPosition.y) / size.height; sushi->stopAllActions();// must stop pre drop action sushi->runAction(CCMoveTo::create(speed, endPosition)); // 設定壽司的新行 sushi->setRow(newRow); } } } // 記錄col列上空缺數 colEmptyInfo[col] = removedSushiOfCol; } // 2. 建立新的壽司精靈並讓它落到上方空缺的位置 for (int col = 0; col < m_width; col++) { for (int row = m_height - colEmptyInfo[col]; row < m_height; row++) { createAndDropSushi(row, col); } } free(colEmptyInfo); }
結合程式碼中的註釋和原理圖,它的實現原理是不是一目瞭然,很簡單的吧。
好了,今天就到此為止,敬請期待下一章的教程吧。
Where to go
在下一章中,我們將實現觸控控制,四消產生特殊壽司,以及特殊壽司爆炸消除一行或列。