近期出現一款魔性的消除類HTML5游戲《神奇的六邊形》,今天我們一起來看看如何通過開源免費的青瓷引擎(www.zuoyouxi.com)來實現這款游戲。
(點擊圖片可進入游戲體驗)
因內容太多,為方便大家閱讀,所以分成四部分來講解。
本文為第一部分,主要包括:
1. 功能分析
2. 創建工程與場景
3. 玩家分數管理
4. 棋盤設計與實現
5. 屏幕布局
若要一次性查看所有文檔,也可點擊這里。
一. 功能分析
首先分析游戲的功能點、算法和數據,然后依此制訂代碼組織結構。如下圖:
主要功能點
- 棋盤的數據結構與繪制
- 3個形狀的生成
- 形狀拖拽填入棋盤
- 行消除判定與死亡判定
- 各種表現,例如消除動畫、加分動畫等
代碼結構
將游戲邏輯(例如棋盤數據結構、死亡判定等)和界面邏輯分開,分別置於logic和ui界面。所有的UI界面交給UIManager腳本統一維護管理。
二. 創建工程與場景
創建工程Tetris和空的主場景Main,設置如下:
本工程中,畫布背景(background)設置為透明
游戲入口與游戲初始化
在Scripts目錄下創建文件:Tetris.js。代碼如下:
1 /** 2 * 游戲入口 3 */ 4 window.Tetris = qc.Tetris = { 5 // 所有的操作指令集合 6 operation: {} 7 }; 8 9 // 游戲邏輯初始化 10 qc.initGame = function(game) { 11 // 將游戲實例記錄下來,便於訪問 12 Tetris.game = game; 13 14 // 幀率顯示為60幀(滿幀) 15 game.time.frameRate = 60; 16 };
設置此腳本為入口腳本:
此腳本首先定義了名字空間,將全局的數據都記錄在qc.Tetris。
游戲入口中,記錄了game的實例並將幀率限定為60幀(默認在手機下為30幀)
三. 玩家分數管理
-
創建腳本:Scripts/logic/Score.js:
1 /** 2 * 維護分數信息 3 */ 4 var Score = qc.Tetris.Score = function() { 5 var self = this; 6 self._current = 0; 7 self._best = 0; 8 9 // 將本地數據讀取出來 10 var game = qc.Tetris.game; 11 var current = game.storage.get('current'), 12 best = game.storage.get('best'); 13 if (current) self._current = current; 14 if (best) self._best = best; 15 }; 16 Score.prototype = {}; 17 Score.prototype.constructor = Score; 18 19 Object.defineProperties(Score.prototype, { 20 current: { 21 get: function() { return this._current; }, 22 set: function(v) { 23 this._current = v; 24 if (this.best < v) this.best = v; 25 } 26 }, 27 28 best: { 29 get: function() { return this._best; }, 30 set: function(v) { 31 this._best = v; 32 var storage = qc.Tetris.game.storage; 33 storage.set('best', v); 34 storage.save(); 35 } 36 } 37 });
Score類維護了兩個數據:current(當前玩家的分數)、best(玩家的歷史最高分)
-
實例化Score類
打開Tetris.js腳本,在initGame方法中,加入代碼:1 qc.initGame = function(game) { 2 // 將游戲實例記錄下來,便於訪問 3 Tetris.game = game; 4 5 // 幀率顯示為60幀(滿幀) 6 game.time.frameRate = 60; 7 8 // 初始化分數信息 9 Tetris.score = new qc.Tetris.Score(); 10 };
四. 棋盤設計與實現
棋盤為一邊長為5的正六變形,為了方便計算,我們如下設定棋盤的坐標系(下文稱為:格子邏輯坐標):
原點在六邊形中心點,半徑為4。
-
修改Tetris.js文件,增加棋盤的配置信息:
1 window.Tetris = qc.Tetris = { 2 // 棋盤的大小(半徑) 3 SIZE: 4, 4 5 // 棋盤中,每個格子的寬度和高度 6 BLOCK_W: 61, 7 BLOCK_H: 67, 8 9 // 所有的操作指令集合 10 operation: {} 11 };
棋盤格子的大小 = 格子圖片的大小,后續導入資源后可以看到其大小為61*67。
-
在Scripts/logic下創建文件Board.js,維護棋盤的數據,代碼如下:
1 var Board = qc.Tetris.Board = function() { 2 var self = this, 3 size = qc.Tetris.SIZE, 4 len = qc.Tetris.BLOCK_H; 5 6 // 構建用來轉換格子坐標的矩陣 7 var m = self.m = new qc.Matrix(); 8 m.a = len; 9 m.c = len / 2; 10 m.d = len * (Math.sqrt(3) / 2); 11 12 // 初始化棋盤數據 13 self.data = {}; 14 for (var i = -size; i <= size; i++) { 15 for (var j = -size; j <= size; j++) { 16 // 這些格子落在六邊形外,忽略掉 17 if (i * j > 0 && Math.abs(i + j) > size) continue; 18 if (i * j < 0 && (Math.abs(i) > size || Math.abs(j) > size)) continue; 19 20 // 計算格子的坐標和對應屏幕上的偏移 21 var pos = Tetris.makePos(i, j); 22 var pt = self.toWorld(new qc.Point(i, j)); 23 self.data[pos] = { 24 value: 0, 25 x: pt.x, 26 y: pt.y 27 }; 28 } 29 } 30 }; 31 Board.prototype = {}; 32 Board.prototype.constructor = Board; 33 34 Object.defineProperties(Board.prototype, { 35 /** 36 * @property {boolean} die - 當前是否已經死亡了 37 * @readonly 38 */ 39 die: { 40 get: function() { 41 // TODO: 等待實現 42 } 43 } 44 }); 45 46 /** 47 * 清空棋盤 48 */ 49 Board.prototype.clear = function() { 50 for (var pos in this.data) { 51 this.data[pos].value = 0; 52 } 53 }; 54 55 /** 56 * 重新開始游戲 57 */ 58 Board.prototype.restart = function() { 59 this.clear(); 60 }; 61 62 // 判定形狀可以放進來不 63 // pos: 目標邏輯坐標 64 // list: 形狀的信息 65 Board.prototype.checkPutIn = function(pos, list) { 66 // TODO: 等待實現 67 }; 68 69 // 把某個形狀放進來 70 Board.prototype.putIn = function(pos, list, value) { 71 // TODO: 等待實現 72 }; 73 74 // 根據格子的邏輯坐標,算出所在的屏幕坐標 75 // distance: 兩個格子中心點之間的距離 76 Board.prototype.toWorld = function(p, distance) { 77 if (!distance) 78 return this.m.apply(p); 79 80 var m = new qc.Matrix(); 81 m.a = distance; 82 m.c = distance * 0.5; 83 m.d = distance * (Math.sqrt(3) * 0.5); 84 return m.apply(p); 85 }; 86 87 // 根據格子的屏幕坐標,反算格子的邏輯坐標 88 Board.prototype.toLocal = function(p) { 89 return this.m.applyInverse(p); 90 };
-
修改Tetris.js,在qc.initGame方法中,實例化本對象:
1 qc.initGame = function(game) { 2 // 將游戲實例記錄下來,便於訪問 3 Tetris.game = game; 4 5 // 幀率顯示為60幀(滿幀) 6 game.time.frameRate = 60; 7 8 // 初始化分數信息 9 Tetris.score = new qc.Tetris.Score(); 10 11 // 構建棋盤對象 12 Tetris.board = new qc.Tetris.Board(); 13 };
同時,在本文件中實現兩個函數:makePos和readPos:
1 // 構建坐標 2 window.Tetris.makePos = function(x, y) { 3 return x + '_' + y; 4 }; 5 6 // 獲取坐標 7 window.Tetris.readPos = function(pos) { 8 var arr = pos.split('_'); 9 return new qc.Point(arr[0]*1, arr[1]*1); 10 };
五. 屏幕布局
在美術設計時,以640*960分辨率(iPhone4)進行設計,其他分辨率的屏幕需要自適應。如下圖:

- 整個界面分為標題欄(Top)、棋盤(Board)、3個形狀(Shape)
- Top:高度在iPhone4上為130。這里有兩個信息:當前分數與歷史最高分數
- Board:棋盤,其大小為600*580
- Shape:3個形狀,大小為600*230,距離底部20
- 自適應方案:
- 以 640*960為基准,等比縮放,確保所有內容都能全部顯示
- 當分辨率比較瘦長時(即Height/Width > 960/640)時,Board和Shape保持和底部位置不變(方便單手操作)。Top高度自動增加
- 當分辨率比較寬時(即Height/Width < 960/640)時,Board和Shape保持居中,兩邊留白
導入資源
-
新建文件夾:Assets/atlas/ui@atlas,將以下文件拖入並打包圖集(圖片請在示例工程中查看)
blue.png、cyan.png、gray.png、green.png、lightyellow.png、red.png、shadow.png、white.png、yellow.png
darkblue.png、darkcyan.png、darkgreen.png、darklightyellow.png、darkred.png、darkyellow.png等,具體請參考示例工程
格子在沒有數據時,顯示gray.png。其他形狀的格子顏色,有6種(blue、cyan、green、lightyellow、yellow、red) -
將以下文件拖入文件夾Assets/raw(raw目錄下的資源都不會被打包,例如圖片直接原樣保留,適用於css樣式表指定資源)
blue.png、cyan.png、gray.png、green.png、lightyellow.png、red.png、yellow.png等,具體請參考示例工程,各圖片的用途在后續中會說明。
界面布局
1. 創建UIRoot,並設置Reference Resolution(參考分辨率)為 640*960,Manual Type為Expand
簡單的理解:設置了以后,就可以認為屏幕的寬度>=640,高度>=960
2. 創建棋盤。棋盤大部分情況下是“靜態”的,只是在有新的形狀放入時才會變化。如果棋盤的每個格子作為UIImage進行貼圖,則每幀都需要重繪幾十個格子圖片,對渲染效率會有所影響。這里我們適用DOM方案,里面每個格子使用div進行繪制。因此創建一個DOM節點,設置其大小為:600*580,同時由於棋盤距離底部的位置固定,因此在布局上:水平居中、垂直距離底部250,自身中心點在底部中心位置。如下圖:
3. 添加一個Node節點,掛載3個形狀。Node大小為 600*230,距離底部20。如下圖:
4. 創建DOM節點顯示歷史最高分(不常變化,因此不用UIText,使用Dom更高效)。本節點大小為200*60,距離屏幕右邊20,頂部20:
5. 創建DOM節點顯示當前分(不常變化,因此不用UIText,使用Dom更高效)。本節點大小為200*80,水平居中,頂部頂部29: