六邊形遊戲的鼻祖應該是這個 hex-frvr,原做者開發用的是 pixi 遊戲引擎,本着快速開發的理念,本遊戲採用 cocos creator,UI 延用 hex-frvr。學習過程當中,有借鑑各路實現。此源碼僅供學習使用,謝謝。node
六邊形遊戲本質是俄羅斯方塊,理解這個對接下來的開發會有很大的幫助。git
本遊戲實現功能以下:github
在講遊戲開發思路前,建議先了解 cocos creatorweb
必須瞭解的 API 有:算法
其中,Node、Event、Vec2,是此遊戲開發的重點。api
下面從功能逐一介紹開發思路。數組
棋盤用的是六角網格佈局,電子遊戲中六角網格的運用沒有方形網格那樣常見,先來簡單瞭解下六角網格。dom
本文中討論的六角網格使用的都是正六邊形。六角網格最典型的朝向有兩種:水平方向( 頂點朝上 )與豎直方形( 邊線朝上 )。本遊戲用的是,頂點朝上的朝向。異步
細心的同窗會發現,圖中有相似座標系的東西,稱之爲軸座標。
軸座標系,有時也叫作「梯形座標系」,是從立方座標系的三個座標中取兩個創建的座標系。因爲咱們有約束條件 x + y + z = 0
,所以第三個座標實際上是多餘的。軸座標適合用於地圖數據儲存,也適合用於做爲面向玩家的顯示座標。相似立方座標,你也可使用笛卡爾座標系中的加,減,乘,除等基本運算。
有許多種立方座標系,所以,也天然有許多種由其衍生的軸座標系。本遊戲,選用的是 q = x
以及 r = z
的狀況。這裏 q 表明列而 r 表示行。
偏移座標是人們最早會想到的座標系,由於它可以直接使用方形網格的笛卡爾座標。但不幸的是,偏移座標系中的一個軸總會顯得格格不入,而且最終會把問題變得複雜化。立方座標和軸座標則顯得相得益彰,算法也更簡單明瞭,只是地圖存儲方面會略微變得複雜一點。因此,使用立方/軸座標系是較爲簡單的。
大體瞭解了什麼是六角網格,接下來了解如何把六角網格轉換爲像素。
若是使用的軸座標,那麼能夠先觀察下圖中示意的單位矢量。在下圖中,箭頭 A→Q
表示的是 q 軸的單位矢量而 A→R
是 r 軸的單位矢量。像素座標即 q_basis _ q + r_basis _ r
。例如,B 點位於 (1, 1)
,等於 q 與 r 的單位矢量之和。
在網格爲 水平 朝向時,六邊形的 高度 爲 高度 = size * 2
. 相鄰六邊形的 豎直 距離則爲 豎直 = 高度 * 3/4
。
六邊形的 寬度 爲 寬度 = sqrt(3)/2 * 高度
。相鄰六邊形的 水平 距離爲 水平 = 寬度
。
對於本遊戲中,取棋盤中心點爲,(0,0)。從已知的六角網格座標(正六邊形)以及六邊形的高度,就能夠獲得每一個正六邊形的座標。能夠獲得以下像素轉換代碼:
hex2pixel(hex, h) {
let size = h / 2;
let x = size * Math.sqrt(3) * (hex.q + hex.r / 2);
let y = ((size * 3) / 2) * hex.r;
return cc.p(x, y);
}
複製代碼
座標系轉像素問題解決了,接下來,須要得到本遊戲中六角網格佈局相應的座標系。
這個問題,本質是軸座標系統的地圖存儲。 8)
對半徑爲 N 的六邊形佈局,當N = max(abs(x), abs(y), abs(z)
,有 first_column[r] == -N - min(0, r)
。最後你訪問的會是 array[r][q + N + min(0, r)]
。然而,因爲咱們可能會把一些 r < 0
的位置做爲起點,所以咱們也必須偏移行,有 array[r + N][q + N + min(0, r)]
。
如本遊戲中,棋盤爲邊界六邊形個數爲 5 的六角網格佈局,生成的座標系存儲代碼以下:
setHexagonGrid() {
this.hexSide = 5;
this.hexSide--;
for (let q = -this.hexSide; q <= this.hexSide; q++) {
let r1 = Math.max(-this.hexSide, -q - this.hexSide);
let r2 = Math.min(this.hexSide, -q + this.hexSide);
for (let r = r1; r <= r2; r++) {
let col = q + this.hexSide;
let row = r - r1;
if (!this.hexes[col]) {
this.hexes[col] = [];
}
this.hexes[col][row] = this.hex2pixel({ q, r }, this.tileH);
}
}
}
複製代碼
邊界個數爲 6 的六角網格佈局,六邊形總數爲 61。接着,只須要遍歷添加背景便可完成棋盤的繪製。
setSpriteFrame(hexes) {
for (let index = 0; index < hexes.length; index++) {
let node = new cc.Node('frame');
let sprite = node.addComponent(cc.Sprite);
sprite.spriteFrame = this.tilePic;
node.x = hexes[index].x;
node.y = hexes[index].y;
node.parent = this.node;
hexes[index].spriteFrame = node;
this.setShadowNode(node);
this.setFillNode(node);
this.boardFrameList.push(node);
}
}
複製代碼
至此,棋盤繪製結束。
方塊的形狀能夠變幻無窮,先來看下本遊戲事先約定的 23 種形狀。
在前面六角網格的知識基礎上,實現這 23 種形狀並不難。只須要約定好每一個形狀對應的軸座標。
代碼配置以下:
const Tiles = [
{
type: 1,
list: [[[0, 0]]]
},
{
type: 2,
list: [
[[1, -1], [0, 0], [1, 0], [0, 1]],
[[0, 0], [1, 0], [-1, 1], [0, 1]],
[[0, 0], [1, 0], [0, 1], [1, 1]]
]
},
{
type: 3,
list: [
[[0, -1], [0, 0], [0, 1], [0, 2]],
[[0, 0], [1, -1], [-1, 1], [-2, 2]],
[[-1, 0], [0, 0], [1, 0], [2, 0]]
]
},
{
type: 4,
list: [
[[0, 0], [0, 1], [0, -1], [-1, 0]],
[[0, 0], [0, -1], [1, -1], [-1, 1]],
[[0, 0], [0, 1], [0, -1], [1, 0]],
[[0, 0], [1, 0], [-1, 0], [1, -1]],
[[0, 0], [1, 0], [-1, 0], [-1, 1]]
]
},
{
type: 5,
list: [
[[0, 0], [0, 1], [0, -1], [1, -1]],
[[0, 0], [1, -1], [-1, 1], [-1, 0]],
[[0, 0], [1, -1], [-1, 1], [1, 0]],
[[0, 0], [1, 0], [-1, 0], [0, -1]],
[[0, 0], [1, 0], [-1, 0], [0, 1]]
]
},
{
type: 6,
list: [
[[0, -1], [-1, 0], [-1, 1], [0, 1]],
[[-1, 0], [0, -1], [1, -1], [1, 0]],
[[0, -1], [1, -1], [1, 0], [0, 1]],
[[-1, 1], [0, 1], [1, 0], [1, -1]],
[[-1, 0], [-1, 1], [0, -1], [1, -1]],
[[-1, 0], [-1, 1], [0, 1], [1, 0]]
]
}
];
複製代碼
因爲沒有涉及方塊出現的機率,這裏就簡單粗暴地用 random 來實現方塊隨機生成。
const getRandomInt = function(min, max) {
let ratio = cc.random0To1();
return min + Math.floor((max - min) * ratio);
};
複製代碼
網格和方塊都搞定了,蠻喜歡這種簡單的 UI 風格,很是適合遊戲開發的入門學習。接下來處理遊戲交互邏輯。
方塊與棋盤之間的交互關係是 Drag 與 Drop ,在 cocos creator
中暫時沒發現有 Drag 相關的組件,目前是經過 touch 事件來模擬。在方塊 touchmove
的過程,須要處理兩件事,第一,檢測拖拽過程當中方塊是否與棋盤有交叉,就是遊戲裏所謂的 碰撞檢測
,cc 有提供相應的碰撞組件,但不夠靈活,由於咱們要獲得的是方塊與棋盤重合關係(ps:並不須要徹底重合),因此仍是用腳原本模擬實現,cc 爲此提供了不少 API,主要都與 vec2 有關。第二,檢測方塊是否能夠落入棋盤。
方塊與棋盤其實都是由正六邊形組合而成,這裏有種比較簡單地方式來判斷二者是否有重合部分,即判斷兩個六邊形圓心的距離,當小於設定值,則認爲有重合。
這邊簡單起見,特地將棋盤與方塊的父節點的座標系原點設爲同一個(中心點)。cocos 座標系可參考這篇
因爲方塊是相對於它的父級中心點定位,而它的父級是相對於 Canvas 定位,所以能夠經過 cc.pAdd(this.node.position, tile.position)
來獲取方塊相對於棋盤原點的座標值。接着遍歷棋盤內六邊形座標值,來檢查拖拽進入的六邊形與棋盤哪些存在重合關係。相關代碼以下:
checkCollision(event) {
const tiles = this.node.children; // this.node 爲 方塊的父級,拖拽改變的是這個節點的座標
this.boardTiles = []; // 保存棋盤與方塊重合部分。
this.fillTiles = []; // 保存方塊當前重合的部分。
for (let i = 0; i < tiles.length; i++) {
const tile = tiles[i];
const pos = cc.pAdd(this.node.position, tile.position); // pAdd 是cc早期提供的 api,可用 vec2 中向量相加替換
const boardTile = this.checkDistance(pos);
if (boardTile) {
this.fillTiles.push(tile);
this.boardTiles.push(boardTile);
}
}
},
checkDistance(pos) {
const distance = 50;
const boardFrameList = this.board.boardFrameList;
for (let i = 0; i < boardFrameList.length; i++) {
const frameNode = boardFrameList[i];
const nodeDistance = cc.pDistance(frameNode.position, pos);
if (nodeDistance <= distance) {
return frameNode;
}
}
},
複製代碼
在拖拽過程,實時保存棋盤有重合關係的六邊形,用於斷定方塊是否能夠落入棋盤
只要方塊的個數與棋盤所在區域可填充部分(棋盤裏面沒有方塊)數目一致,則認爲能夠落子。
checkCanDrop() {
const boardTiles = this.boardTiles; // 當前棋盤與方塊重合部分。
const fillTiles = this.node.children; // 當前拖拽的方塊總數
const boardTilesLength = boardTiles.length;
const fillTilesLength = fillTiles.length;
// 若是當前棋盤與方塊重合部分爲零以及與方塊數目不一致,則斷定爲不能落子。
if (boardTilesLength === 0 || boardTilesLength != fillTilesLength) {
return false;
}
// 若是方塊內以及存在方塊,則斷定爲不能落子。
for (let i = 0; i < boardTilesLength; i++) {
if (this.boardTiles[i].isFulled) {
return false;
}
}
return true;
},
複製代碼
獲得落入與否的斷定值後,須要給用戶能夠落子的提示。這邊的一個作法是,在生成棋盤以前就給每一個棋盤格子節點新建一個 name 爲 shadowNode
的子節點。接着只須要修改符合條件的節點的spriteFrame
爲當前拖拽方塊的spriteFrame
,同時下降透明度便可。代碼以下:
dropPrompt(canDrop) {
const boardTiles = this.boardTiles;
const boardTilesLength = boardTiles.length;
const fillTiles = this.fillTiles;
this.resetBoardFrames();
if (canDrop) {
for (let i = 0; i < boardTilesLength; i++) {
const shadowNode = boardTiles[i].getChildByName('shadowNode');
shadowNode.opacity = 100;
const spriteFrame = fillTiles[i].getComponent(cc.Sprite).spriteFrame;
shadowNode.getComponent(cc.Sprite).spriteFrame = spriteFrame;
}
}
}
複製代碼
至此,方塊的 touchmove
事件添加完畢。接下來,須要作的是,拖拽結束後的相關邏輯處理。
兩種狀況,方塊能夠落入,與方塊不能落入。前面已經獲取了是否能夠落入的斷定。那接下來就是添加相應的處理。
能夠落入的狀況須要作的是在棋盤添加對應方塊,方塊添加結束後從新隨機生成新的方塊。不能夠落入則讓拖拽的方塊返回原位置。
在添加方塊上用了跟以前說到的落入提示相似的方法,給棋盤內每一個格子節點下新增一個名爲 fillNode
的節點,方塊落入都跟這個節點有關。
tileDrop() {
this.resetBoardFrames();
if (this.checkCanDrop()) {
const boardTiles = this.boardTiles;
const fillTiles = this.fillTiles;
const fillTilesLength = fillTiles.length;
for (let i = 0; i < fillTilesLength; i++) {
const boardTile = boardTiles[i];
const fillTile = fillTiles[i];
const fillNode = boardTile.getChildByName('fillNode');
const spriteFrame = fillTile.getComponent(cc.Sprite).spriteFrame;
boardTile.isFulled = true;
fillNode.getComponent(cc.Sprite).spriteFrame = spriteFrame;
this.resetTile();
}
this.board.curTileLength = fillTiles.length;
this.board.node.emit('dropSuccess');
} else {
this.backSourcePos();
}
this.board.checkLose();
}
複製代碼
棋盤有了,也能夠判斷方塊是否能夠落入棋盤。接下來要作的就是消除邏輯的處理,以前說,六邊形消除遊戲就是俄羅斯方塊的衍生版,其實就是多了幾個消除方向,來看張圖:
若是把這個棋盤當作數組,即從左斜方向依次添加 [0,1,2.....]
,最終能夠獲得以下消除規則:
const DelRules = [
//左斜角
[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9, 10],
[11, 12, 13, 14, 15, 16, 17],
[18, 19, 20, 21, 22, 23, 24, 25],
[26, 27, 28, 29, 30, 31, 32, 33, 34],
[35, 36, 37, 38, 39, 40, 41, 42],
[43, 44, 45, 46, 47, 48, 49],
[50, 51, 52, 53, 54, 55],
[56, 57, 58, 59, 60],
//右斜角
[26, 35, 43, 50, 56],
[18, 27, 36, 44, 51, 57],
[11, 19, 28, 37, 45, 52, 58],
[5, 12, 20, 29, 38, 46, 53, 59],
[0, 6, 13, 21, 30, 39, 47, 54, 60],
[1, 7, 14, 22, 31, 40, 48, 55],
[2, 8, 15, 23, 32, 41, 49],
[3, 9, 16, 24, 33, 42],
[4, 10, 17, 25, 34],
//水平
[0, 5, 11, 18, 26],
[1, 6, 12, 19, 27, 35],
[2, 7, 13, 20, 28, 36, 43],
[3, 8, 14, 21, 29, 37, 44, 50],
[4, 9, 15, 22, 30, 38, 45, 51, 56],
[10, 16, 23, 31, 39, 46, 52, 57],
[17, 24, 32, 40, 47, 53, 58],
[25, 33, 41, 48, 54, 59],
[34, 42, 49, 55, 60]
];
複製代碼
規則有了,接着添加消除邏輯,直接看代碼:
deleteTile() {
let fulledTilesIndex = []; // 存儲棋盤內有方塊的的索引
let readyDelTiles = []; // 存儲待消除方塊
const boardFrameList = this.boardFrameList;
this.isDeleting = true; // 方塊正在消除的標識,用於後期添加動畫時,充當異步狀態鎖
this.addScore(this.curTileLength, true);
// 首先獲取棋盤內存在方塊的格子信息
for (let i = 0; i < boardFrameList.length; i++) {
const boardFrame = boardFrameList[i];
if (boardFrame.isFulled) {
fulledTilesIndex.push(i);
}
}
for (let i = 0; i < DelRules.length; i++) {
const delRule = DelRules[i]; // 消除規則獲取
// 逐一獲取規則數組與存在方塊格子數組的交集
let intersectArr = _.arrIntersect(fulledTilesIndex, delRule);
if (intersectArr.length > 0) {
// 判斷兩數組是否相同,相同則將方塊添加到待消除數組裏
const isReadyDel = _.checkArrIsEqual(delRule, intersectArr);
if (isReadyDel) {
readyDelTiles.push(delRule);
}
}
}
// 開始消除
let count = 0;
for (let i = 0; i < readyDelTiles.length; i++) {
const readyDelTile = readyDelTiles[i];
for (let j = 0; j < readyDelTile.length; j++) {
const delTileIndex = readyDelTile[j];
const boardFrame = this.boardFrameList[delTileIndex];
const delNode = boardFrame.getChildByName('fillNode');
boardFrame.isFulled = false;
// 這裏能夠添加相應消除動畫
const finished = cc.callFunc(() => {
delNode.getComponent(cc.Sprite).spriteFrame = null;
delNode.opacity = 255;
count++;
}, this);
delNode.runAction(cc.sequence(cc.fadeOut(0.3), finished));
}
}
if (count !== 0) {
this.addScore(count);
this.checkLose();
}
this.isDeleting = false;
}
複製代碼
三個方塊都沒法放入棋盤,則認爲遊戲結束。
首先獲得未填充的棋盤格子信息,再將三個方塊逐一放入未填充區域判斷是否能夠放入。代碼以下:
checkLose() {
let canDropCount = 0;
const tiles = this.node.children;
const tilesLength = tiles.length;
const boardFrameList = this.board.boardFrameList;
const boardFrameListLength = boardFrameList.length;
// TODO: 存在無效檢測的狀況,可優化
for (let i = 0; i < boardFrameListLength; i++) {
const boardNode = boardFrameList[i];
let srcPos = cc.p(boardNode.x, boardNode.y);
let count = 0;
if (!boardNode.isFulled) {
// 過濾出未填充的棋盤格子
for (let j = 0; j < tilesLength; j++) {
let len = 27; // 設定重合斷定最小間距
// 將方塊移到未填充的棋盤格子原點,並獲取當前各方塊座標值
let tilePos = cc.pAdd(srcPos, cc.p(tiles[j].x, tiles[j].y));
// 遍歷棋盤格子,判斷方塊中各六邊形是否能夠放入
for (let k = 0; k < boardFrameListLength; k++) {
const boardNode = boardFrameList[k];
let dis = cc.pDistance(cc.p(boardNode.x, boardNode.y), tilePos);
if (dis <= len && !boardNode.isFulled) {
count++;
}
}
}
if (count === tilesLength) {
canDropCount++;
}
}
}
if (canDropCount === 0) {
return true;
} else {
return false;
}
}
複製代碼
計分規則變幻無窮,看你需求。通常方塊放入與消除都可加分。
scoreRule(count, isDropAdd) {
let x = count + 1;
let addScoreCount = isDropAdd ? x : 2 * x * x;
return addScoreCount;
}
複製代碼
項目屬於入門級別,初次接觸 cocos creator 遊戲開發,多數參考了網上一些六邊形開源遊戲。在此感謝開源,項目有融入本身的一些方法,好比處理六角網格那塊,可是消除規則,還須要接觸更多知識後才能完善。先寫這麼一篇入門級的,後續再深刻,但願對一些像我同樣剛接觸遊戲開發的人能有一些幫助。後續可能會結合適當的例子,講一些,cocos creator 動畫,粒子系統,物理系統,webgl等。