青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 2

继上一次介绍了《神奇的六边形》的完整游戏开发流程后可点击这里查看,这次将为大家介绍另外一款魔性游戏《跳跃的方块》的完整开发流程。

青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 2_第1张图片

          (点击图片可进入游戏体验)

因内容太多,为方便大家阅读,所以分多次来讲解。

若要一次性查看所有文档,也可点击这里

 

接上回(《跳跃的方块》Part 1

三. 游戏世界

为了能更快的体验到游戏的主体玩法,调整游戏数值,这里我们先来搭建游戏世界。

建立基础世界

在《跳跃的方块》中,下一关的信息尤为关键。如果能提前获知阻挡点或者通道位置,会为当前的操作提供一定的指导。为了保证所有玩家获取的信息基本一致,屏幕中显示的关卡数量需要严格的控制。

所以这里我们将屏幕的高度通过UIRoot映射为一个固定值:960,添加一个锁定屏幕旋转方向的脚本,并创建游戏的根节点game,设置game节点铺满屏幕。
操作如下所示:

分步构建世界

  • 游戏配置
  • 构建世界逻辑
  • 控制展示游戏世界

 

(一)游戏配置

设置可调整参数

这个游戏中,一些参数会严重影响用户体验,需要进行不停的尝试,以找到最合适的设置。所以,这里将这些参数提取出来,群策群力,快速迭代出最终版本。
分析游戏内容后,将游戏数据分为两类:

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轴原点位于屏幕中间。如下图所示:

青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 2_第2张图片

基础设定

  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. 当方块的包围矩形和矩形相交时。如下图分为两种情况处理。

青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 2_第3张图片

代码实现如下:

  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

 

你可能感兴趣的:(青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 2)