繼上一次介紹了《神奇的六邊形》的完整遊戲開發流程後(可點擊這裏查看),此次將爲你們介紹另一款魔性遊戲《跳躍的方塊》的完整開發流程。javascript
(點擊圖片可進入遊戲體驗)php
因內容太多,爲方便你們閱讀,因此分屢次來說解。html
若要一次性查看全部文檔,也可點擊這裏。java
接上回(《跳躍的方塊》Part 1)web
三. 遊戲世界數組
爲了能更快的體驗到遊戲的主體玩法,調整遊戲數值,這裏咱們先來搭建遊戲世界。數據結構
在《跳躍的方塊》中,下一關的信息尤其關鍵。若是能提早獲知阻擋點或者通道位置,會爲當前的操做提供必定的指導。爲了保證全部玩家獲取的信息基本一致,屏幕中顯示的關卡數量須要嚴格的控制。app
因此這裏咱們將屏幕的高度經過UIRoot映射爲一個固定值:960,添加一個鎖定屏幕旋轉方向的腳本,並建立遊戲的根節點game,設置game節點鋪滿屏幕。
操做以下所示:dom
(一)遊戲配置ide
這個遊戲中,一些參數會嚴重影響用戶體驗,須要進行不停的嘗試,以找到最合適的設置。因此,這裏將這些參數提取出來,羣策羣力,快速迭代出最終版本。
分析遊戲內容後,將遊戲數據分爲兩類:
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軸原點位於屏幕中間。以下圖所示:
建立虛擬世界的管理腳本: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 };
通過前面的準備,虛擬遊戲世界已經構建完成,下次將講解如何着手將虛擬世界呈現出來。敬請期待!
其餘相關連接