zoukankan      html  css  js  c++  java
  • 青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 2

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

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

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

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

    接上回(《跳跃的方块》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轴原点位于屏幕中间。如下图所示:

    基础设定

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

    代码实现如下:

      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

     
  • 相关阅读:
    python--进程
    python---多线程
    python--上下文管理器
    python中的单例模式
    装饰器
    匿名函数
    python的内置方法
    命名元组
    如何管理我们的项目环境
    启动APP遇到“UiAutomator exited unexpectedly with code 0, signal null”解决
  • 原文地址:https://www.cnblogs.com/qici/p/5069304.html
Copyright © 2011-2022 走看看