青瓷引擎之纯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作为显示虚拟世界的原点。

  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

转载于:https://www.cnblogs.com/qici/p/5072206.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值