青瓷引擎之純JavaScript打造HTML5遊戲第二彈——《跳躍的方塊》Part 2

繼上一次介紹了《神奇的六邊形》的完整遊戲開發流程後可點擊這裏查看,此次將爲你們介紹另一款魔性遊戲《跳躍的方塊》的完整開發流程。javascript

          (點擊圖片可進入遊戲體驗)php

因內容太多,爲方便你們閱讀,因此分屢次來說解。html

若要一次性查看全部文檔,也可點擊這裏java

 

接上回(《跳躍的方塊》Part 1web

三. 遊戲世界數組

爲了能更快的體驗到遊戲的主體玩法,調整遊戲數值,這裏咱們先來搭建遊戲世界。數據結構

創建基礎世界

在《跳躍的方塊》中,下一關的信息尤其關鍵。若是能提早獲知阻擋點或者通道位置,會爲當前的操做提供必定的指導。爲了保證全部玩家獲取的信息基本一致,屏幕中顯示的關卡數量須要嚴格的控制。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 };
View Code

 

(二)構建世界邏輯

《跳躍的方塊》是一個無盡的虛擬世界,世界的高度不限,寬度根據顯示的寬度也不盡相同。爲了方便處理顯示,咱們設定一個x軸從左至右,y軸從下至上的座標系,x軸原點位於屏幕中間。以下圖所示:

基礎設定

  1. 方塊的座標爲方塊中心點的座標。
  2. 方塊的初始位置爲(0, 480)。
  3. 關卡的下邊界的y軸座標值爲960。保證第一個屏幕內,看不到關卡;而當方塊跳動後,關卡出現。
  4. 關卡只須要生成可通行範圍的矩形區域,阻擋區域根據屏幕寬度和可通行區域計算獲得。
  5. 阻擋塊須要生成實際佔據的矩形區域。

建立虛擬世界

建立虛擬世界的管理腳本: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 };
View Code

遊戲涉及到的數據

在虛擬世界中,方塊有本身的位置、水平和豎直方向上的速度、受到的重力加速度、點擊後上升速度保持的時間等信息。每次遊戲開始時,須要重置這些數據。 如今你們玩遊戲的時間很零碎,很難一直關注在遊戲上,因此當遊戲暫停時,咱們須要保存當前的遊戲數據。這樣,玩家能夠再找合適的時間來繼續遊戲。
先將重置、保存數據、恢復數據實現以下:

 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 };
View Code

動態建立關卡數據

世界座標已經肯定,如今開始着手建立關卡信息。 由於遊戲限制了每屏能顯示的關卡數,方塊只會和本關和下關的阻擋間產生碰撞,因此遊戲中不用在一開始就建立不少的關卡。並且遊戲中方塊不能下落出屏幕,已經經過的,而且不在屏幕的內的關卡,也能夠刪除,不予保留。
因此,咱們根據需求建立關卡信息,建立完成後保存起來,保證一局遊戲中,關卡信息是固定的。 代碼以下:

 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 };
View Code

分數計算

根據設定,當方塊徹底經過關卡的通行區域後,就加上一分,沒有其餘的加分途徑,因而,能夠將分數計算簡化爲計算當前徹底經過的最高關卡。代碼以下:

 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 };
View Code

物理表現

方塊在移動過程當中,會被給予向左或者向右跳的指令。下達指令後,方塊被賦予一個向上的速度,和一個水平方向的速度,向上的速度會保持一段時間後才受重力影響。 理清這些效果後,能夠用下面這段代碼來處理:

 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 };
View Code

碰撞檢測

這樣方塊就開始運動了,須要讓它和屏幕邊緣、關卡通道、阻擋碰撞,產生不一樣的效果。

  1. 當方塊與關卡阻擋碰撞後,結束遊戲。
  2. 當方塊與屏幕下邊緣碰撞後,結束遊戲。
  3. 當方塊與屏幕左右邊緣碰撞後,將不受重力加速度影響,沿屏幕邊緣作向下的勻速運動,直到遊戲結束,或者接收到一個向另外一邊邊緣跳躍的指令後恢復正常。

旋轉45°後的方塊與矩形的碰撞:

  1. 當方塊的包圍矩形和矩形不相交時,不碰撞。
  2. 當方塊的包圍矩形和矩形相交時。以下圖分爲兩種狀況處理。

代碼實現以下:

  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 };
View Code

添加時間處理

到此,遊戲世界的基本邏輯差很少快完成了。如今加入時間控制。

 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 };
View Code

 

 

通過前面的準備,虛擬遊戲世界已經構建完成,下次將講解如何着手將虛擬世界呈現出來。敬請期待!

 

其餘相關連接

開源免費的HTML5遊戲引擎——青瓷引擎(QICI Engine) 1.0正式版發佈了!

JS開發HTML5遊戲《神奇的六邊形》(一)

青瓷引擎之純JavaScript打造HTML5遊戲第二彈——《跳躍的方塊》Part 1

相關文章
相關標籤/搜索