• Chrome自带恐龙小游戏的源码研究(完)


      在上一篇《Chrome自带恐龙小游戏的源码研究(七)》中研究了恐龙与障碍物的碰撞检测,这一篇主要研究组成游戏的其它要素。

    游戏分数记录

      如图所示,分数及最高分记录显示在游戏界面的右上角,每达到100分就会出现闪烁特效,游戏第一次gameover时显示历史最高分。分数记录器由DistanceMeter构造函数实现,以下是它的全部代码:

      1 DistanceMeter.dimensions = {
      2     WIDTH: 10,    //每个字符的宽度
      3     HEIGHT: 13,    //每个字符的高
      4     DEST_WIDTH: 11 //间隙
      5 };
      6 DistanceMeter.config = {
      7     // 初始时记录的分数上限为5位数,即99999
      8     MAX_DISTANCE_UNITS: 5,
      9 
     10     // 每隔100米距离记录器的数字出现闪动特效
     11     ACHIEVEMENT_DISTANCE: 100,
     12 
     13     // 将移动距离转化为合理的数值所用的转化系数
     14     COEFFICIENT: 0.025,
     15 
     16     // 每250ms闪动一次
     17     FLASH_DURATION: 1000 / 4,
     18 
     19     // 闪动次数
     20     FLASH_ITERATIONS: 3
     21 };
     22 /**
     23          * 距离记录器
     24          * @param {HTMLCanvasElement} canvas
     25          * @param {Object} spritePos 雪碧图上的坐标.
     26          * @param {number} canvasWidth
     27          * @constructor
     28          */
     29 function DistanceMeter(canvas, spritePos, canvasWidth) {
     30     this.canvas = canvas;
     31     this.canvasCtx = canvas.getContext('2d');
     32     this.image = imgSprite;
     33     this.spritePos = spritePos;
     34     //相对坐标
     35     this.x = 0;
     36     this.y = 5;
     37 
     38     //最大分数
     39     this.maxScore = 0;
     40     //高分榜
     41     this.highScore = 0;
     42 
     43     this.digits = [];
     44     //是否进行闪动特效
     45     this.acheivement = false;
     46     this.defaultString = '';
     47     //闪动特效计时器
     48     this.flashTimer = 0;
     49     //闪动计数器
     50     this.flashIterations = 0;
     51     this.invertTrigger = false;
     52 
     53     this.config = DistanceMeter.config;
     54     //最大记录为万位数
     55     this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
     56     this.init(canvasWidth);
     57 }
     58 
     59 DistanceMeter.prototype = {
     60     /**
     61              * 初始化距离记录器为00000
     62              * @param canvasWidth canvas的宽度
     63              */
     64     init: function(canvasWidth) {
     65         var maxDistanceStr = '';
     66 
     67         this.calcXPos(canvasWidth);
     68         for (var i = 0; i < this.maxScoreUnits; i++) {
     69             this.draw(i, 0);
     70             this.defaultString += '0';
     71             maxDistanceStr += '9';
     72         }
     73 
     74         //99999
     75         this.maxScore = parseInt(maxDistanceStr);
     76     },
     77     /**
     78              * 计算出xPos
     79              * @param canvasWidth
     80              */
     81     calcXPos: function(canvasWidth) {
     82         this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH * (this.maxScoreUnits + 1));
     83     },
     84     draw: function(digitPos, value, opt_highScore) {
     85         var sourceWidth = DistanceMeter.dimensions.WIDTH;
     86         var sourceHeight = DistanceMeter.dimensions.HEIGHT;
     87         var sourceX = DistanceMeter.dimensions.WIDTH * value;
     88         var sourceY = 0;
     89 
     90         var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
     91         var targetY = this.y;
     92         var targetWidth = DistanceMeter.dimensions.WIDTH;
     93         var targetHeight = DistanceMeter.dimensions.HEIGHT;
     94 
     95         sourceX += this.spritePos.x;
     96         sourceY += this.spritePos.y;
     97 
     98         this.canvasCtx.save();
     99 
    100         if (opt_highScore) {
    101             // 将最高分放至当前分数的左边
    102             var highScoreX = this.x - (this.maxScoreUnits * 2) * DistanceMeter.dimensions.WIDTH;
    103             this.canvasCtx.translate(highScoreX, this.y);
    104         } else {
    105             this.canvasCtx.translate(this.x, this.y);
    106         }
    107 
    108         this.canvasCtx.drawImage(this.image, sourceX, sourceY, sourceWidth, sourceHeight, targetX, targetY, targetWidth, targetHeight);
    109 
    110         this.canvasCtx.restore();
    111     },
    112     /**
    113              * 将像素距离转化为“真实距离”
    114              * @param distance  像素距离
    115              * @returns {number} “真实距离”
    116              */
    117     getActualDistance: function(distance) {
    118         return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
    119     },
    120     /**
    121              * 更新距离记录器
    122              * @param {number} deltaTime
    123              * @param {number} distance
    124              * @returns {boolean} 是否播放声音
    125              */
    126     update: function(deltaTime, distance) {
    127         var paint = true;
    128         var playSound = false;
    129 
    130         if (!this.acheivement) {
    131             distance = this.getActualDistance(distance);
    132             // 分数超过最大分数时增加至十万位999999
    133             if (distance > this.maxScore && this.maxScoreUnits === this.config.MAX_DISTANCE_UNITS) {
    134                 this.maxScoreUnits++;
    135                 this.maxScore = parseInt(this.maxScore + '9');
    136             }
    137 
    138             if (distance > 0) {
    139                 // 每100距离开始闪动特效并播放声音
    140                 if (distance % this.config.ACHIEVEMENT_DISTANCE === 0) {
    141                     this.acheivement = true;
    142                     this.flashTimer = 0;
    143                     playSound = true;
    144                 }
    145 
    146                 // 让数字以0开头
    147                 var distanceStr = (this.defaultString + distance).substr( - this.maxScoreUnits);
    148                 this.digits = distanceStr.split('');
    149             } else {
    150                 this.digits = this.defaultString.split('');
    151             }
    152         } else {
    153             // 到达目标分数时闪动分数
    154             if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
    155                 this.flashTimer += deltaTime;
    156 
    157                 if (this.flashTimer < this.config.FLASH_DURATION) {
    158                     paint = false;
    159                 } else if (this.flashTimer > this.config.FLASH_DURATION * 2) {
    160                     this.flashTimer = 0;
    161                     this.flashIterations++;
    162                 }
    163             } else {
    164                 this.acheivement = false;
    165                 this.flashIterations = 0;
    166                 this.flashTimer = 0;
    167             }
    168         }
    169 
    170         // 非闪动时绘制分数
    171         if (paint) {
    172             for (var i = this.digits.length - 1; i >= 0; i--) {
    173                 this.draw(i, parseInt(this.digits[i]));
    174             }
    175         }
    176 
    177         this.drawHighScore();
    178         return playSound;
    179     },
    180     //绘制高分榜
    181     drawHighScore: function() {
    182         this.canvasCtx.save();
    183         this.canvasCtx.globalAlpha = .8; //让字符看起来颜色稍浅
    184         for (var i = this.highScore.length - 1; i >= 0; i--) {
    185             this.draw(i, parseInt(this.highScore[i], 10), true);
    186         }
    187         this.canvasCtx.restore();
    188     },
    189     setHighScore: function(distance) {
    190         distance = this.getActualDistance(distance);
    191         var highScoreStr = (this.defaultString + distance).substr( - this.maxScoreUnits);
    192         //10和11分别对应雪碧图中的H、I
    193         this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
    194     },
    195     //重置记录器为00000
    196     reset: function() {
    197         this.update(0);
    198         this.acheivement = false;
    199     }
    200 };
    View Code

    GameOver

      恐龙和障碍物碰撞后,游戏结束,游戏界面显示gameover面板,该功能由GameOverPanel构造函数实现:

     1 GameOverPanel.dimensions = {
     2     TEXT_X: 0,
     3     TEXT_Y: 13,
     4     TEXT_WIDTH: 191,
     5     TEXT_HEIGHT: 11,
     6     RESTART_WIDTH: 36,
     7     RESTART_HEIGHT: 32
     8 };
     9 
    10 function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
    11     this.canvas = canvas;
    12     this.canvasCtx = canvas.getContext('2d');
    13     this.canvasDimensions = dimensions;
    14     this.textImgPos = textImgPos;
    15     this.restartImgPos = restartImgPos;
    16     this.draw();
    17 }
    18 
    19 GameOverPanel.prototype = {
    20     draw: function() {
    21         var dimensions = GameOverPanel.dimensions;
    22 
    23         var centerX = this.canvasDimensions.WIDTH / 2;
    24 
    25         // Game over text
    26         var textSourceX = dimensions.TEXT_X;
    27         var textSourceY = dimensions.TEXT_Y;
    28         var textSourceWidth = dimensions.TEXT_WIDTH;
    29         var textSourceHeight = dimensions.TEXT_HEIGHT;
    30 
    31         var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
    32         var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
    33         var textTargetWidth = dimensions.TEXT_WIDTH;
    34         var textTargetHeight = dimensions.TEXT_HEIGHT;
    35 
    36         var restartSourceWidth = dimensions.RESTART_WIDTH;
    37         var restartSourceHeight = dimensions.RESTART_HEIGHT;
    38         var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
    39         var restartTargetY = this.canvasDimensions.HEIGHT / 2;
    40 
    41         textSourceX += this.textImgPos.x;
    42         textSourceY += this.textImgPos.y;
    43 
    44         // Game over text from sprite.
    45         this.canvasCtx.drawImage(imgSprite, textSourceX, textSourceY, textSourceWidth, textSourceHeight, textTargetX, textTargetY, textTargetWidth, textTargetHeight);
    46 
    47         // Restart button.
    48         this.canvasCtx.drawImage(imgSprite, this.restartImgPos.x, this.restartImgPos.y, restartSourceWidth, restartSourceHeight, restartTargetX, restartTargetY, dimensions.RESTART_WIDTH, dimensions.RESTART_HEIGHT);
    49     }
    50 };
    View Code
     1 function gameOver() {
     2     cancelAnimationFrame(raq);
     3     raq = 0;
     4     crashed = true;
     5     trex.update(0, Trex.status.CRASHED);
     6 
     7     distanceMeter.acheivement = false;
     8     if (distanceRan > highestScore) {
     9         highestScore = Math.ceil(distanceRan);
    10         distanceMeter.setHighScore(highestScore);
    11     }
    12 
    13     if (!gameOverPanel) {
    14         gameOverPanel = new GameOverPanel(c, spriteDefinition.TEXT_SPRITE, spriteDefinition.RESTART, dimensions);
    15     } else {
    16         gameOverPanel.draw();
    17     }
    18 }
    View Code

    游戏重新开始

      GameOver后,按下Spacebar游戏重新开始,restart方法负责将游戏各个元素或数据重置:

     1 function restart() {
     2     trex.reset();
     3     Obstacle.obstacles = [];
     4     h.reset();
     5     night.reset();
     6     crashed = false;
     7     time = performance.now();
     8     distanceRan = 0;
     9     ctx.clearRect(0, 0, 600, 150);
    10     distanceMeter.reset();
    11     raq = requestAnimationFrame(draw, c);
    12 }
    View Code

    游戏暂停

      当游戏窗口失去焦点时,游戏暂停,得到焦点时游戏继续。游戏通过注册三个事件来实现:

    document.addEventListener('visibilitychange',onVisibilityChange);
    window.addEventListener('blur',onVisibilityChange);
    window.addEventListener('focus',onVisibilityChange);
     1 onVisibilityChange: function(e) {
     2     if (document.hidden || document.webkitHidden || e.type == 'blur' || document.visibilityState != 'visible') {
     3         this.stop();
     4     } else if (!this.crashed) {
     5         this.tRex.reset();
     6         this.play();
     7     }
     8 },
     9 stop: function() {
    10     this.activated = false;
    11     this.paused = true;
    12     cancelAnimationFrame(this.raqId);
    13     this.raqId = 0;
    14 },
    15 play: function() {
    16     if (!this.crashed) {
    17         this.activated = true;
    18         this.paused = false;
    19         this.tRex.update(0, Trex.status.RUNNING);
    20         this.time = getTimeStamp();
    21         this.update();
    22     }
    23 }
    View Code

    开场动画

      第一次开始游戏时,会有一个过渡动画,效果是地面逐渐展开,并且恐龙向前移动50像素。

     1 // CSS animation definition.
     2 var keyframes = '@-webkit-keyframes intro { ' + 'from { ' + Trex.config.WIDTH + 'px }' + 'to {  ' + this.dimensions.WIDTH + 'px }' + '}';
     3 document.styleSheets[0].insertRule(keyframes, 0);
     4 
     5 this.containerEl.addEventListener('webkitAnimationEnd', this.startGame.bind(this));
     6 
     7 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
     8 this.containerEl.style.width = this.dimensions.WIDTH + 'px';
     9 
    10 
    11 //向前移动50像素
    12 if (this.playingIntro && this.xPos < this.config.START_X_POS) {
    13     this.xPos += Math.round((this.config.START_X_POS / this.config.INTRO_DURATION) * deltaTime);
    14 }
    View Code

    游戏音效

      游戏准备了三种音效,分别是游戏点击空格键开始时、与障碍物碰撞时、每到达100分时。游戏在代码中放置了三个audio标签来存放音效,并且是base64形式,所以在播放时要经过解码,可以查阅文档了解AudioContext API的用法:

     1 function decodeBase64ToArrayBuffer(base64String) {
     2     var len = (base64String.length / 4) * 3;
     3     var str = atob(base64String);
     4     var arrayBuffer = new ArrayBuffer(len);
     5     var bytes = new Uint8Array(arrayBuffer);
     6 
     7     for (var i = 0; i < len; i++) {
     8         bytes[i] = str.charCodeAt(i);
     9     }
    10     return bytes.buffer;
    11 }
    View Code
     1 var data = '........base64String.......';
     2 var soundFx = {};
     3 var soundSrc = data.substr(data.indexOf(',')+1);
     4 var buffer = decodeBase64ToArrayBuffer(soundSrc);
     5 var audioContext = new AudioContext();
     6 audioContext.decodeAudioData(buffer,function(index,audioData) {
     7     soundFx[index] = audioData;
     8 }.bind(this,'audio1'));
     9 
    10 function playSound(soundBuffer) {
    11     if (soundBuffer) {
    12         var sourceNode = audioContext.createBufferSource();
    13         sourceNode.buffer = soundBuffer;
    14         sourceNode.connect(audioContext.destination);
    15         sourceNode.start(0);
    16     }
    17 }
    18 
    19 window.onload = function() {
    20     playSound(soundFx['audio1']);
    21 };
    View Code

    对移动设备的处理  

      游戏还专门对移动设备进行了处理,包括屏幕大小的自适应,游戏速度调节,为高清屏加载高清素材等等。具体代码就不一一列出了。

      至此,对这个小游戏的代码研究结束,下面是完整的游戏:

     

    总结

      通过对这个游戏的源码进行研究,从中收获了不少干货,对2d游戏的制作思路有一定的启发,特别是基于时间的运动有了进一步的认识。游戏大致可以划分为以下功能:

    大部分构造函数里都包含了一个名为update的方法,在每次GameLoop里调用以更新该游戏元件的状态,并根据条件判断是否在画布上绘制(draw)。

    暂时就想到这么多,接下来就是以开发一个2D游戏为目标努力了。

  • 相关阅读:
    Firefox扩展IE Tab Plus内置功能导致浏览所有网页加载superfish.com脚本
    iconv编码转换
    Firefox扩展IE Tab Plus内置功能导致浏览所有网页加载superfish.com脚本
    mysql导入邮件
    Rails gem 打包css javascript 提升网站性能 jammit 简介
    装箱/拆箱测试一例(转)
    nifity scaffold gem
    软硬链接
    软硬链接
    git服务搭建
  • 原文地址:https://www.cnblogs.com/undefined000/p/trex_8.html
Copyright © 2020-2023  润新知