chrome离线小恐龙改造版

谷歌小恐龙之对打版(一)—— 准备

改造离线小恐龙github源码地址

设计

在这里插入图片描述

制作一个仿照Google小恐龙的小游戏,对那种对战版的,同时为了增加一些趣味性,加了一点恶趣味的东西自己去发现吧

资源准备工作

准备一个canvas元素

<div id="game">
    <canvas id="canvas" width="800" height="250"></canvas>
</div>

再写一些简单的样式,居中、背景色啥的,不多解释,看起来好看就OK;

body {
    position: relative;
}
canvas {
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -0%);
    background-color: #eee;
}

先制作一些游戏所需要的素材资源,这里只用到了图片资源,一张Google小恐龙的原味精灵图,其他的都是艺术字制作网站做的png格式的图片,用来做游戏Logo、提示文字、按钮啥的
准备工作就这么多了,如果你对canvas不是很了解,那就往下看了解了解就ok了;

canvas基本知识

canvas的基础知识请看廖雪峰老师的这篇关于博客,这里就不重复了,毕竟是很简单的东西,但是还有一个非常重要的知识要在这里补充,大家在了解完canvas的基础后,都知道,canvas有一个坐标系统,利用坐标系统我们可以绘制一些形状,但是不仅仅可以绘制简单形状,还可以绘制图片,如下:

// 获取canvas对象
var canvas = document.getElementById('canvas');
// 获取2d的画笔
var ctx = canvas.getContext('2d');
// 绘制图形
ctx.drawImage(
    SourceImage, //第一个参数:图片对象
    sx, sy, //第2、3个参数:矩形区域的顶点坐标
    sw, sh, //第4、5个参数:矩形区域的宽高
    dx, dy, //第6、7个参数:矩形区域位于画布的顶点坐标
    dw, dh //第8、9个参数:矩形区域位于画布中的宽高
);

下图的参数和上面代码对应,最终我们就把SourceImage中的指定绿色区域按照我们设置的宽高绘制在Canvas上了,因此,我们可以将图片扩大或者缩放,但是这样做会使绘制出来的图像有一定程度的模糊;我们就利用这一点,将精灵图中的一个个元素绘制在画布上,加上一些动作,就变成了游戏;

画布小游戏基本知识

按照上面的代码,我们只是将禁止的图片绘制在画布上,如何将绘制的图形动起来了,ok,那就是定时器了,先定义一个定时器

var timer = setInterval(function () {
  // To do Something
}, 30);

这里做一个小球跟随鼠标滚动的动画,思路就是,在定时器没执行一次,我们就获取一次鼠标的位置,同时把小球的位置更新到鼠标的位置

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var ball = {
    x: 0, // x坐标
    y: 0, // y坐标
    r: 10, // 圆半径
    draw: function () {
        // 在更新小球的位置之前,将上一帧绘制的画布清除
        ctx.clearRect(0, 0, 500, 300);
        // 开始一个画笔
        ctx.beginPath();
        // 绘制一个圆
        ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
        ctx.stroke();
    }
}
// 监听鼠标在canvas上的移动事件,并把鼠标的坐标赋给小球
canvas.onmousemove = function (e) {
    ball.x = e.offsetX;
    ball.y = e.offsetY;
}
// 在定时器中执行
var timer = setInterval(function () {
    ball.draw();
}, 30);

简单吧,这样,一个简单的带有交互的游戏就OK了,总结一下,先创建一个画布,然后将一个物体绘制在画布上,随着定时器的执行,我们就将上一次绘制的东西清除,然后重新绘制,这样一帧一帧的图像连起来就成了动画,然后加上用户交互,加上规则就成了小游戏,以后我们将开始逐渐的扩展我们上边所完成的这段简短的代码,使其更像一个小游戏;

谷歌小恐龙之对打版(二)—— 起步

上篇中,我用定时器来执行绘制任务,现在我们换一个性能更高的’定时器’——requestAnimationFrame,大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。

大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms,而setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。

如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行,,requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果;

像setTimeout、setInterval一样,requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据自己的频率进行一次重绘,它接收一个回调函数作为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间作为参数。由于requestAnimationFrame的功效只是一次性的,所以若想达到动画效果,则必须连续不断的调用requestAnimationFrame,就像我们使用setTimeout来实现动画所做的那样。

来看看基本用法

// 这样就完成了和定时器一样的功能,实现了多次调用
function rander(){
    // To do Something
    // return stop the requestAnimationFrame
    requestAnimationFrame(rander);
}
requestAnimationFrame(function () {
    rander();
});

我们做一下简单的封装来解决一下浏览器的兼容性

window.requestAnimFrame = (function () {
    return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    /**
     * 降级处理,用setTimeout实现
     * 大约16ms 两帧间隔时间
     */
    function (callback, element) {
        return window.setTimeout(callback, 1000 / 60);
    };
})();

ok,定时器的优化封装做好了,然后我们写一些常用的工具函数,这些方法也可以不急着定义,等到开发过程中根据需求来定义

// 指定范围的随机整数
function randomNumBoth(min, max) {
    var range = max - min;
    var rand = Math.random();
    var num = min + Math.round(rand * range); //四舍五入
    return num;
}

// 取绝对值
function absolute(x) {
    return x >= 0 ? x : -x;
}

// 获取两点之间的距离
function getS(pos1, pos2) {
    return Math.sqrt(Math.pow(pos1.x - pos2.x, 2) +  Math.pow(pos1.y - pos2.y, 2));
}

// 判断是否为偶数
function isOdd(num) {
    if ((num % 2) == 1) {
        return false;
    }
    return true;
}

我们先缓存我们用到的素材资源

// 游戏的画布
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

// 以下为加载游戏使用到的图片
var gameImage = new Image();
gameImage.src = './lib/game.png';

var logo = new Image();
logo.src = './lib/logo.png';

var over = new Image();
over.src = './lib/over.png';

var kaishi = new Image();
kaishi.src = './lib/start.png';

var win = new Image();
win.src = './lib/win.png';

var suc = new Image();
suc.src = './lib/success.png';

我们还得配置一些全局的配置,比如游戏背景的宽度,高度之类的

var gameConfig = {
  bg: {
    WIDTH: 800,
    HEIGHT: 250
  }
}

用我们之前封装的requestAnimFrame方法来绘制我们的画布,这样我们就制作了一个游戏的核心控制器,每执行一次都更新一帧,我们之后的代码也只需要关心当前帧和下一帧就ok了

(function draw() {
    // 在下一帧绘制之前我们先清除上一帧的画布
    ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);
    // 跑一下,就可以看到大约每过60ms就会打印一个'下一帧'
    console.log('下一帧');
    window.requestAnimFrame(draw);
})();

到这里,游戏的起步工作就差不多了,下篇开始我们就要绘制一些对象在画布上了,好激动啊,终于要画东西了。

谷歌小恐龙之对打版(三)—— 地板、路障

绘制地板,在Google小恐龙的游戏中,小恐龙是前行的,用固定小恐龙来使地板后退的方式,使小恐龙在视觉上前进,但是我们这个场景是固定的,小恐龙的实际运动就是自己在动,所以地板就是一个固定的背景,所以也是整个代码中最简单的一块。

二话不说,先定义一个Floor类

/**
 * 地板
 * canvas 画布
 */
function Floor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    // 这个方法用来初始化一些参数,在其原型上定义,往下看就明白了
    this.init();
}

然后我们要绘制地板,需要一些地板的参数,比如他的长宽之类的

// 配置是我用PS一个像素一个像素量出来的
Floor.config = {
    WIDTH: 800,     // 宽600像素
    HEIGHT: 12,     // 高12像素
    YPOS: 227,      // 在Y轴的位置
    IMG: {y: 54},   // 地面在画布上的位置
    RANDOMPOS: {min: 2, max: 402} // 地板随机位置
}

我们现在定义Floor的方法,貌似没有啥方法,一个地板能有啥方法

Floor.prototype = {
    // 在canvas上绘制地板
    draw: function () {
        /**
         * 这里就是canvas的绘制图像的基本用法
         * 将精灵图中的地板取出来,然后画在画布上
         */ 
        this.ctx.drawImage(gameImage,
            Floor.config.IMG.x, Floor.config.IMG.y, Floor.config.WIDTH, Floor.config.HEIGHT,
            0, Floor.config.YPOS, Floor.config.WIDTH, Floor.config.HEIGHT
        )
    },
    // 初始化一些参数
    init: function () {
        /**
         * 因为每次加载都想要不同的地板
         * 所以,就把地板的位置随机一下,就从精灵图中截取出不同位置的地板
         * 给人的感觉就是不一样的地板了
         */ 
        Floor.config.IMG.x = randomNumBoth(Floor.config.RANDOMPOS.min, Floor.config.RANDOMPOS.max);
        this.draw();
    }
}

Floor我们就定义完了,然后要做的就是创建实例,调用其draw方法就可以把地板绘制在画布上了,因为我们每帧都会会清除整个画布,所以,我们必须每帧都绘制地板,否则当第二帧开始,地板就被清除了,所以这样

// 实例化一个floor对象,执行构造函数的时候就会调用floor.init方法,将地板绘制在画布上作为第一帧
var floor = new Floor(canvas);
(function draw() {
    // 在下一帧绘制之前我们先清除上一帧的画布
    ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);
    // 跑一下,就可以看到大约每过60ms就会打印一个'下一帧'
    // 这里调用draw方法,每次清除画布之后就会在画一个新的地板上去
    floor.draw();
    console.log('下一帧');
    window.requestAnimFrame(draw);
})();

当然,你也可以另外创建一个canvas,将一些不动的背景画在上面,这样性能会有所提升;
地板就画好了,还有一个就是固定不变的路障,这个路障虽然也是固定的,但是也和上边的地板一样,我们想随机的绘制不同的路障,使得我们的游戏更加丰富,上代码,解释都在代码里

/**
 * 分割线的仙人掌 
 * canvas 画布
 */
function Cacti(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.init();
}

Cacti.config = {
    SPLIT_POS: gameConfig.bg.WIDTH,            // 路障的位置,总宽的一半
    TYPES_RATIO: 0.5                           // 这个是出现不同路障的比例 
}
/***
 * 路障的配置,这里是个数组
 * 因为我们想随机从这两个路障中挑选一个出来
 * 作为游戏中间的路障
 */
Cacti.types = [
    {
        WIDTH: 50,             // 宽12
        HEIGHT: 35,            // 高35像素
        XPOS: 400,             // 在x轴的位置
        YPOS: 205,             // 在Y轴的位置
        IMG: {x: 228, y: 3},   // 在图上的位置
    },
    {
        WIDTH: 50,             // 宽12
        HEIGHT: 50,            // 高35像素
        XPOS: 400,             // 在x轴的位置
        YPOS: 190,             // 在Y轴的位置
        IMG: {x: 430, y: 3},   // 地面在图上的位置
    }
]

Cacti.prototype = {
    // 在canvas上绘制仙人掌
    draw: function() {
        this.ctx.drawImage(gameImage,
            Cacti.config.CACTI_TYPE.IMG.x, Cacti.config.CACTI_TYPE.IMG.y,
            Cacti.config.CACTI_TYPE.WIDTH, Cacti.config.CACTI_TYPE.HEIGHT,
            Cacti.config.CACTI_TYPE.XPOS - (Cacti.config.CACTI_TYPE.WIDTH / 2), Cacti.config.CACTI_TYPE.YPOS,
            Cacti.config.CACTI_TYPE.WIDTH, Cacti.config.CACTI_TYPE.HEIGHT
        )
    },
    // 获得随机类型的仙人掌
    init: function() {
        /**
         * 随机获取一个路障的类型,并作为路障的静态属性
         * 绘制的时候就从Cacti.config.CACTI_TYPE中获取参数
         */ 
        Cacti.config.CACTI_TYPE = Math.random() > Cacti.config.TYPES_RATIO ? Cacti.types[0] : Cacti.types[1];
        this.draw();
    }
}

最后实例化这个路障,在动画帧中绘制我们的路障,因为实例化的时候会调用init方法,同时获取一个路障的类型,所以在帧动画中不断的调用draw方法是不会随即改变路障类型的

var floor = new Floor(canvas);
// 实例化的时候已经确定的路障类型
var cacti = new Cacti(canvas);
(function draw() {
    // 在下一帧绘制之前我们先清除上一帧的画布
    ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);
    // 跑一下,就可以看到大约每过60ms就会打印一个'下一帧'
    // 这里调用draw方法,每次清除画布之后就会在画一个新的地板上去
    floor.draw();
    // 绘制路障, 这里重复绘制也不会改变路障的类型了
    cacti.draw();
    console.log('下一帧');
    window.requestAnimFrame(draw);
})();

ok,我们已经完成了两个静态的物体的绘制,下篇,我们画一个动态的东西,云朵,原理着静态的物体一样,只不过每次调用draw方法之前都会执行一些操作,使的云的位置不断的改变;

谷歌小恐龙之对打版(四)—— 云朵

为了丰富我们的游戏内容,我们要绘制一些背景使其不单调,上片我们画了一些地板和路障,这次,我们绘制一些在天空中飘动的云朵,云朵的数量和位置都在指定的范围内随机生成,来增加自然度,又特么随机

原理带大部分的代码逻辑和先前的一样,先创建云朵类并配置一些云朵的基本参数

/**
 * 云朵
 * canvas 画布
 */
function Cloud(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.init();
}

// 云朵的配置
Cloud.config = {
    WIDTH: 46,           // 云朵的宽度
    HEIGHT: 14,          // 云朵的高度
    IMG: {x: 88, y: 3},  // 云朵在图中的位置
    MAX_BASE_HEIGHT: 30, // 云朵离地面的最大高度
    MIN_BASE_HEIGHT: 71, // 云朵离地面的最小高度
    MAX_GAP: 300,        // 云朵之间的最大间隔
    MIN_GAP: 100,        // 云朵之间的最小间隔
    RANDOMSPEED: {min: 1, max: 2}, // 云朵飘动的随机速度范围
}

但是和之前不同的是,地面和路障都只有一个,我们的云朵却不止一朵,所以我们创建一个云朵类的静态属性来存储实例化好的云朵

Cloud.clouds = [];

ok,就这么简单,接下来就要定义方法了,但是和先前不一样的是,我希望当我实例化云朵的时候,得到的漫天的所有云朵实例的集合,而不是一朵,我并不想在动画帧的控制器中做太多业务,来使代码看起来很丑,所以我们要一个creat方法来创建单个云朵的实例,并存储在Cloud.clouds中,当画面更新调用Cloud实例的draw方法时,遍历这个由单个云朵组成的集合,然后再调用相应的方法来绘制云朵,上代码

Cloud.prototype = {
    // 在canvas上绘制云朵
    draw: function() {
        this.ctx.drawImage(gameImage,
            Cloud.config.IMG.x, Cloud.config.IMG.y,
            Cloud.config.WIDTH, Cloud.config.HEIGHT,
            this.xPos, this.yPos,
            Cloud.config.WIDTH, Cloud.config.HEIGHT
        )
    },
    // 实例化云朵时的初始参数
    init: function() {
        // 初始化的时候云朵的位置
        this.xPos = gameConfig.bg.WIDTH + Cloud.config.WIDTH;
        this.yPos = randomNumBoth(Cloud.config.MAX_BASE_HEIGHT, Cloud.config.MIN_BASE_HEIGHT);
        // 随机生成一个云朵和云朵之间的间隔宽度
        this.cloudGap = randomNumBoth(Cloud.config.MIN_GAP,Cloud.config.MAX_GAP)
        // 随机生成云朵的速度
        this.spead = randomNumBoth(Cloud.config.RANDOMSPEED.min, Cloud.config.RANDOMSPEED.max);
    },
    // 更新canvas上云朵的位置
    update: function() {
        // 当没有云朵的时候,绘制一个云朵
        if(Cloud.clouds.length == 0) {
            return this.creat();
        }
        // 当云朵移出屏幕外的时候,删掉这个云朵
        if(Cloud.clouds[0].xPos < -Cloud.config.WIDTH) {
            Cloud.clouds.shift();
        }
        var len = Cloud.clouds.length;
        /**
         * 当最后一个云朵大于其云朵间距的时候,生成一个云朵
         * 没有云朵在inti的时候都会初始化一个云朵间距
         * 当这个云朵距离右边界的位置的时候,就生成一个新的云朵
         * 这个间距是在一个范围内随机生成的,所以看起来就自然一些
         */ 
        if(gameConfig.bg.WIDTH - Cloud.clouds[len-1].xPos > Cloud.clouds[len-1].cloudGap) {
            this.creat();
        }
        // 循环这些云朵数组,绘制他们的自己
        for(var i = 0; i < len; i ++) {
            // 这里使每个云朵都在下一帧移动一定的距离
            Cloud.clouds[i].xPos = Cloud.clouds[i].xPos - Cloud.clouds[i].spead;
            // 调用云朵的绘制方法,画在画布上
            Cloud.clouds[i].draw();
        }
    },
    // 存储云朵的数组中添加一个云朵
    creat: function() {
        Cloud.clouds.push(new Cloud(this.canvas));
    }
}

这样我们就完成了云朵的绘制,老样子,在动画帧中绘制这些云朵,但是我们这次调用的是update方法,因为在update中我们调用了云朵集合中每个云朵的draw方法,本来命名是和先前一样的,后来为了大伙区分不同,就改叫update,之后静态的物体我们就用draw,动态的我们就用update

var floor = new Floor(canvas);
var cacti = new Cacti(canvas);

// 创建漫天云朵
var cloud = new Cloud(canvas);
(function draw() {
    // 在下一帧绘制之前我们先清除上一帧的画布
    ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);
    floor.draw();
    cacti.draw();

    // 调用云朵的update方法
    cloud.update();
    console.log('下一帧');
    window.requestAnimFrame(draw);
})();

ok,云朵的绘制就成功了,跑一下代码,就会发现还不错,总结一下,动态的物体和静态物体的区别就是,动态的物体在每帧动画执行的时候,根据一些规则,更新一下自己的位置,在下一帧的时候,位置就会发生变动;下篇,我们来做一些有意思的东西;

谷歌小恐龙之对打版(五)—— AABB盒模型

因为游戏实在太简单了,没有必要用到牛闪闪的物理引擎,只做一个简单的碰撞检测就OK了,算法不过关,可能会导致计算量比较大,但是我电脑配置高啊,以及后续的起跳和下落也一样,做一些简单的实现就ok了
先了解一下盒模型的概念,如下图所示,AABB盒的概念总结一下,就是用一个正方形来表示物体的边界,与其他两个不同的是AABB盒的正方形始终都是正的,这是这种模型的检测碰撞时计算量不是很大,但是范围不是很准确
当然有很多游戏都采用这种模型,我们小时候玩的红白机,大部分都采用这种碰撞检测,但是,他们会用很多AABB盒来提高边界的检测精度,先计算外圈红色盒的碰撞,当另一物体与其发生碰撞后再去检测内部绿色的碰撞,这样的方法减少计算量。
我这次只用了简单的两个碰撞盒来描述两个物体,看着下图思考一下,两个盒子如何碰撞

// X1 : AAX, X2 : AAX + AA.width, 以此类推
// 不过网上找的这个图和我们即将写的不太一样,坐标原点在左上角
AAX < BBX + BB.width && AAX + AA.width > BBX && AAY < BBY + BB.height && AA.height + AAY > BBY

大致了解之后就能开始写代码了

// AABB盒模型
function AABB(x, y, w, h, type) {
    // 盒的x坐标
    this.x = x;
    // 盒的y坐标
    this.y = y;
    // 盒的宽度
    this.width = w;
    // 盒的高度
    this.height = h;
    // 盒的类型,这里是为了不是让相同的盒做碰撞检测,如果类型相同,直接return
    this.type = type;
}
// 这个set也可以不要,直接通过.x、.y来修改
AABB.prototype = {
    setXY: function(x, y) {
        this.x = x;
        this.y = y;
    }
}

盒的大概就是这样了,但是还需要一个重要的方法来检测碰撞

// 我们把这个当作AABB的静态方法
AABB.boxCompare = function(AA, BB) {
    // AA、BB就是我们的AABB的实例
    // 这里检测AA和BB的类型,毕竟我们不希望相同类型的盒做碰撞检测
    if(AA.type == BB.type) {
        return false;
    }
    // 这里就是检测碰撞的关键
    return
      AA.x < BB.x + BB.width &&
      AA.x + AA.width > BB.x &&
      AA.y < BB.y + BB.height &&
      AA.height + AA.y > BB.y;
}

当然这就简单的实现了碰撞检测,我们还可以封装一些其他的方法来完善一些代码,比如获取所有注册盒的类型之类的方法,来方便我们写代码,这一篇就到这里了,比较轻松有意思的一节。

谷歌小恐龙之对打版(六)—— Boss

这篇来完成我们的boss,因为boss没有用户操作,所以想对比较简单,就先写这个简单一点的。先来分析一下,boss需要发射子弹和向指定区域的随机位置移动两种功能,boss要在天上飞就要呼扇翅膀,呼扇翅膀就要进行图片的切换来做成,就是下面两幅图的切换没有一种呼扇翅膀的感觉,最后就是boss要能被子弹打中,因此,boss和子弹需要碰撞盒
上代码

/**
 * Boss
 * canvas 画布
 */
// 还是一样的套路
function Boss(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.init();
}

// 这里分别代表Boss翅膀向上和向下的两种状态
Boss.type = [
    { 
        WIDTH: 48,     // 宽600
        HEIGHT: 29,     // 高12像素
        POS: [410, 735, 10, 190],      // boss的活动范围
        IMG: {x: 180, y: 3},    // Boss在图上的位置
        SPEED: 1.5,
        MOUSEHEIGHT: {x: 0, y: 17},
        HP: 10, // boss的血量
        FLY_TIME: 30
    },
    {
        WIDTH: 48,     // 宽600
        HEIGHT: 39,     // 高12像素
        POS: [410, 735, 10, 190],      // boss的活动范围
        IMG: {x: 134, y: 3},    // Boss在图上的位置
        SPEED: 1.5,
        MOUSEHEIGHT: {x: 0, y: 17},
        HP: 10, // boss的血量
        FLY_TIME: 30
    }
]

接下来就是boss所具有的方法,首先就是通用的那些,init,draw,update,但是update有点不同的是,就是要接受一个时间的参数,为什么要这样做呢,因为我们要一个时间来制作boss呼扇翅膀的动画,我们就根据这个时间来绘制动画,比如每隔300ms就"扑棱"一哈,转换成代码就是这个时间是300ms奇数倍数就展示第一张图,偶数倍数就展示第二张图,往后的动画都采用这个方法来做;还有就是随机位置,思路就是在指定的区域内随机生成一个点,然后计算当前位置距离这个位置的距离,转化成x轴和y轴的分量,让每帧都移动一段距离就OK了,至于shoot方法,我们等子弹定义好之后在补充

Boss.prototype = {
    // 在canvas上绘制Boss
    draw: function() {
        this.ctx.drawImage(gameImage,
            Boss.config.IMG.x, Boss.config.IMG.y,
            Boss.config.WIDTH, Boss.config.HEIGHT,
            this.xPos, this.yPos,
            Boss.config.WIDTH, Boss.config.HEIGHT
        );
        // 我们将boss的血量绘制在boss头顶上
        this.ctx.font = "13px Verdana";
        this.ctx.fillText('HP: ' + this.hp, this.xPos, this.yPos - 10);
        
    },
    // 初始化boss
    init: function() {
        // boss的type,用作bossAABB盒的type
        this.type = 0;
        // boss呼扇翅膀的类型
        Boss.config = Boss.type[0]
        // 这个记录的是时间,主要用来做呼扇翅膀的动画
        this.timer = 0;
        // boss的血量
        this.hp = Boss.config.HP;
        // 在y轴上的位置
        this.yPos = randomNumBoth(Boss.config.POS[2], Boss.config.POS[3]);
        // 在x轴上的位置
        this.xPos = randomNumBoth(Boss.config.POS[0], Boss.config.POS[1]);
        // 这个就是boss的盒
        this.box = new AABB(this.xPos, this.yPos, Boss.config.WIDTH, Boss.config.HEIGHT, this.type);
        // 生成下一个位置,在调用update方法时就像这边移动
        this.nextPos();
        // 调用draw方法,来吧boss绘制在canvas上
        this.draw();
    },
    update: function(time) {
        // 这里的time是在帧动画的控制器里传来的,为了就是记录这个时间来使boss呼扇翅膀
        this.timer = time;
        // 当这个不断增加的时间除以boss呼扇翅膀的时间间隔为基数就用翅膀向上的图,偶数就用翅膀向下的图
        if(isOdd(Math.floor(this.timer / Boss.config.FLY_TIME))) {
            Boss.config = Boss.type[0]
        }else {
            Boss.config = Boss.type[1]
        }
        // 每一帧boss的位置就增加一个对应的分量
        this.yPos += this.yIncrement;
        this.xPos += this.xIncrement;
        this.yPos = Math.ceil(this.yPos * 10) / 10;
        this.xPos = Math.ceil(this.xPos * 10) / 10;
        // 同时还得更新boss的盒
        this.box.setXY(this.xPos, this.yPos);
        // 当达到指定位置的时候,获取下个位置
        // 这里这么写是因为这些增量的分量是经过四舍五入的,即使不经过四舍五入,也是误差,毕竟
        // js的数字都是64位的,所以只要到达了这个误差范围内,我们就叫boss向下一个位置移动
        if(absolute(this.nextXPos - this.xPos) < 1 || absolute(this.nextYPos - this.yPos) < 1) {
            this.nextPos();
        }
        // 调用draw方法绘制
        this.draw();
    },
    // 这个是移动到下个位置的方法
    nextPos: function() {
        // 在boss的活动范围内随机生成下一个位置(x坐标和y坐标)
        this.nextXPos = randomNumBoth(Boss.config.POS[0], Boss.config.POS[1]);
        this.nextYPos = randomNumBoth(Boss.config.POS[2], Boss.config.POS[3]);
        // 计算移动到下个位置所用的时间
        var t = getS({x: this.nextXPos, y: this.nextYPos}, {x: this.xPos, y:this.yPos}) / Boss.config.SPEED;
        // 每 t 内 xPos 和 yPos 的增量
        // 这里计算的是每一帧boss要移动的距离在x轴和y轴的分量,取一下整数,方便计算
        this.xIncrement = Math.ceil((this.nextXPos - this.xPos) / t * 10) / 10;
        this.yIncrement = Math.ceil((this.nextYPos - this.yPos) / t * 10) / 10;
        this.shoot();
    },
    // 这个就是boss射击的方法,调用的子弹类的生成子弹的方法来生成子弹
    shoot: function() {
        // 子弹还没有定义,我们先注释掉
        // Bullet.creat(this.canvas, {x: Boss.config.MOUSEHEIGHT.x + this.xPos, y: Boss.config.MOUSEHEIGHT.y + this.yPos}, -1, 0);
    }
}

这一篇也没有什么难理解的地方,就是boss的随机移动,射击几个方法,稍微有一点复杂的地方就是boss的呼扇翅膀,下篇,开始写子弹。

谷歌小恐龙之对打版(七)—— 子弹

完成boss之后,boss需要发射子弹的功能,然后我们的小恐龙也需要发射子弹的功能,所以先把子弹撸完;子弹其实和云朵一个写法,只不过多了几个方法和碰撞盒,思路和云朵一样,将全屏幕的子弹都写到一个集合中,每次都去遍历集合中的子弹,子弹也有类型,来区分是boss的子弹还是我们小恐龙的子弹,其他的方法大家都快看吐了,init,draw,update

/**
 * 子弹的画布
 * canvas 画布
 * 子弹的构造函数和之前云朵啥的不一样,因为多了一些子弹的方向,子弹的
 * 类型和子弹的初始位置,因为子弹是跟着boss移动的
 */
function Bullet(canvas, initPos, dir, type) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.b_xPos = initPos.x;
    this.b_yPos = initPos.y;
    // 子弹前进的方向,boss的子弹向左,小恐龙的子弹向右
    this.dir = dir;
    this.type = type;
    this.config = Bullet.types[this.type];
    this.box = new AABB(this.b_xPos, this.b_yPos, this.config.WIDTH, this.config.HEIGHT, this.type);
}

// 这个就是子弹的类型了,我们想区分boss的子弹和小恐龙的子弹
// png图上并没有子弹这个东西,所以随便找了个黑色的像素将就了一下
// 如果觉得不够酷炫,可以去网上找一些冒蓝火的那种子弹画上来也是非常棒的
Bullet.types = [
    {
        WIDTH: 10,
        HEIGHT: 4,
        IMG: {x: 5, y: 5},
        SPEED: 5 
    },
    {
        WIDTH: 4,
        HEIGHT: 4,
        IMG: {x: 65, y: 7},
        SPEED: 5
    }
];

基本的一些参数配置完成后,就是子弹的方法了,还记得云朵吗,用一个方法生成云朵,存储在一个集合中,需要执行某些操作的时候,就遍历这个集合,那么子弹也一样,先创建一个集合来存这些子弹

Bullet.bullets = [];

然后就是子弹的方法,按照上面的思路来写

Bullet.prototype = {
    // 在canvas上绘制子弹
    draw: function() {
        this.ctx.drawImage(gameImage,
            this.config.IMG.x, this.config.IMG.y,this.config.WIDTH, this.config.HEIGHT,
            this.b_xPos, this.b_yPos, this.config.WIDTH, this.config.HEIGHT
        )
    },
    // 子弹需要移动,所以要给每个子弹加上move方法
    move: function() {
        this.b_xPos += this.config.SPEED * this.dir;
    }
}
// 这里就是创建子弹,然后存到集合中,并在子弹构造函数中生成AB盒
Bullet.creat = function(canvas, bPos, dir, type) {
    Bullet.bullets.push(new Bullet(canvas, bPos, dir, type)); 
}

// 这里我写的很乱,按道理检测子弹是否打到boss或者小恐龙的方法不应该在这里
// 这个往后慢慢研究吧,我也第一次接触canvas的小游戏,还没有很好的思维方式
Bullet.update = function(trex, boss) {
    // 非空的判断
    var len = Bullet.bullets.length;
    if(len == 0) {
        return;
    }
    // 当子弹飞出屏幕外的时候,删除子弹,要不然的话通过时间的累积,子弹越来
    // 子弹越来越多,计算量就会越来越大
    if(Bullet.bullets[0].b_xPos > gameConfig.bg.WIDTH){
        Bullet.bullets.shift();
    }
    // 遍历这些集合
    for(var i = 0; i < Bullet.bullets.length; i ++) {
        // 移动
        Bullet.bullets[i].move();
        // 同时设置他的盒的位置
        Bullet.bullets[i].box.setXY(Bullet.bullets[i].b_xPos, Bullet.bullets[i].b_yPos);
        // 绘制
        Bullet.bullets[i].draw();
        // 检测子弹与boss和恐龙的碰撞,如果发生碰撞就把各自的血量减1,并删掉这个子弹
        if(Bullet.bullets[i] && AABB.boxCompare(trex.box, Bullet.bullets[i].box)) {
            trex.hp -= 1;
            Bullet.bullets.splice(i, 1);
        }
        if(Bullet.bullets[i] && AABB.boxCompare(boss.box, Bullet.bullets[i].box)) {
            boss.hp -= 1;
            Bullet.bullets.splice(i, 1);
        }
    }
}

子弹和云朵也没有什么差别,多了几个判断是否打到物体的方法,接下来就是小恐龙了,比较复杂一点的,写这个小恐龙花的时间最久。

谷歌小恐龙之对打版(八)—— 小恐龙

小恐龙放在最后,因为确实挺复杂的,花的时间也比较长,因为先前并没有接触过小游戏,事先设计的全是bug,通过不断的修修补补终于是完成了,虽然可以用,但是还是比较的乱,先把我的思路和代码亮出来,以后我会好好的设计一下,再者用一些打包工具,比如webpack之类的工具重新设计一份,学习打包工具的同时学习canvas,一举多得。

来整理一下思路,小恐龙应该具有哪些属性和方法,仔细想想,前进、后退、跳跃、射击这几个方法,然后是血量,最后就是跑动的动画和跳跃的动画,一个一个来分析。

前进后退是一组功能,这组功能中只能有一个在执行,或者都不执行,我们可以用一个数组来存放这些状态,每次都把用户的操作push进数组中,每次也都执行栈顶的操作,比如有一个数组

var state = [];
// 当用户按下前进的时候
state.push['GO'];
// 执行恐龙的前进方法
trex.GO();

大致的思路就是这样,当用户抬起按键时删除数组中对应的操作,这样满足用户按下一个案件的同时按下另一个按键的情况,同时满足用户按下很多按键之后按照不同顺序抬起按键的情况,因为我们只执行栈顶的操作,比如,当用户按下前进之后没有抬起按键,继续按下后退,这时我们就执行后退,然后松开后退,执行删除后退的操作,是的前进继续到达栈顶,然后执行前进的操作,松开前进按键,删除前进操作,小恐龙停止运动,流程图如下,灵魂画手,自行理解吧
当然不知这一个栈,我们要做很多栈来存放这些状态,比如存前进后退的,存跳跃和射击的,因为当前进或者后退的时候是可以跳跃或者射击的

接下来是一个起跳和下落的分析,看个图先,就是这个样子,当然没有改变canvas的坐标系的话,坐标原点在左上方,转化一下就OK了

/**
 * 恐龙
 * canvas 画布
 */
function Trex(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.init();
}

Trex.config = {
    JUMP_INITV: -18,     // 起跳初速度
    G: 1,                // 重力加速度
    BOTTOM_YPOS: 193,    // 恐龙与地面的接触位置
    SPEED: 4,             // 移动速度
    HP: 10,  // 恐龙的血量
    RUN_TIME: 4
}

Trex.type = {
    STATICING: {
        NAME: 'STATICING', // 静止
        WIDTH: 44,        // 静止时的宽度
        HEIGHT: 47,      // 静止时的高度
        MOUSEHEIGHT: 40, // 嘴的高度 子弹发射口的高度
        IMG1: {x: 848, y: 3}, // 静止1状态在图中的位置
        IMG2: {x: 892, y: 3}, // 静止2状态在图中的位置
        IMG_TIMING: 3000, // 静止时 图片切换时间 用于眨眼
        BLINK_TIMING: 100, // 眨眼瞬间的时间
        MOUSEHEIGHT: {x: 20, y: 12},
        IMG: {x: 848, y: 3}
    },
    RUNNING: {
        NAME: 'RUNNING',   // 前进后退的图片
        WIDTH: 44,        // 前进时的宽度
        HEIGHT: 47,      // 前进时的高度
        MOUSEHEIGHT: 40, // 嘴的高度 子弹发射口的高度
        IMG1: {x: 980, y: 3},  // 前进状态在图中的位置
        IMG2: {x: 936, y: 3}, // 前进状态在图中的位置
        IMG_TIMING: 100,        // 奔跑时图片切换的时间
        MOUSEHEIGHT: {x: 20, y: 12},
        IMG: {x: 980, y: 3},
    }
}

老样子,构造函数和一些配置参数,上面配置中,把前进后退跑动过程中的图片切换配置在RUNNING状态中,分别为IMG1和IMG2,就像boss呼扇翅膀一样,小恐龙跑动的迈步子的动画就靠切换这两张图来执行了。
接下来是方法,老样子,把方法放在原型对象上,实在不知道单独拿出来该如何讲,所以把代码都贴上来,一点一点慢慢解释

Trex.prototype = {
    // 画笔工具绘制恐龙
    draw: function() {
        this.ctx.drawImage(gameImage,
            this.config.IMG.x, this.config.IMG.y, this.config.WIDTH, this.config.HEIGHT,
            this.xPos, this.yPos, this.config.WIDTH, this.config.HEIGHT
        )
        // 将hp绘制在小恐龙头部
        this.ctx.font = "13px Verdana";
        this.ctx.fillText('HP: ' + this.hp, this.xPos, this.yPos - 10);
    },
    /**
     * 初始化一些参数
     *    - 恐龙的状态,比如是站立还是跑动
     *    - 初始的位置,血量等等
     * 绑定AABB盒
     */
    init: function() {
        // 初始化一些参数
        this.type = 1; // 盒模型的类型,用于检测不同类型之间的检测的
        this.timer = 0; // 记录各种动画切换的计时器
        this.hp = Trex.config.HP; // 恐龙的HP
        this.xPos = 100; // 恐龙的初始位置
        this.yPos = Trex.config.BOTTOM_YPOS; // 恐龙的初始位置
        this.config = Trex.type.STATICING; // 恐龙初始的精灵图
        // 恐龙的AABB盒
        this.box = new AABB(this.xPos, this.yPos, this.config.WIDTH, this.config.HEIGHT, this.type);
        // 恐龙的起跳速度
        this.jumpV = Trex.config.JUMP_INITV;
        // 恐龙是否在起跳过程中
        this.isJumping = false;
        // 游戏进行的时间,用来制作恐龙跑动的动画
        this.timer = 0;
        // 恐龙的状态
        this.stateMap = {
            JUMPING: 'STATICING',
            WAITING: 'STATICING',
            GOING: 'STATICING',
            BACKING: 'STATICING'
        }
        // 控制前进后退的行为栈,WATING为默认行为,永远执行栈顶的行为;
        this.actionState = ['WATING'];
        // 跳跃的功能行为栈,
        this.funcState = [];
        this.draw();
    },
    /**
     * 根据用户的操作更新小恐龙的位置等状态 
     */
    update: function(time) {
        // 当血量为零的时候,游戏结束
        if(this.hp <= 0) {
            gameState = 'OVER';
        }
        // 这里是个小彩蛋,当小恐龙超过右边界的时候,游戏通关
        if(this.xPos >= gameConfig.bg.WIDTH) {
            success = 'SUCCESS';
        }
        this.timer = time;
        // 为actionState栈顶的行为
        // 获取前进还是后退状态
        var tmpActionState = this.actionState[this.actionState.length - 1];
        // 为funcState栈顶的行为
        // 获取是否为跳跃状态
        var tmpFuncState = this.funcState[this.funcState.length - 1];
        // 执行前进后退与跳跃的执行函数
        this.actionFsm(tmpActionState);
        this.funcFsm(tmpFuncState);
        // 当游戏进行时,根据恐龙的状态来绘制不同的动画帧,切换不同的小恐龙
        // 和boss一样,通过改变绘制的精灵图来达到动画的效果
        if(tmpFuncState != 'JUMPING' && (tmpActionState == 'GOING' || tmpActionState == 'BACKING')) {
            // 是奇数就迈左脚,偶数就迈右脚,跳跃状态或者等待状态就用两脚着地的图片
            if(isOdd(Math.floor(this.timer / Trex.config.RUN_TIME))) {
                Trex.type.RUNNING.IMG = Trex.type.RUNNING.IMG1;
                this.config =  Trex.type.RUNNING;
            }else {
                Trex.type.RUNNING.IMG = Trex.type.RUNNING.IMG2;
                this.config =  Trex.type.RUNNING;
            }
        }else {
            this.config =  Trex.type.STATICING;
        }
        // 更新AABB盒的状态
        this.box.setXY(this.xPos, this.yPos);
        // 绘制小恐龙
        this.draw();
    },
    // 将行为push到栈顶,并执行
    setState: function(stateType, state) {
        this.config = Trex.type[this.stateMap[state]];
        this[stateType].push(state);
    },
    // 当按键弹起时,将起对应的行为删掉
    delState: function(name, state) {
        // 同事删除其栈中的行为
        for(var i = 0, len = this[name].length; i < len; i ++) {
            if(state == this[name][i]) {
                this[name].splice(i, 1);
            }
        }
        // console.log(this.funcState);
    },
    // 根据不同的状态来执行不同的方法,这里是前进后退
    // 为什么要把跳跃和前进后退分开,因为前进的时候可以跳跃,但是前进的时候不能后退
    actionFsm: function(actionState) {
        switch(actionState) {
            case 'GOING':
                this.go();
                break;
            case 'BACKING':
                this.back();
                break;
            case 'WAITING':
                this.wait();
                break;
        }
    },
    // 根据不同的状态来执行不同的方法,这里是跳跃
    funcFsm: function(funcState) {
        switch(funcState) {
            case 'JUMPING':
                this.jump();
                break;
        }
    },
    // 没有任何操作的时侯执行的函数,但是没想好没有任何操作该执行怎样的操作
    wait: function() {
      // do wait
    },
    // 前进
    go: function() {
        // 当小恐龙走到中间的位置的时候停下了,被障碍物当着
        if(bossState == 'LIVE' && this.xPos >= gameConfig.bg.WIDTH / 2 - this.config.WIDTH) {
            return;
        }
        // 移动的速度是配置中的参数
        this.xPos += Trex.config.SPEED;
    },
    // 后退
    back: function() {
        // 当后退到边界的时候停下来
        if(this.xPos <= 0) {
            return;
        }
        this.xPos -= Trex.config.SPEED;
    },
    /**
     * 起跳
     * 起跳的时候要有状态标识是否在起跳中
     * 如果在起跳中则不做任何操作
     * 如果不在起跳状态中则执行起跳的操作
     * 起跳是怎么个逻辑呢,单独画个图讲一下 
     */
    jump: function() {
        this.isJumping = true;
        if(this.isJumping) {
            this.jumpV += Trex.config.G;
            this.yPos += this.jumpV;
        }
        if(this.yPos >= Trex.config.BOTTOM_YPOS) {
            this.yPos = Trex.config.BOTTOM_YPOS;
            this.jumpV = Trex.config.JUMP_INITV;
            this.isJumping = false;
            this[67] = true;
            this.delState('funcState', 'JUMPING');
        }
    },
    // 射击
    shoot: function() {
        // 这个简单,就调用方法创建子弹就OK了,子弹中包含有type信息会指定是谁的子弹
        Bullet.creat(this.canvas, {x: this.config.MOUSEHEIGHT.x + this.xPos, y: this.config.MOUSEHEIGHT.y + this.yPos}, 1, 1);
    }
}

boss到这里就完结了,确实有点乱,以后慢慢整理吧,接下来就是启动了和一些用户交互了

谷歌小恐龙之对打版(九)—— 交互和启动

到现在未知还差交互和启动了,先说交互,无非就是监听一些键盘事件,获取code码,根据不同的code码来执行不同的操作,因为我们先前定义的一些状态,当处于某个状态时会执行某个函数,所以我们只关心是哪种状态就OK了,而不需要直接调用函数

// 监听键盘事件
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);

// 根据不同的键盘code码来设置不同的状态
function onKeyDown(e) {
    switch(e.keyCode){
        case 37:
            trex.setState('actionState', 'BACKING');
            break;
        case 67:
            trex.setState('funcState', 'JUMPING');
            break;
        case 39:
            trex.setState('actionState', 'GOING');
            break;
        case 88:
            trex.shoot();
            break;
    }
}

function onKeyUp(e) {
    switch(e.keyCode){
        case 37:
            trex.delState('actionState', 'BACKING');
            break;
        case 39:
            trex.delState('actionState', 'GOING');
            break;
        case 88:
            break;
    }
}

启动

// 监听Enter键,按下启动
document.addEventListener('keydown', GameStart);
// 当游戏的状态处在等待时启动游戏,并将游戏状态改为正在进行中
function GameStart(e) {
    if(gameState == 'WAITING' && e.keyCode == 13) {
        gameState = 'RUNNING';
        start();
    }
}

最后补充一下,前面的小恐龙和boss完成之后需要在动画帧的控制器中调用他们的update方法

var timer = 0;
(function draw(time) {
    timer ++;
    // time 大约16ms 两帧间隔时间
    ctx.clearRect(0,0,800,250);
    // 绘制完成后不会改变的图像 用draw方法
    floor.draw();
    cacti.draw();
    // 绘制完成后会改变的图像 用update方法
    cloud.update();
    trex.update(timer);
    boss.update(timer, trex);
    Bullet.update(trex, boss);
    // 游戏结束之后绘制gameover的图像
    if(gameState == 'OVER') {
        ctx.clearRect(0,0,800,250);
        return ctx.drawImage(over, 0, 0, 500, 98, 160, 70, 500, 98);
    }
    // 游戏成功之后绘制成功的图像
    if(gameState == 'SUCCESS') {
        ctx.clearRect(0,0,800,250);
        ctx.drawImage(suc, 0, 0, 387, 51, 200, 140, 387, 51);
        return ctx.drawImage(win, 0, 0, 653, 49, 120, 70, 653, 49);
    }
    window.requestAnimFrame(draw);
})();

到此为止,改造版的小恐龙就到此为止了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值