继上一次介绍了《神奇的六边形》的完整游戏开发流程后(可点击这里查看),这次将为大家介绍另外一款魔性游戏《跳跃的方块》的完整开发流程。
(点击图片可进入游戏体验)
因内容太多,为方便大家阅读,所以分多次来讲解。
若要一次性查看所有文档,也可点击这里。
接上回(《跳跃的方块》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。那么当四个边可以根据公式表达分别为:
就可以通过如下设置达到希望的效果:
在前面章节中创建的game节点,是一个铺满屏幕的节点,可以理解为对应屏幕,且Pivot为(0,0),那么坐标系为从左上到右下的一个坐标系,这个坐标系和虚拟世界的不同,需要转换下。 在game节点下创建一个节点origin,把origin作为显示虚拟世界的原点。
在虚拟世界的创建过程中,分析了关卡的特性,在显示时只需要显示屏幕中的关卡,甚至连创建也不需要,并且关卡是一个连着一个,有点类似于单列表格的形式。于是这里选择使用官方插件中的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 };
创建脚本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 };
创建一个预制level,level下有三个节点:leftLevel(UIImage),rightLevel(UIImage),block(Node)。 并为其添加上一步创建的脚本LevelShow。
创建脚本,并预设功能相关的节点,监听相关事件。具体实现如下:
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 };
游戏运行时,分为开始引导、游戏运行、游戏暂停、游戏结束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 };
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 };
对暂停、恢复进行处理。
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 };
让游戏支持输入。
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 };
需要处理游戏世界反馈回来的分数变更和游戏结束事件。
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 };
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 };
brick添加TweenRotaion后,设置如图:
brick添加TweenPosition后,设置如图:
添加脚本并设置关联的操作如下(未包含Tween添加和设置):
至此,游戏部分就已经基本完成了。因为代码处理中兼容了其他模块不存在的情况,所以现在已经可以提供给其他人进行测试、反馈,然后进行优化调整了。
下次将继续讲解如何进行游戏当中的数据处理,敬请期待!
另外,今天是平安夜,祝大家圣诞快乐!
★
圣 诞 快 乐 .★ * ★.
.*★ *. *..* ★
★ Marry X'mas *
★ & ‧°∴°﹒☆°.
‘*. Happy New Year *
‘★ ★
‘*..★'
其他相关链接
开源免费的HTML5游戏引擎——青瓷引擎(QICI Engine) 1.0正式版发布了!
JS开发HTML5游戏《神奇的六边形》(一)
青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 1