javascript
青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 3
繼上一次介紹了《神奇的六邊形》的完整游戲開發流程后(可點擊這里查看),這次將為大家介紹另外一款魔性游戲《跳躍的方塊》的完整開發流程。
? ? ? ? ??(點擊圖片可進入游戲體驗)
因內容太多,為方便大家閱讀,所以分多次來講解。
若要一次性查看所有文檔,也可點擊這里。
?
接上回(《跳躍的方塊》Part 2)
?
(三)控制展示游戲世界
?
經過前面的準備,虛擬游戲世界已經構建完成,開始著手將虛擬世界呈現出來。
添加圖形資源
導入所有的圖形資源到Assets/atlas/main@atlas下,并重新打包圖集。
創建功能按鈕和界面
在game下添加引導界面、暫停按鈕(UIImage)、分數顯示(UIText)、暫停界面(Node)、暫停界面下的恢復按鈕(UIImage)和一個半透明層(UIImage)
-
暫停按鈕
暫停按鈕的范圍為相對于父節點左上角(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作為顯示虛擬世界的原點。
映射規則
- 原點所在的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
轉載于:https://www.cnblogs.com/qici/p/5072206.html
總結
以上是生活随笔為你收集整理的青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 3的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MHA监控进程异常退出(MHA版本:0.
- 下一篇: Python使用中文注释和输出中文(原创