繼上一次介紹了《神奇的六邊形》的完整游戲開發流程后(可點擊這里查看),這次將為大家介紹另外一款魔性游戲《跳躍的方塊》的完整開發流程。
(點擊圖片可進入游戲體驗)
因內容太多,為方便大家閱讀,所以分多次來講解。
若要一次性查看所有文檔,也可點擊這里。
接上回(《跳躍的方塊》Part 1)
三. 游戲世界
為了能更快的體驗到游戲的主體玩法,調整游戲數值,這里我們先來搭建游戲世界。
建立基礎世界
在《跳躍的方塊》中,下一關的信息尤為關鍵。如果能提前獲知阻擋點或者通道位置,會為當前的操作提供一定的指導。為了保證所有玩家獲取的信息基本一致,屏幕中顯示的關卡數量需要嚴格的控制。
所以這里我們將屏幕的高度通過UIRoot映射為一個固定值:960,添加一個鎖定屏幕旋轉方向的腳本,並創建游戲的根節點game,設置game節點鋪滿屏幕。
操作如下所示:
分步構建世界
- 游戲配置
- 構建世界邏輯
- 控制展示游戲世界
(一)游戲配置
設置可調整參數
這個游戲中,一些參數會嚴重影響用戶體驗,需要進行不停的嘗試,以找到最合適的設置。所以,這里將這些參數提取出來,群策群力,快速迭代出最終版本。
分析游戲內容后,將游戲數據分為兩類:
1. 關卡數據 如何生成關卡、如何生成阻擋。把這些數據配置到一個Excel文件JumpingBrick.xls中,並拷貝到Assets/excel目錄下。內容如下:

2. 物理信息 游戲使用的物理碰撞比較簡單,而且移動的方塊自身有旋轉45度,不太適合直接使用引擎的物理插件。故而這里直接設置方塊上升的速度,下落的加速度等物理信息,由游戲腳本自己處理。
新建一個腳本GameConfig.js,內容如下:
1 /* 2 * 游戲配置 3 */ 4 var GameConfig = qc.defineBehaviour('qc.JumpingBrick.GameConfig', qc.Behaviour, function() { 5 var self = this; 6 7 // 設置到全局中 8 JumpingBrick.gameConfig = self; 9 10 // 等級配置 11 self.levelConfigFile = null; 12 13 // 游戲使用的重力 14 self.gravity = -1600; 15 16 // 點擊后左右移動的速度 17 self.horVelocity = 100; 18 19 // 點擊后上升的速度 20 self.verVelocity = 750; 21 22 // 點擊后上升速度的持續時間 23 self.verVelocityKeepTime = 0.001; 24 25 // 鎖定狀態下豎直速度 26 self.verLockVelocity = -200; 27 28 // 塊位置超過屏幕多少后,屏幕上升 29 self.raiseLimit = 0.5; 30 31 // 層阻擋高度 32 self.levelHeight = 67; 33 34 // 層間距 35 self.levelInterval = 640; 36 37 // 普通阻擋的邊長 38 self.blockSide = 45; 39 40 // 方塊的邊長 41 self.brickSide = 36; 42 43 // 計算碰撞的最大時間間隔 44 self.preCalcDelta = 0.1; 45 46 // 關卡顏色變化步進 47 self.levelColorStride = 5; 48 49 // 關卡顏色的循環數組 50 self.levelColor = [0x81a3fc, 0xeb7b49, 0xea3430, 0xf5b316, 0x8b5636, 0x985eb5]; 51 52 // 保存配置的等級信息 53 self._levelConfig = null; 54 55 self.runInEditor = true; 56 }, { 57 levelConfigFile: qc.Serializer.EXCELASSET, 58 gravity : qc.Serializer.NUMBER, 59 horVelocity : qc.Serializer.NUMBER, 60 verVelocity : qc.Serializer.NUMBER, 61 verVelocityKeepTime : qc.Serializer.NUMBER, 62 raiseLimit : qc.Serializer.NUMBER, 63 levelHeight : qc.Serializer.NUMBER, 64 levelInterval : qc.Serializer.NUMBER, 65 blockSide : qc.Serializer.NUMBER, 66 preCalcDelta : qc.Serializer.NUMBER, 67 levelColorStride : qc.Serializer.NUMBER, 68 levelColor : qc.Serializer.NUMBERS 69 }); 70 71 GameConfig.prototype.getGameWidth = function() { 72 return this.gameObject.width; 73 }; 74 75 GameConfig.prototype.awake = function() { 76 var self = this; 77 78 // 將配置表轉化下,讀取出等級配置 79 var rows = self.levelConfigFile.sheets.config.rows; 80 var config = []; 81 var idx = -1, len = rows.length; 82 while (++idx < len) { 83 var row = rows[idx]; 84 // 為了方便配置,block部分使用的是javascript的數據定義語法 85 // 通過eval轉化為javascript數據結構 86 row.block = eval(row.block); 87 config.push(row); 88 } 89 90 self._levelConfig = config; 91 92 // 計算出方塊旋轉后中心到頂點的距離 93 self.brickRadius = self.brickSide * Math.sin(Math.PI / 4); 94 }; 95 96 /* 97 * 獲取關卡配置 98 */ 99 GameConfig.prototype.getLevelConfig = function(level) { 100 var self = this; 101 var len = self._levelConfig.length; 102 while (len--) { 103 var row = self._levelConfig[len]; 104 if (row.start > level || (row.end > 0 && row.end < level)) { 105 continue; 106 } 107 return row; 108 } 109 return null; 110 };
(二)構建世界邏輯
《跳躍的方塊》是一個無盡的虛擬世界,世界的高度不限,寬度根據顯示的寬度也不盡相同。為了方便處理顯示,我們設定一個x軸從左至右,y軸從下至上的坐標系,x軸原點位於屏幕中間。如下圖所示:

基礎設定
- 方塊的坐標為方塊中心點的坐標。
- 方塊的初始位置為(0, 480)。
- 關卡的下邊界的y軸坐標值為960。保證第一個屏幕內,看不到關卡;而當方塊跳動后,關卡出現。
- 關卡只需要生成可通行范圍的矩形區域,阻擋區域根據屏幕寬度和可通行區域計算得到。
- 阻擋塊需要生成實際占據的矩形區域。
創建虛擬世界
創建虛擬世界的管理腳本:GameWorld.js。代碼內容如下:
1 var GameWorld = qc.defineBehaviour('qc.JumpingBrick.GameWorld', qc.Behaviour, function() { 2 var self = this; 3 4 // 設置到全局中 5 JumpingBrick.gameWorld = self; 6 7 // 創建結束監聽 8 self.onGameOver = new qc.Signal(); 9 10 // 分數更新的事件 11 self.onScoreChanged = new qc.Signal(); 12 13 self.levelInfo = []; 14 15 self.runInEditor = true; 16 }, { 17 18 }); 19 20 GameWorld.prototype.awake = function() { 21 var self = this; 22 // 初始化狀態 23 this.resetWorld(); 24 };
游戲涉及到的數據
在虛擬世界中,方塊有自己的位置、水平和豎直方向上的速度、受到的重力加速度、點擊后上升速度保持的時間等信息。每次游戲開始時,需要重置這些數據。 現在大家玩游戲的時間很零碎,很難一直關注在游戲上,所以當游戲暫停時,我們需要保存當前的游戲數據。這樣,玩家可以再找合適的時間來繼續游戲。
先將重置、保存數據、恢復數據實現如下:
1 /** 2 * 設置分數 3 */ 4 GameWorld.prototype.setScore = function(score, force) { 5 if (force || score > this.score) { 6 this.score = score; 7 this.onScoreChanged.dispatch(score); 8 } 9 }; 10 11 /** 12 * 重置世界 13 */ 14 GameWorld.prototype.resetWorld = function() { 15 var self = this; 16 17 // 方塊在虛擬世界坐標的位置 18 self.x = 0; 19 self.y = 480; 20 21 // 方塊在虛擬世界的速度值 22 self.horV = 0; 23 self.verV = 0; 24 25 // 當前受到的重力 26 self.gravity = JumpingBrick.gameConfig.gravity; 27 28 // 維持上升速度的剩余時間 29 self.verKeepTime = 0; 30 31 // 死亡線的y軸坐標值 32 self.deadline = 0; 33 34 // 已經生成的關卡 35 self.levelInfo = []; 36 37 // 是否游戲結束 38 self.gameOver = false; 39 40 // 當前的分數 41 self.setScore(0, true); 42 }; 43 44 /** 45 * 獲取要保存的游戲數據 46 */ 47 GameWorld.prototype.saveGameState = function() { 48 var self = this; 49 var saveData = { 50 deadline : self.deadline, 51 x : self.x, 52 y : self.y, 53 horV : self.horV, 54 verV : self.verV, 55 gravity : self.gravity, 56 verKeepTime : self.verKeepTime, 57 levelInfo : self.levelInfo, 58 gameOver : self.gameOver, 59 score : self.score 60 }; 61 return saveData; 62 }; 63 64 /** 65 * 恢復游戲 66 */ 67 GameWorld.prototype.restoreGameState = function(data) { 68 if (!data) { 69 return false; 70 } 71 var self = this; 72 self.deadline = data.deadline; 73 self.x = data.x; 74 self.y = data.y; 75 self.horV = data.horV; 76 self.verV = data.verV; 77 self.gravity = data.gravity; 78 self.verKeepTime = data.verKeepTime; 79 self.levelInfo = data.levelInfo; 80 self.gameOver = data.gameOver; 81 self.setScore(data.score, true); 82 return true; 83 };
動態創建關卡數據
世界坐標已經確定,現在開始着手創建關卡信息。 因為游戲限制了每屏能顯示的關卡數,方塊只會和本關和下關的阻擋間產生碰撞,所以游戲中不用在一開始就創建很多的關卡。而且游戲中方塊不能下落出屏幕,已經通過的,並且不在屏幕的內的關卡,也可以刪除,不予保留。
所以,我們根據需求創建關卡信息,創建完成后保存起來,保證一局游戲中,關卡信息是固定的。 代碼如下:
1 /** 2 * 獲取指定y軸值對應的關卡 3 */ 4 GameWorld.prototype.transToLevel = function(y) { 5 // 關卡從0開始,-1表示第一屏的960區域 6 return y < 960 ? -1 : Math.floor((y - 960) / JumpingBrick.gameConfig.levelInterval); 7 }; 8 9 /** 10 * 獲取指定關卡開始的y軸坐標 11 */ 12 GameWorld.prototype.getLevelStart = function(level) { 13 return level < 0 ? 0 : (960 + level * JumpingBrick.gameConfig.levelInterval); 14 }; 15 16 /** 17 * 刪除關卡數據 18 */ 19 GameWorld.prototype.deleteLevelInfo = function(level) { 20 var self = this; 21 22 delete self.levelInfo[level]; 23 }; 24 25 26 /** 27 * 獲取關卡信息 28 */ 29 GameWorld.prototype.getLevelInfo = function(level) { 30 if (level < 0) 31 return null; 32 33 var self = this; 34 var levelInfo = self.levelInfo[level]; 35 36 if (!levelInfo) { 37 // 不存在則生成 38 levelInfo = self.levelInfo[level] = self.buildLevelInfo(level); 39 } 40 return levelInfo; 41 }; 42 43 /** 44 * 生成關卡 45 */ 46 GameWorld.prototype.buildLevelInfo = function(level) { 47 var self = this, 48 gameConfig = JumpingBrick.gameConfig, 49 blockSide = gameConfig.blockSide, 50 levelHeight = gameConfig.levelHeight; 51 52 var levelInfo = { 53 color: gameConfig.levelColor[Math.floor(level / gameConfig.levelColorStride) % gameConfig.levelColor.length], 54 startY: self.getLevelStart(level), 55 passArea: null, 56 block: [] 57 }; 58 59 // 獲取關卡的配置 60 var cfg = JumpingBrick.gameConfig.getLevelConfig(level); 61 62 // 根據配置的通行區域生成關卡的通行區域 63 var startX = self.game.math.random(cfg.passScopeMin, cfg.passScopeMax - cfg.passWidth); 64 levelInfo.passArea = new qc.Rectangle( 65 startX, 66 0, 67 cfg.passWidth, 68 levelHeight); 69 70 // 生成阻擋塊 71 var idx = -1, len = cfg.block.length; 72 while (++idx < len) { 73 var blockCfg = cfg.block[idx]; 74 // 阻擋塊x坐標的生成范圍是可通行區域的左側x + minX 到 右側x + maxX 75 var blockX = startX + 76 self.game.math.random(blockCfg.minx, cfg.passWidth + blockCfg.maxx - blockSide); 77 // 阻擋塊y坐標的生成范圍是關卡上邊界y + minY 到上邊界y + maxY 78 var blockY = JumpingBrick.gameConfig.levelHeight + 79 self.game.math.random(blockCfg.miny, blockCfg.maxy - blockSide); 80 81 levelInfo.block.push(new qc.Rectangle( 82 blockX, 83 blockY, 84 blockSide, 85 blockSide)); 86 } 87 return levelInfo; 88 };
分數計算
根據設定,當方塊完全通過關卡的通行區域后,就加上一分,沒有其他的加分途徑,於是,可以將分數計算簡化為計算當前完全通過的最高關卡。代碼如下:
1 /** 2 * 更新分數 3 */ 4 GameWorld.prototype.calcScore = function() { 5 var self = this; 6 7 // 當前方塊所在關卡 8 var currLevel = self.transToLevel(self.y); 9 // 當前關卡的起點 10 var levelStart = self.getLevelStart(currLevel); 11 12 // 當方塊完全脫離關卡通行區域后計分 13 var overLevel = self.y - levelStart - JumpingBrick.gameConfig.levelHeight - JumpingBrick.gameConfig.brickRadius; 14 var currScore = overLevel >= 0 ? currLevel + 1 : 0; 15 self.setScore(currScore); 16 };
物理表現
方塊在移動過程中,會被給予向左或者向右跳的指令。下達指令后,方塊被賦予一個向上的速度,和一個水平方向的速度,向上的速度會保持一段時間后才受重力影響。 理清這些效果后,可以用下面這段代碼來處理:
1 /** 2 * 控制方塊跳躍 3 * @param {number} direction - 跳躍的方向 < 0 時向左跳,否則向右跳 4 */ 5 GameWorld.prototype.brickJump = function(direction) { 6 var self = this; 7 // 如果重力加速度為0,表示方塊正在靠邊滑動,只響應往另一邊跳躍的操作 8 if (self.gravity === 0 && direction * self.x >= 0) { 9 return; 10 } 11 // 恢復重力影響 12 self.gravity = JumpingBrick.gameConfig.gravity; 13 self.verV = JumpingBrick.gameConfig.verVelocity; 14 self.horV = (direction < 0 ? -1 : 1) * JumpingBrick.gameConfig.horVelocity; 15 self.verKeepTime = JumpingBrick.gameConfig.verVelocityKeepTime; 16 }; 17 18 /** 19 * 移動方塊 20 * @param {number} delta - 經過的時間 21 */ 22 GameWorld.prototype.moveBrick = function(delta) { 23 var self = this; 24 25 // 首先處理水平方向上的移動 26 self.x += self.horV * delta; 27 28 // 再處理垂直方向上得移動 29 if (self.verKeepTime > delta) { 30 // 速度保持時間大於經歷的時間 31 self.y += self.verV * delta; 32 self.verKeepTime -= delta; 33 } 34 else if (self.verKeepTime > 0) { 35 // 有一段時間在做勻速運動,一段時間受重力加速度影響 36 self.y += self.verV * delta + 0.5 * self.gravity * Math.pow(delta - self.verKeepTime, 2); 37 self.verV += self.gravity * (delta - self.verKeepTime); 38 self.verKeepTime = 0; 39 } 40 else { 41 // 完全受重力加速度影響 42 self.y += self.verV * delta + 0.5 * self.gravity * Math.pow(delta, 2); 43 self.verV += self.gravity * delta; 44 } 45 };
碰撞檢測
這樣方塊就開始運動了,需要讓它和屏幕邊緣、關卡通道、阻擋碰撞,產生不同的效果。
- 當方塊與關卡阻擋碰撞后,結束游戲。
- 當方塊與屏幕下邊緣碰撞后,結束游戲。
- 當方塊與屏幕左右邊緣碰撞后,將不受重力加速度影響,沿屏幕邊緣做向下的勻速運動,直到游戲結束,或者接收到一個向另一邊邊緣跳躍的指令后恢復正常。
旋轉45°后的方塊與矩形的碰撞:
- 當方塊的包圍矩形和矩形不相交時,不碰撞。
- 當方塊的包圍矩形和矩形相交時。如下圖分為兩種情況處理。

代碼實現如下:
1 /** 2 * 掉出屏幕外結束 3 */ 4 GameWorld.GAMEOVER_DEADLINE = 1; 5 /** 6 * 碰撞結束 7 */ 8 GameWorld.GAMEOVER_BLOCK = 2; 9 10 /** 11 * 塊與一個矩形阻擋的碰撞檢測 12 */ 13 GameWorld.prototype.checkRectCollide = function(x, y, width, height) { 14 var self = this, 15 brickRadius = JumpingBrick.gameConfig.brickRadius; 16 17 var upDis = self.y - y - height; // 距離上邊距離 18 if (upDis >= brickRadius) 19 return false; 20 21 var downDis = y- self.y; // 距離下邊距離 22 if (downDis >= brickRadius) 23 return false; 24 25 var leftDis = x - self.x; // 距離左邊距離 26 if (leftDis >= brickRadius) 27 return false; 28 29 var rightDis = self.x - x - width; // 記錄右邊距離 30 if (rightDis >= brickRadius) 31 return false; 32 33 // 當塊中點的y軸值,在阻擋的范圍內時,中點距離左右邊的邊距小於brickRadius時相交 34 if (downDis < 0 && upDis < 0) { 35 return leftDis < brickRadius && rightDis < brickRadius; 36 } 37 38 // 當塊的中點在阻擋范圍上時 39 if (upDis > 0) { 40 return leftDis < brickRadius - upDis && rightDis < brickRadius - upDis; 41 } 42 // 當塊的中點在阻擋范圍下時 43 if (downDis > 0) { 44 return leftDis < brickRadius - downDis && rightDis < brickRadius - downDis; 45 } 46 return false; 47 }; 48 49 /** 50 * 碰撞檢測 51 */ 52 GameWorld.prototype.checkCollide = function() { 53 var self = this; 54 55 // game節點鋪滿了屏幕,那么節點的寬即為屏幕的寬 56 var width = this.gameObject.width; 57 var brickRadius = JumpingBrick.gameConfig.brickRadius; 58 var leftEdge = -0.5 * width; 59 var rightEdge = 0.5 * width; 60 61 // 下邊緣碰撞判定,方塊中心的位置距離下邊緣的距離小於方塊的中心到頂點的距離 62 if (this.deadline - self.y > brickRadius) { 63 return GameWorld.GAMEOVER_DEADLINE; 64 } 65 66 // 左邊緣判定,方塊中心的位置距離左邊緣的距離小於方塊的中心到頂點的距離 67 if (self.x - leftEdge < brickRadius) { 68 self.x = leftEdge + brickRadius; 69 self.horV = 0; 70 self.verV = JumpingBrick.gameConfig.verLockVelocity; 71 self.gravity = 0; 72 } 73 // 右邊緣判定,方塊中心的位置距離右邊緣的距離小於方塊的中心到頂點的距離 74 if (rightEdge - self.x < brickRadius) { 75 self.x = rightEdge - brickRadius; 76 self.horV = 0; 77 self.verV = JumpingBrick.gameConfig.verLockVelocity; 78 self.gravity = 0; 79 } 80 81 // 方塊在世界中,只會與當前關卡的阻擋和下一關的阻擋進行碰撞 82 var currLevel = self.transToLevel(self.y); 83 for (var idx = currLevel, end = currLevel + 2; idx < end; idx++) { 84 var level = self.getLevelInfo(idx); 85 if (!level) 86 continue; 87 88 var passArea = level.passArea; 89 // 檢測通道左側和右側阻擋 90 if (self.checkRectCollide( 91 leftEdge, 92 passArea.y + level.startY, 93 passArea.x - leftEdge, 94 passArea.height) || 95 self.checkRectCollide( 96 passArea.x + passArea.width, 97 passArea.y + level.startY, 98 rightEdge - passArea.x - passArea.width, 99 passArea.height)) { 100 return GameWorld.GAMEOVER_BLOCK; 101 } 102 103 // 檢測本關的阻擋塊 104 var block = level.block; 105 var len = block.length; 106 while (len--) { 107 var rect = block[len]; 108 if (self.checkRectCollide(rect.x, rect.y + level.startY, rect.width, rect.height)) { 109 return GameWorld.GAMEOVER_BLOCK; 110 } 111 } 112 } 113 114 return 0; 115 };
添加時間處理
到此,游戲世界的基本邏輯差不多快完成了。現在加入時間控制。
1 /** 2 * 游戲結束的處理 3 */ 4 GameWorld.prototype.doGameOver = function(type) { 5 var self = this; 6 self.gameOver = true; 7 self.onGameOver.dispatch(type); 8 }; 9 10 /** 11 * 更新邏輯處理 12 * @param {number} delta - 上一次計算到現在經歷的時間,單位:秒 13 */ 14 GameWorld.prototype.updateLogic = function(delta) { 15 var self = this, 16 screenHeight = self.gameObject.height; 17 if (self.gameOver) { 18 return; 19 } 20 // 將經歷的時間分隔為一小段一小段進行處理,防止穿越 21 var calcDetla = 0; 22 while (delta > 0) { 23 calcDetla = Math.min(delta, JumpingBrick.gameConfig.preCalcDelta); 24 delta -= calcDetla; 25 // 更新方塊位置 26 self.moveBrick(calcDetla); 27 // 檢測碰撞 28 var ret = self.checkCollide(); 29 if (ret !== 0) { 30 // 如果碰撞關卡阻擋或者碰撞死亡線則判定死亡 31 self.doGameOver(ret); 32 return; 33 } 34 } 35 36 // 更新DeadLine 37 self.deadline = Math.max(self.y - screenHeight * JumpingBrick.gameConfig.raiseLimit, self.deadline); 38 39 // 結算分數 40 self.calcScore(); 41 };
經過前面的准備,虛擬游戲世界已經構建完成,下次將講解如何着手將虛擬世界呈現出來。敬請期待!
其他相關鏈接
開源免費的HTML5游戲引擎——青瓷引擎(QICI Engine) 1.0正式版發布了!
青瓷引擎之純JavaScript打造HTML5游戲第二彈——《跳躍的方塊》Part 1

