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

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

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

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

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

 

接上回(《跳躍的方塊》Part 2app

 

(三)控制展現遊戲世界編輯器

 

通過前面的準備,虛擬遊戲世界已經構建完成,開始着手將虛擬世界呈現出來。ide

添加圖形資源

導入全部的圖形資源到Assets/atlas/main@atlas下,並從新打包圖集。
佈局

建立功能按鈕和界面

在game下添加引導界面、暫停按鈕(UIImage)、分數顯示(UIText)、暫停界面(Node)、暫停界面下的恢復按鈕(UIImage)和一個半透明層(UIImage)post

  • 暫停按鈕
    暫停按鈕的範圍爲相對於父節點左上角(20, 20, 70, 70),而且須要能接受事件。測試

  • 分數顯示
    分數顯示區域爲相對於父節點右上角(width - 90, 20, width - 110, 70)的範圍,並設置文本向右對齊。

  • 暫停界面
    暫停界面有一個屏蔽操做用的半透明遮罩和一個恢復按鈕組成。遮罩須要鋪滿全屏,恢復按鈕以父節點的中心爲錨點,向下偏移40。 遮罩和恢復按鈕都須要接受事件。

  • 引導界面 引導界面提示玩家應該如何操做,以屏幕中心爲描點,進行佈局。

一點關於佈局的理解

顯示的內容能夠看作一個矩形區域,Pivot控制節點原點在矩形區域中的位置,Anchors和偏移值(left,right,top, bottom, anchoredX, anchoredY, width, height)則控制矩形四個頂點的位置。
因此設置時,先肯定但願節點原點的位置,設置好Pivot後,根據但願的矩形設置四個頂點的位置。
假設:父節點的高度、寬度分別爲h、w。那麼當四個邊能夠根據公式表達分別爲:

  • x1 = a1 * w + b1
  • x2 = a2 * w + b2
  • y1 = c1 * h + d1
  • y2 = c2 * h + d2

就能夠經過以下設置達到但願的效果:

  • anchor中 minX = a1, maxX = a2。當a1 === a2時,設置width = -b2 - b1;不然設置left = b1,right = -b2。
  • anchor中 minY = c1, maxY = c2。當c1 === c2時,設置height = -d2 - d1; 否者設置top = d1,right = -d2。

建立遊戲世界的基礎座標系

在前面章節中建立的game節點,是一個鋪滿屏幕的節點,能夠理解爲對應屏幕,且Pivot爲(0,0),那麼座標系爲從左上到右下的一個座標系,這個座標系和虛擬世界的不一樣,須要轉換下。 在game節點下建立一個節點origin,把origin做爲顯示虛擬世界的原點。

  1. 先調整x軸的座標系。 調整origin的width爲0,MinAnchors中的minX,maxX調整爲0.5。這樣origin就位於父節點的水平中心上。
  2. 在調整y軸方向。 原來的y軸正方向爲從上至下,而虛擬世界的倒是從下至上,因此對節點進行垂直翻轉(scaleY = -1)來達到預期效果。
  3. 調整y軸原點位置。 垂直翻轉後,原點位置位於屏幕上邊緣,經過設置AnchoredY=960將節點移動到屏幕下邊緣。 最終該節點的佈局參數以下:

映射規則

  • 原點所在的y軸即爲遊戲的deadLine。
  • 方塊在屏幕中的位置爲:y - deadLine。
  • 關卡在屏幕中的位置爲:-deadLine。

建立關卡父節點

在虛擬世界的建立過程當中,分析了關卡的特性,在顯示時只須要顯示屏幕中的關卡,甚至連建立也不須要,而且關卡是一個連着一個,有點相似於單列表格的形式。因而這裏選擇使用官方插件中的TableView來實現關卡效果。 使用TableView時,須要爲全部的關卡建立一個父節點,和建立方塊相似,咱們建立一個levels的Node節點,做爲全部關卡的父節點。 佈局參數以下:

建立方塊

在origin節點下建立一個UIImage節點:brick。設置它相對於父節點的上邊緣水平中心爲錨點,以本身的中心爲中心,旋轉45度。 最終佈局參數以下: 

 

建立座標系、關卡父節點、方塊的具體操做以下:

 

建立關卡數據適配器

使用TableView時,還須要一個數據適配器(TableViewAdapter)來提供關卡數據。 先引入插件ExtraUI(com.qici.extraUI),創建一個腳本LevelAdapter。內容以下:

 1 var LevelAdapter = qc.defineBehaviour('qc.engine.LevelAdapter', com.qici.extraUI.TableViewAdapter, function() {
 2     var self = this;
 3 
 4     // 載入配置和遊戲世界
 5     self.config = JumpingBrick.gameConfig;
 6     self.world = JumpingBrick.gameWorld;
 7 }, {
 8 });
 9 
10 LevelAdapter.prototype.awake = function() {
11 };
12 
13 /**
14  * 獲取表格大小,x、y同時只能有一個爲Infinity
15  * @return {{x: number|Infinity, y: number| Infinity}}
16  */
17 LevelAdapter.prototype.getTableSize = function() {
18     // 關卡爲無限的
19     return { x: 1, y: Infinity};
20 };
21 
22 /**
23  * 根據在Table中的點返回對應的單元格
24  * @param  {number} x - x軸座標
25  * @param  {number} y - y軸座標
26  * @return {{x: number, y: number}}} 返回點所在的單元格信息
27  */
28 LevelAdapter.prototype.findCellWithPos = function(x, y) {
29     // 第一個格子爲第一屏960的高度,第二個格子爲第一關
30     return { 
31         x: 0,
32         y: y < 960 ? 0 : (1 + Math.floor((y - 960) / this.config.levelInterval))
33     };
34 };
35 
36 /**
37  * 獲取節點的顯示位置
38  */
39 LevelAdapter.prototype.getCellRect = function(col, row) {
40     if (row === 0) 
41         return new qc.Rectangle(0, 0, 100, 960);
42     else
43         return new qc.Rectangle(0, 960 + (row - 1) * this.config.levelInterval, 100, this.config.levelInterval);
44 };
45 
46 /**
47  * 節點處於不可見時,回收節點,
48  * @param  {qc.Node} cell - 節點
49  * @param  {number} col - 所在列
50  * @param  {number} row - 所在行
51  */
52 LevelAdapter.prototype.revokeCell = function(cell, col, row) {
53     // 關卡不可見時,刪除已經生成的關卡數據
54     this.world.deleteLevelInfo(row - 1);
55 };
56 
57 /**
58  * 節點處於可見時,建立節點,
59  * @param  {qc.Node} cell - 節點
60  * @param  {number} col - 所在列
61  * @param  {number} row - 所在行
62  */
63 LevelAdapter.prototype.createCell = function(cell, col, row) {
64     // 建立關卡時,設置關卡信息
65     var self = this,
66         levelInfo = self.world.getLevelInfo(row - 1);
67     cell.levelShow.setLevelInfo(levelInfo);
68 };
View Code

 

建立關卡的單元格處理腳本

建立腳本LevelShow,用來控制關卡預製的顯示方式。 內容以下:

 1 var LevelShow = qc.defineBehaviour('qc.engine.LevelShow', qc.Behaviour, function() {
 2     // 將腳本對象關聯到節點上
 3     this.gameObject.levelShow = this;
 4 }, {
 5     leftLevel : qc.Serializer.NODE,   // 關卡左邊阻擋
 6     rightLevel : qc.Serializer.NODE,  // 關卡右邊阻擋
 7     block : qc.Serializer.NODE        // 阻擋塊的父節點
 8 });
 9 
10 LevelShow.prototype.onDestory = function() {
11     // 釋放關聯
12     this.gameObject.levelShow = null;
13 };
14 
15 LevelShow.prototype.update = function() {
16     var self = this,
17         width = JumpingBrick.gameConfig.getGameWidth();
18     // 若是是電腦瀏覽器打開,遊戲顯示的寬度可能會變化,因此須要根據屏幕寬度的變化,動態調整關卡阻擋的範圍。
19     // 防止左右兩邊出現空白區域
20     if (width !== self.recordWidth) {
21         var diff = (width - self.recordWidth) / 2;
22         self.recordWidth = width;
23 
24         if (diff + self.leftLevel.width > 0) {
25             self.leftLevel.x -= diff;
26             self.leftLevel.width += diff;    
27         }
28 
29         if (diff + self.rightLevel.width > 0) {
30             self.rightLevel.width += diff;    
31         }
32     }
33 };
34 
35 LevelShow.prototype.setLevelInfo = function(levelInfo) {
36     var self = this,
37         width = JumpingBrick.gameConfig.getGameWidth();
38     var blockChildren = self.block.children;
39     var blockLen = blockChildren.length;
40 
41     self.recordWidth = width;
42     if (!levelInfo) {
43         self.leftLevel.visible = self.rightLevel.visible = false;
44         while (blockLen--) {
45             blockChildren[blockLen].visible = false;
46         }
47         return;
48     }
49     var passArea = levelInfo.passArea,
50         color = new qc.Color(levelInfo.color);
51 
52     self.leftLevel.visible = self.rightLevel.visible = true;
53     // 設置左邊阻擋
54     self.leftLevel.x = -0.5 * width;
55     self.leftLevel.y = passArea.y;
56     self.leftLevel.width = passArea.x - self.leftLevel.x;
57     self.leftLevel.height = passArea.height;
58     self.leftLevel.colorTint = color;
59 
60     // 設置右邊阻擋
61     self.rightLevel.x = passArea.x + passArea.width;
62     self.rightLevel.y = passArea.y;
63     self.rightLevel.width = 0.5 * width - self.rightLevel.x;
64     self.rightLevel.height = passArea.height;
65     self.rightLevel.colorTint = color;
66 
67     // 確保塊夠用
68     while (blockLen < levelInfo.block.length) {
69         blockLen++;
70         self.game.add.clone(self.leftLevel, self.block);
71     }
72 
73     blockChildren = self.block.children;
74     blockLen = blockChildren.length;
75     var idx = -1;
76     while (++idx < blockLen) {
77         var blockInfo = levelInfo.block[idx];
78         if (!blockInfo) {
79             blockChildren[idx].visible = false;
80         }
81         else {
82             blockChildren[idx].colorTint = color;
83             blockChildren[idx].visible = true;
84             blockChildren[idx].x = blockInfo.x;
85             blockChildren[idx].y = blockInfo.y;
86             blockChildren[idx].width = blockInfo.width;
87             blockChildren[idx].height = blockInfo.height;
88         }
89     }
90 };
View Code

 

建立關卡預製

建立一個預製level,level下有三個節點:leftLevel(UIImage),rightLevel(UIImage),block(Node)。 併爲其添加上一步建立的腳本LevelShow。

構建控制腳本

建立腳本GameControl

建立腳本,並預設功能相關的節點,監聽相關事件。具體實現以下:

 1 /**
 2  * 遊戲控制,將虛擬世界投影到遊戲世界,並管理暫停等處理
 3  */
 4 var GameControl = qc.defineBehaviour('qc.JumpingBrick.GameControl', qc.Behaviour, function() {
 5     var self = this;
 6 
 7     // 設置到全局中
 8     JumpingBrick.gameControl = self;
 9 
10     // 方塊
11     self.brick = null;
12 
13     // 關卡的父節點,用於動態掛載關卡節點
14     self.levelParent = null;
15 
16     // 開始指引界面
17     self.startManual = null;
18 
19     // 暫停界面
20     self.pausePanel = null;
21 
22     // 暫停按鈕
23     self.pauseButton = null;
24 
25     // 回到遊戲按鈕
26     self.resumeButton = null;
27 
28     // 當前的狀態
29     self._state = 0;
30 }, {
31     brick: qc.Serializer.NODE,
32     tableViewNode : qc.Serializer.NODE,
33     scoreText: qc.Serializer.NODE,
34     levelParent: qc.Serializer.NODE,
35     startManual: qc.Serializer.NODE,
36     pausePanel: qc.Serializer.NODE,
37     pauseButton: qc.Serializer.NODE,
38     resumeButton: qc.Serializer.NODE
39 });
40 
41 /**
42  * 初始化
43  */
44 GameControl.prototype.awake = function() {
45     var self = this,
46         config = JumpingBrick.gameConfig;
47 
48     // 監聽節點的鼠標或者觸摸按下事件
49     self.addListener(self.gameObject.onDown, self.doPointDown, self);
50     // 監聽鍵盤事件
51     self.addListener(self.game.input.onKeyDown, self.doKeyDown, self);
52     // 監聽暫停按鈕
53     self.pauseButton && self.addListener(self.pauseButton.onClick, self.doPause, self);
54     // 監聽恢復按鈕
55     self.resumeButton && self.addListener(self.resumeButton.onClick, self.doResume, self);
56     // 監聽遊戲結束
57     self.addListener(JumpingBrick.gameWorld.onGameOver, self.doGameOver, self);
58 
59       // 監聽分數變化
60       self.addListener(JumpingBrick.gameWorld.onScoreChanged, self.doScoreChanged, self);
61 
62     // 獲取Brick上的結束時播放的TweenPosition
63     self._brickTweenPosition = self.brick.getScript('qc.TweenPosition');
64     if (self._brickTweenPosition)
65     self.addListener(self._brickTweenPosition.onFinished, self.doGameFinished, self);
66 
67     // 獲取levelParent上的結束時播放的TweenPosition
68     self._levelTweenPosition = self.levelParent.getScript('qc.TweenPosition');
69 
70     // 根據配置初始化方塊信息
71     if (self.brick) {
72         self.brick.width = self.brick.height = config.brickSide;
73         self.brick.rotation = Math.PI / 4;
74     }
75 
76     // 初始化
77     self.switchState(GameControl.STATE_MANUEL);
78 };
79 
80 /**
81  * 銷燬時
82  */
83 GameControl.prototype.onDestroy = function() {
84    // 預生成的關卡節點清理
85     this._blockPool = [];
86 
87     // 使用中的關卡節點清理
88     this._showLevel = [];
89 };
View Code

 

運行時狀態管理

遊戲運行時,分爲開始引導、遊戲運行、遊戲暫停、遊戲結束4個狀態,對這四個狀態進行統一管理。代碼以下:

 1 /**
 2  * 遊戲開始時,指引界面狀態
 3  */
 4 GameControl.STATE_MANUEL     = 0;
 5 /**
 6  * 遊戲運行狀態
 7  */
 8 GameControl.STATE_RUN         = 1;
 9 /**
10  * 遊戲暫停狀態
11  */
12 GameControl.STATE_PAUSE     = 2;
13 
14 /**
15  * 遊戲結束處理
16  */
17 GameControl.STATE_GAMEOVER     = 3;
18 
19 /**
20  * 切換狀態
21  */
22 GameControl.prototype.switchState = function(state) {
23     var self = this;
24     self.state = state;
25     self.startManual.visible = self.state === GameControl.STATE_MANUEL;
26     if (self.startManual.visible) {
27         // 進入開始引導時,必須重置遊戲世界
28         JumpingBrick.gameWorld.resetWorld();
29         self.tableViewNode.getScript('com.qici.extraUI.TableView').revokeAllCell();
30     }
31 
32      self.pausePanel.visible = self.state === GameControl.STATE_PAUSE;
33     // 同步虛擬世界和顯示
34      self.syncWorld();
35 };
View Code

 

處理暫停和恢復時的數據保存及恢復

 1 /**
 2  * 保存遊戲
 3  */
 4 GameControl.prototype.saveGameState = function() {
 5     var self = this,
 6         gameWorld = JumpingBrick.gameWorld,
 7         data = JumpingBrick.data;
 8     if (!data)
 9         return;
10     var saveData = gameWorld.saveGameState();
11     data.saveGameState(saveData);
12 };
13 
14 /**
15  * 恢復遊戲
16  */
17 GameControl.prototype.restoreGameState = function() {
18     var self = this,
19         gameWorld = JumpingBrick.gameWorld,
20         data = JumpingBrick.data;
21     if (!data)
22         return;
23     var saveData = data.restoreGameState();
24     if (saveData) {
25         gameWorld.restoreGameState(saveData);
26         self.switchState(GameControl.STATE_PAUSE);    
27     }
28 };
29 
30 /**
31  * 清理遊戲
32  */
33 GameControl.prototype.clearGameState = function() {
34     var self = this,
35         data = JumpingBrick.data;
36     if (!data)
37         return;
38     data.clearGameState();
39 };
View Code

 

處理功能效果

對暫停、恢復進行處理。

 1 /**
 2  * 處理暫停
 3  */
 4 GameControl.prototype.doPause = function() {
 5     var self = this;
 6     self.saveGameState();
 7     self.switchState(GameControl.STATE_PAUSE);
 8 };
 9 
10 /**
11  * 處理恢復
12  */
13 GameControl.prototype.doResume = function() {
14     var self = this;
15     self.clearGameState();
16     self.switchState(GameControl.STATE_RUN);
17 };
View Code

 

處理輸入事件處理

讓遊戲支持輸入。

 1 **
 2  * 處理方塊跳躍
 3  */
 4 GameControl.prototype.doBrickJump = function(direction) {
 5     var self = this,
 6         world = JumpingBrick.gameWorld;
 7 
 8     if (self.state === GameControl.STATE_MANUEL) {
 9         // 引導狀態跳躍直接切換到運行狀態
10         self.switchState(GameControl.STATE_RUN);
11     }
12 
13     world.brickJump(direction);
14 };
15 
16 /**
17  * 處理點擊
18  */
19 GameGControl.prototype.doPointDown = function(node, event) {
20     var self = this;
21     if (self.state !== GameControl.STATE_MANUEL &&
22         self.state !== GameControl.STATE_RUN) {
23         return;
24     }
25     var localPoint = self.gameObject.toLocal({x: event.source.x, y: event.source.y});
26     var halfWidth = self.gameObject.width * 0.5;
27     self.doBrickJump(localPoint.x - halfWidth);
28 };
29 
30 /**
31  * 處理鍵盤
32  */
33 GameControl.prototype.doKeyDown = function(keycode) {
34     var self = this;
35     if (keycode === qc.Keyboard.LEFT || keycode === qc.Keyboard.RIGHT) {
36         if (self.state !== GameControl.STATE_MANUEL &&
37             self.state !== GameControl.STATE_RUN) {
38             return;
39         }
40         self.doBrickJump(keycode === qc.Keyboard.LEFT ? -1 : 1);
41     }
42     else if (keycode === qc.Keyboard.ENTER || keycode === qc.Keyboard.SPACEBAR) {
43         if (self.state === GameControl.STATE_RUN) {
44             self.doPause();
45         }
46         else if (self.state === GameControl.STATE_PAUSE) {
47             self.doResume();
48         }
49     }
50 };
View Code

 

 

處理遊戲世界的事件

須要處理遊戲世界反饋回來的分數變動和遊戲結束事件。

 1 /**
 2  * 分數變動
 3  */
 4 GameControl.prototype.doScoreChanged = function(score) {
 5     var self = this;
 6     if (self.scoreText) {
 7         self.scoreText.text = '' + score;
 8     }
 9     JumpingBrick.data.buildShareContent(score);
10 };
11 
12 /**
13  * 處理遊戲結束
14  */
15 GameControl.prototype.doGameOver = function(type) {
16     var self = this;
17     // 切換狀態
18     self.switchState(GameControl.STATE_GAMEOVER);
19     // 播放結束動畫
20     if (type !== qc.JumpingBrick.GameWorld.GAMEOVER_DEADLINE && self._brickTweenPosition) {
21         if (self._levelTweenPosition) {
22             self._levelTweenPosition.setCurrToStartValue();
23             self._levelTweenPosition.setCurrToEndValue();
24             self._levelTweenPosition.to.x += 6;
25             self._levelTweenPosition.to.y += 6;
26             self._levelTweenPosition.resetToBeginning();
27             qc.Tween.playGroup(self.levelParent, 1);
28         }
29         self._brickTweenPosition.setCurrToStartValue();
30         self._brickTweenPosition.setCurrToEndValue();
31         self._brickTweenPosition.to.y = -2 * JumpingBrick.gameConfig.brickRadius;
32         self._brickTweenPosition.duration = Math.max(0.01, Math.sqrt(Math.abs(2 * (self._brickTweenPosition.to.y - self._brickTweenPosition.from.y) / JumpingBrick.gameConfig.gravity)));    
33         self._brickTweenPosition.resetToBeginning();
34         qc.Tween.playGroup(self.brick, 1);
35     }
36     else {
37         self.doGameFinished();
38     }
39 
40 };
41 
42 /**
43  * 處理遊戲完結
44  */
45 GameControl.prototype.doGameFinished = function() {
46     var self = this;
47     // 更新數據
48     if (JumpingBrick.data)
49         JumpingBrick.data.saveScore(JumpingBrick.gameWorld.score);
50 
51     // 切換到結算界面
52     qc.Tween.stopGroup(self.brick, 1);
53     qc.Tween.stopGroup(self.levelParent, 1);
54     self.brick.rotation = Math.PI / 4;
55     // 當不存在界面管理時,直接從新開始遊戲
56     if (JumpingBrick.uiManager)
57         JumpingBrick.uiManager.switchStateTo(qc.JumpingBrick.UIManager.GameOver);
58     else
59         self.switchState(GameControl.STATE_MANUEL);
60 };
View Code

 

調度遊戲並同步世界顯示

 1 GameControl.prototype.resetFPS = function() {
 2     var self = this;
 3     self.game.debug.total = 0;
 4     self._fpsCount = 1;
 5 };
 6 
 7 /**
 8  * 每幀更新
 9  */
10 GameControl.prototype.update = function() {
11     var self = this;
12 
13     if (self.state === GameControl.STATE_RUN) {
14         // 只有運行狀態才處理虛擬世界更新
15         var delta = self.game.time.deltaTime * 0.001;
16         JumpingBrick.gameWorld.updateLogic(delta);
17         self.syncWorld();
18     }
19 
20     // 幀率分析,若是當前能支持60幀則60幀調度
21     if (self._fpsCount > 50) {
22         var cost = self.game.debug.total / self._fpsCount;
23         self._fpsCount = 1;
24         self.game.debug.total = 0;
25         if (cost < 10) {
26             self.game.time.frameRate = 60;
27         }
28         else {
29             self.game.time.frameRate = 30;
30         }
31     }
32     else {
33         self._fpsCount++;
34     }
35 };
36 
37 
38 /**
39  * 同步世界數據
40  */
41 GameControl.prototype.syncWorld = function() {
42     var self = this,
43         world = JumpingBrick.gameWorld;
44 
45     // 同步方塊
46     self.brick.x = world.x;
47     self.brick.y = world.y - world.deadline;
48 
49     self.levelParent.y = -world.deadline;
50 };
View Code

 

組合腳本

  • 保存場景,刷新編輯頁面,讓編輯器能載入設置的插件。
  • 將GameConfig, GameWorld, GameControl, LevelAdapter都掛載到game節點上。並設置game節點可接受輸入事件。
  • 將TableView掛載到origin上
  • 爲brick,levels添加遊戲結束的表現效果。添加TweenPosition和TweenRotation組件。
    levels添加TweenPosition後,設置如圖:

brick添加TweenRotaion後,設置如圖:

brick添加TweenPosition後,設置如圖:

  • 設置關聯。

添加腳本並設置關聯的操做以下(未包含Tween添加和設置):

測試調整

至此,遊戲部分就已經基本完成了。由於代碼處理中兼容了其餘模塊不存在的狀況,因此如今已經能夠提供給其餘人進行測試、反饋,而後進行優化調整了。

 

下次將繼續講解如何進行遊戲當中的數據處理,敬請期待!

 

另外,今天是平安夜,祝你們聖誕快樂!


聖 誕 快 樂 .★ * ★.
.*★ *. *..*     ★
★ Marry X'mas     *
★    & ‧°∴°﹒☆°.
‘*.  Happy New Year *
  ‘★     ★
    ‘*..★'

 

其餘相關連接

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

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

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

相關文章
相關標籤/搜索