【老脸教你做游戏】小鸟飞过障碍物的游戏(下)

上期我们只实现了小鸟飞行以及障碍物地图随机生成、绘制和滚动,这期我们要完成整个游戏的大体框架,即游戏运行的主体部分,而其他的比如计分,重新开始等不会去实现。文章最后会讲一些个人心得。

本期内容依旧是在微信小游戏上进行实现的。由于内容以及代码都承接以前文章,如果你没有阅读过,可以从这里开始。

本文不允许任何形式的转载!

阅读提示

本系列文章不适合以下人群阅读,如果你无意点开此文,请对号入座,以免浪费你宝贵的时间。

  • 想要学习利用游戏引擎开发游戏的朋友。本文不会涉及任何第三方游戏引擎。
  • 不具备面向对象编程经验的朋友。本文的语言主要是Javascript(ECMA 2016),如果你不具备JS编程经验倒也无妨,但没有面向对象程序语言经验就不好办了。

关注我的微信公众号,回复“源代码4”可获得本文示例代码下载地址,谢谢各位了!


老脸的公众号

最基础的碰撞判定

为什么要加上一个“最基础的”呢,因为二维图形碰撞(Collision)一般分为Bounds接触判定(可能会碰撞)和最终碰撞判定(多边形到底是否有接触),比如下图:

多边形碰撞

首先说下Bounds是什么。我们可以认为Bounds就是一个区域,通常我们都以一个矩形作为Bounds,但有时候可能会是其他形状。就上图而言,我们的Bounds是一个矩形,并且是和XY轴平行的,这种Bounds我们成为Axis-aligned bounding boxes,简称AABB(下文提到的Bounds都是这个AABB)。一般只需要知道这个矩形的左上角和右下角两个点的位置,就能通过计算得知两个Bounds是否接触。

通常来说我们称左上角为min,右下角为max,意为该Bounds最小的点和最大的点。而我们接下来用的是[left,top],[right,bottom]分别来表示最小点和最大点的坐标。

怎么判断两个Bounds没有接触呢:
设两个不同的Bounds A和B,如果其中一个的left比另外一个的right大,或者它的top比另一个的bottom大,则我们认为 A没有和B接触。反之,判断两个Bounds是否接触的代码如下:

function overlaps(boundsA,boundsB){
    if(boundsA.left > boundsB.right 
       || boundsA.top > boundsB.bottom){
        return false;
    }
    if(boundsB.left > boundsA.right 
       || boundsB.top > boundsA.bottom){
        return false;
    }
    return true;
}
最大最小点映射到xy轴上的值进行比较

简化一下上面代码:


    function overlaps(boundsA, boundsB) {
        return (boundsA.left <= boundsB.right && boundsA.right >= boundsB.left
            && boundsA.bottom >= boundsB.top && boundsA.top <= boundsB.bottom);
    }

我就问一下,你觉得哪段代码好?我选第二个。

回到我们的游戏中,为了简化,我们认为只要树干和小鸟的Bounds接触上,就算碰撞。
那小鸟和树干的Bounds怎么得到呢。

Figure类的第1次迭代

综上,我们知道了什么是Bounds,而且注意到,要判断两个Bounds是否接触,必要条件是这两个Bounds必须在同一个坐标系下。
我们设计的Figure类具有left,top,width和height,可以用这几个属性来表示所在区域,所以我们的Figure的Bounds的最小坐标点就是[left,top],最大坐标点就是[left+width,top+height],为了便于代码阅读和简化编码,我们给Figure加上两个属性right,bottom以及一个getBounds方法:

Figure.js : 
   ...
    getBounds() {
        return {
            left: this.left, top: this.top,
            right: this.right, bottom: this.bottom
        };
    }

    get right() {
        return this.left + this.width;
    }

    get bottom() {
        return this.top + this.height;
    }
    ...

如果你对Figure类第0次迭代这篇文章还有印象的话,就应该知道Figure的left和top是相对其父节点的坐标系的,所以Figure的bounds不应该这么简单地计算!比如一个Figure A的子Figure C和另一个Figure B中的子Figure D进行bounds碰撞测试,如果按照上面的代码得到的bounds来判断的话肯定是错了,坐标系都不同比较个卵。

所以我们说bounds首先是有一个相对性的,就我们的游戏而言,要判断小鸟和树桩是否碰撞,就应该获得它们相对于顶层Graph的bounds值,然后再进行判断。幸运的是小鸟和树桩(应该说是障碍地图)正好是Graph的子节点,所以上述代码正好得到它们相对于Graph的bounds,直接判断就好了。

除了相对坐标系的问题,Figure的bounds还应该要考虑到自身的旋转和拉伸,这都会影响到Bounds值,更幸运的是,我们目前这个游戏中小鸟和树桩都没有旋转和拉伸。如何计算旋转和拉伸后的Bounds我会在以后讲到变换矩阵的时候再说,先提示一下各位免得产生误会。

小鸟的Bounds还好说,就刚才代码即可获得,但是TileMap的怎么办?它的Bounds是整个地图的区域,跟树干没关系呀,这里我们就只能是特例特办了,通过TileMap的地图数据来计算地图中所有树干的Bounds。
先看下之前TileMap的drawSelf方法:

drawSelf(ctx) {
        let imageManager = ImageManager.getInstance();
        let trunk1 = imageManager.getImage('trunk1');
        let trunk2 = imageManager.getImage('trunk2');
        let trunk3 = imageManager.getImage('trunk3');
        let startX = 0;
        let startY = 0;
        // 一列一列画:
        for (let i = 0; i < this.mapData.mapData.length; i++) {
            startY = 0;//每一列的y轴坐标都是从0开始
            // 先计算出每一列绘制的起始x轴的值:
            if (i != 0) {
                // 从第二列开始就要计算了,第一列是0
                startX += this.trunkWidth;// 第n列比第n-1列绝对多出一个宽度
                // 再加上它们之间的间隔:
                startX += this.mapData.spaceData[i-1] * this.trunkWidth;
            }
            // 某一列的地图数据:
            let datas = this.mapData.mapData[i];
            for (let j = 0; j < datas.length; j++) {
                let data = datas[j];
                let image = undefined;
                switch (data) {
                    case 1:
                        image = trunk2;
                        break;
                    case 2:
                        image = trunk3;
                        break;
                    case 3:
                        image = trunk1;
                        break;

                }
                // 如果image为空,即地图数据为0,此处是个空白处就不需要绘制
                if (image) {
                    ctx.drawImage(image, startX, startY, this.trunkWidth, this.trunkHeight);
                }
                // 画好一个就往下y坐标往下移动一个树桩高度
                startY += this.trunkHeight;
            }
        }
    }

它是通过地图数据在drawSelf中计算出树桩的位置和类型,然后绘制上去。正好,我们的树桩bounds计算跟这段代码几乎一样,那我们就可以利用这段代码来计算出树桩bounds了。
为了不重复计算,我们可以在生成地图数据的时候就计算出树桩的bounds数据,然后在绘制的时候直接使用即可,无需二次计算。所以我更改了TileMap的代码,你可以拿去和以前的比较一下:

import Figure from "./Figure";
import MapGenerator from "./MapGenerator";
import ImageManager from "./utils/ImageManager";

export default class TileMap extends Figure {
    constructor(p) {
        super(p);
        this.mapData = undefined;
        this.trunkHeight = 0; // 单个树桩的高度
        this.trunkWidth = 0; // 单个树桩的宽度
        this.column = 0;
        this.row = 0;
        this.boundsArray = []; // 地图中树桩的Bounds数据数组
    }

    initMap() {
        if (this.mapData != undefined) {
            MapGenerator.generateMapData(this.mapData);
        } else {
            this.mapData = MapGenerator.generateMapData(this.mapData, this.column, this.row);
        }
        // 根据map数据来设置Map的高度和宽度:
        this.height = this.mapData.row * this.trunkHeight;
        this.width = this.mapData.column * this.trunkWidth;
        // 还要加上每列树桩之间的间隔:
        for (let i = 0; i < this.mapData.spaceData.length; i++) {
            this.width += this.mapData.spaceData[i] * this.trunkWidth;
        }
        // 生成树桩的bounds数据:
        this.calculateTrunkBounds();
    }

    calculateTrunkBounds() {
        let boundsArray = this.boundsArray;
        boundsArray.length = 0;// 先清空
        let startX = 0;
        let startY = 0;
        for (let i = 0; i < this.mapData.mapData.length; i++) {
            startY = 0;//每一列的y轴坐标都是从0开始
            // 先计算出每一列绘制的起始x轴的值:
            if (i != 0) {
                // 从第二列开始就要计算了,第一列是0
                startX += this.trunkWidth;// 第n列比第n-1列绝对多出一个宽度
                // 再加上它们之间的间隔:
                startX += this.mapData.spaceData[i - 1] * this.trunkWidth;
            }
            // 某一列的地图数据:
            let datas = this.mapData.mapData[i];
            for (let j = 0; j < datas.length; j++) {
                let data = datas[j];
                if (data != 0) {
                    // 跳过缺口区域, bounds里还多了一个data属性
                    // 这是为了绘制的时候不必再从mapData里取数据判断树桩image了
                    boundsArray.push({
                        left: startX,
                        top: startY,
                        right: startX + this.trunkWidth,
                        bottom: startY + this.trunkHeight,
                        data: data
                    });
                }
                // y坐标往下移动一个树桩高度
                startY += this.trunkHeight;
            }
        }
    }

    getTrunkBoundsArray() {
        return this.boundsArray;
    }

    drawSelf(ctx) {
        let imageManager = ImageManager.getInstance();
        let trunk1 = imageManager.getImage('trunk1');
        let trunk2 = imageManager.getImage('trunk2');
        let trunk3 = imageManager.getImage('trunk3');
        let trunkDatas = this.getTrunkBoundsArray();
        for(let i = 0 ; i < trunkDatas.length;i++){
            let trunkData = trunkDatas[i];
            let image = undefined;
            switch (trunkData.data) {
                case 1:
                    image = trunk2;
                    break;
                case 2:
                    image = trunk3;
                    break;
                case 3:
                    image = trunk1;
                    break;
            }
            if (image) {
                ctx.drawImage(image, trunkData.left, trunkData.top, 
                this.trunkWidth, this.trunkHeight);
            }
        }
    }
}

新的TileMap类中增加了一个calculateTrunkBounds方法,和一个getTrunkBoundsArray方法。在我们生成地图的时候就开始计算Bounds,然后绘制的时候直接就使用了。

请注意!!,树桩的Bounds目前并不是相对Graph的,而是相对于其所在地图的,这是因为计算bounds的方法只在生成地图的时候被调用,而这时候的地图坐标和需要计算碰撞时候的坐标是不同的,所以只能先算出树桩相对地图的bounds,然后再在需要进行碰撞测试的时候加上地图的left和top来获取其相对Graph的bounds。

便于测试,我把小鸟和树桩的bounds都绘制成了红色边框,得到如下结果:


在这里插入图片描述

注意看,小鸟的bounds实际上有点过大了,因为整个小鸟并没有占满整个Figure的区域,所以在测试碰撞的时候我们要可以缩小一下他的Bounds区域,不然小鸟背部根本没接触到树干还判定为碰撞就不好了。比如这么大,我觉得就挺合适了。


在这里插入图片描述

有一种碰撞判定叫做 像素碰撞,即查看两个图片数据中是否有不全透明的像素点重合,我觉得好麻烦啊,这种判断难道不会很慢吗

最后我们就需要在BirdFlyGame里加入碰撞判定代码,一旦碰撞上游戏就结束:

BirdFlyGame.js部分代码:
....
    afterRefresh(refreshCount) {
        if(!this.gameOver){
            this.collisionTest();
        }
    }
    collisionTest() {
        // 小鸟bounds:
        let birdBounds = this.bird.getBounds();
        let scaleW = this.bird.width * 0.2;
        let scaleH = this.bird.height * 0.2;
        // 让bounds缩小点
        let a = {
            left: birdBounds.left + scaleW,
            top: birdBounds.top + scaleH,
            right: birdBounds.left + (this.bird.width - scaleW),
            bottom: birdBounds.top + (this.bird.height - scaleH)
        };
        let that = this;

        collisionTrunkTest(this.firstMap);
        collisionTrunkTest(this.secondMap);

        function collisionTrunkTest(map){
            let firstBounds = map.getTrunkBoundsArray();
            for (let i = 0; i < firstBounds.length; i++) {
                let bounds = firstBounds[i];
                // bounds是相对地图的,需要加上它在Graph的位置
                // 以此来获得树桩相对于Graph的Bounds
                let b = {
                    left: bounds.left + map.left,
                    top: bounds.top + map.top,
                    right: bounds.right + map.left,
                    bottom: bounds.bottom + map.top
                };
                if(BirdFlyGame.overlaps(a,b)){
                    that.processGameOver();
                    return;
                }
            }
        }
    }
    processGameOver(){
        this.gameOver = true;
        this.gameStop();
        console.log('game over');
    }
    static overlaps(bounds1, bounds2) {
        let a = bounds1;
        let b = bounds2;
        return (a.left <= b.right && a.right >= b.left
            && a.bottom >= b.top && a.top <= b.bottom);
    }

下面是测试结果,一旦小鸟的红色框碰到树干的,游戏就停止了。


在这里插入图片描述

我们就可以在processGameOver里做一些操作,比如显示飞行距离、时间、重来一次按钮等。

性能优化

1.去掉不必要的绘制

如果Figure没有在Graph的显示范围内(即屏幕里面),其实是没必要绘制的。
而我们的地图却不是。第一张地图和第二张地图同时往左移动,在第二张地图实际上还没有进入到Graph的区域的时候,我们却依然在绘制它,并且TileMap类还是一个要绘制大量图片的类,这就让效率降低了不少。


在这里插入图片描述

如果我们提前判断出Figure不在父节点的区域内的话,就不要绘制它。所以我们可以在Figure类的drawChildren方法中进行一次Bounds碰撞判断,发现子Figure没有和父Figure所在显示区域接触,就不绘制:

   drawChildren(ctx) {
        for (let i = 0; i < this.children.length; i++) {
            let childFigure = this.children[i];
            // bounds碰撞判定基于Figure坐标系
            // 所以Figure显示区域的bounds左上角就是[0,0]
            let displayBounds = {left:0,top:0,
                                 right:this.width,bottom:this.height};
            if(!Utils.overlaps(displayBounds,childFigure.getBounds())){
                continue;
            }
            childFigure.draw(ctx);
        }
    }

这个解决了整个Figure在父节点区域外会进行绘制的问题。不过TileMap类却要绘制大量树桩,而且有很多树桩都是在显示区域外的,就如上图所示,蓝色背景的地图中,有一部分还是在屏幕外的。
这个也好解决,我们在TileMap的drawSelf中,通过树桩的Bounds来跟Graph的显示区域Bounds做碰撞测试,没有接触的就不绘制:

drawSelf(ctx) {
        let imageManager = ImageManager.getInstance();
        let trunk1 = imageManager.getImage('trunk1');
        let trunk2 = imageManager.getImage('trunk2');
        let trunk3 = imageManager.getImage('trunk3');
        let trunkDatas = this.getTrunkBoundsArray();
        let graph = this.getGraph();
        let graphDisplayBounds = {
            left: 0, top: 0,
            right: graph.width,
            bottom: graph.height
        };
        for (let i = 0; i < trunkDatas.length; i++) {
            let trunkData = trunkDatas[i];
            let image = undefined;
            // 找到树桩对应graph坐标系的Bounds
            let absoluteBounds = {
                left: trunkData.left + this.left, top: trunkData.top + this.top,
                right: trunkData.right + this.left, bottom: trunkData.bottom + this.top
            };
            // 不在显示区域就跳过这一个(如果像之前是个二维数组就好了,直接可以跳过一列)
            if(!Utils.overlaps(absoluteBounds,graphDisplayBounds)){
                continue;
            }
            switch (trunkData.data) {
                case 1:
                    image = trunk2;
                    break;
                case 2:
                    image = trunk3;
                    break;
                case 3:
                    image = trunk1;
                    break;
            }
            if (image) {
                ctx.drawImage(image, trunkData.left, trunkData.top,
                    this.trunkWidth, this.trunkHeight);
            }
        }
    }

不要小看只是少画了一些图片,性能会提升很多的。再次提醒各位,用CanvasRenderingContext2D绘图是很慢的,能改进的地方就改进,提升一点是一点,否则一旦把程序搬到移动设备上就会卡得出奇。

下图是性能测试的结果,CPU 2 x slowdown,大约录值了6秒,不太准。


在这里插入图片描述
2.取消不必要的碰撞测试

在测试小鸟和树干碰撞的时候,我们的代码如下:

collisionTrunkTest(this.firstMap);
collisionTrunkTest(this.secondMap);

是将两张地图的所有树桩Bounds和小鸟的Bounds进行测试,就如上面优化绘制遇到的问题一样,很多树桩位置根本就没进入屏幕,这种碰撞测试就是无意义的。
更进一步来说,如果地图没有进入到小鸟可能在的位置区域,就没必要进行测试,如下图所示:


在这里插入图片描述

如果还要细分的话就需要改造之前生成的树桩Bounds数据结构,目前我把所有树桩的Bounds放到了一个数组中,可以把它改成一个二维数据结构:

之前存放的数据是这样的:
一维数组,每个数据表示树桩的Bounds(data是之前为了判断树桩类型保留下来的一个属性):
[{left:数字,top:数字,right:数字,bottom:数字,data:数字} , ........]
可以改造成:
[
  ..........
  // 某一列树桩bounds数据:
  {
   trunkBounds : [{left,top,right,bottom,data} , ........]
  }
  ..........
]

目前测试方法是遍历一维数组。改造后虽然也要遍历一遍,但是由于分成了“列”,如果某列第一个不在测试范围,则整列就跳过。这里我只提一下,就不改造原来的数据结构了。
此外,如果某个树桩没有进入小鸟碰撞区域,并且在该区域右侧,那么这个树桩往后的所有树桩都没有进入该区域,不需要测试碰撞:

BirdFlyGame.js ,修改碰撞测试方法:
collisionTest() {
        // 小鸟bounds:
        let birdBounds = this.bird.getBounds();
        let scaleW = this.bird.width * 0.2;
        let scaleH = this.bird.height * 0.2;
        // 让bounds缩小点
        let a = {
            left: birdBounds.left + scaleW,
            top: birdBounds.top + scaleH,
            right: birdBounds.left + (this.bird.width - scaleW),
            bottom: birdBounds.top + (this.bird.height - scaleH)
        };
        let that = this;
        // 小鸟可能出现的区域bounds
        let birdDisplayBounds = {
            left : a.left,
            right:a.right,
            top:0,
            bottom:this.height
        };
        collisionTrunkTest(this.firstMap);
        collisionTrunkTest(this.secondMap);

        function collisionTrunkTest(map){
            if(!Utils.overlaps(map.getBounds(),birdDisplayBounds)){
                // 如果地图没有进入小鸟出现区域,则不测试
                return;
            }
            let firstBounds = map.getTrunkBoundsArray();
            for (let i = 0; i < firstBounds.length; i++) {
                let bounds = firstBounds[i];
                // bounds是相对地图的,需要加上它在Graph的位置
                // 以此来获得树桩相对于Graph的Bounds
                let b = {
                    left: bounds.left + map.left,
                    top: bounds.top + map.top,
                    right: bounds.right + map.left,
                    bottom: bounds.bottom + map.top
                };
                if(Utils.overlaps(a,b)){
                    that.processGameOver();
                    return;
                }else{
                    // 树桩和小鸟的bounds没有接触,看一下该bounds是否在小鸟bounds的右侧
                    // 如果是,则跳过剩余树桩的碰撞测试
                    if( b.left > a.right ) {
                        break;
                    }
                }
            }
        }
    }

上面我提到,如果树桩bounds在小鸟右侧并没接触上则跳过剩余的树桩。那如果树桩bounds如果在小鸟左侧也没接触上,那该树桩之前的树桩也是没必要测试碰撞的,但是我们是遍历一个数组,逐个进行比较,且数组里的Bounds数据的left是从左至右的(从小到大),所以刚才说的那个就不好实现。

小结

我们的游戏主体框架就已经全部完成了,即小鸟可以控制飞行,地图可能随机生成并能滚轴移动,可以进行碰撞测试等,并且我们还进行了一些优化,游戏其余的部分:加入音效和背景音乐,游戏结束后计分,游戏开始的界面,游戏结束的界面,这些的工作量其实不比之前做游戏主框架少,甚至更多,所以我就不一一讲述了,毕竟我很懒。
至此《老脸教你做游戏》就告一段落了(实际上我想写的是《老脸教你做游戏引擎》,后来我想了想,还是要点脸吧)。
如果有下期,那应该会是讲质点运动模拟,有缘的话我们会再见的。

再次感谢您的阅读!

如果你已经强打精神看到此处,说明我们有缘。我怕以后不更新blog没机会写,所以在最后我讲讲一些我觉得挺有用的东西。

图形区域划分

用过数据库吗?没用过没关系。数据库可以建立索引,便于快速定位到某个数据。
我们在未优化测试碰撞的方法中,采用了全局遍历,即用图形A和所有其他图形进行碰撞测试,这是很慢的。如果我们能快速定位到图形A和它附近的图形的话,就没必要进行全局测试了。
所以几乎所有游戏引擎都会对图形进行区域划分,就拿目前的游戏为例:


在这里插入图片描述

我们可以把整个屏幕划分成n个区域,每当有新的图形加入就计算出该图形的Bounds在哪个区域,并把该图形放到区域里图形数组里,如果图形发生变换(移动、缩放、旋转),重新计算图形所在区域并更新,这就相当于数据库里建立索引。写段伪代码:


function onNewFigureAdded(figure){
    updateFigureArea(figure);
}

function onFigureTransformChanged(figure){
    updateFigureArea(figure);
}

function updateFigureArea(figure){
    // 得到figure对应区域数组
    let areas = findFigureAreas(figure);
    // 清空figure维护的area数组
    while(figure.areaArray.length !=0){
        let areaId = figure.areaArray.pop();
        let area = getArea(areaId);
        area.remove(figure.id);
    }
    figure.areaArray.length = 0;// 清空figure所在区域id数组
    // 重新设置一遍
    while(areas.length !=0){
        let area = areas.pop();
        figure.areaArray.push(area.id);
        area.add(figure.id);
    }
}

伪代码,不要较真,是这个意思就可以了

这样一来,如果我想知道小鸟所在区域有哪些些的图形,就可以这样:

let otherFigures = [];
for(let i = 0 ; i < bird.areaArray.length; i++){
     let area = getArea(bird.areaArray[i]);
     copyAll(otherFigures,area.figures);
}

比如想要进行碰撞测试,就不再需要全局比较了,从小鸟所在区域内取出其他图形和小鸟Bounds进行测试即可。
上面例子里的区域是一个固定网格形状的,这种区域划分叫做全局网格(Uniform Grid),是二维中较为简单的区域划分方式,二维游戏引擎常用区域划分还有四叉树(Quadtrees)等,三维的就有BSD等。不管怎么去划分吧,目的就一个,快速定位图形,减少不必要的操作,我们所讲的这个游戏碰撞测试还是很简单的,如果是复杂的碰撞测试,那就会花费很多时间。一帧16毫秒是特别宝贵的,不要浪费在一些无谓的操作上。
当然,选择哪种划分方式还要看区域更新复不复杂,太复杂了也不行,本末倒置了。
下图是我做的刚体运动模拟的网格区域划分测试:


全局网格

devicePixelRatio

devicePixelRatio顾名思义,设备分辨比。
这个是DOM中windows对象的一个属性,微信小游戏里是通过SystemInfo获取的:

let systemInfo = wx.getSystemInfoSync();
let scale = systemInfo.pixelRatio;

比如你绘制一个图片在canvas上,你可能会发现在手机上比PC上模糊(像素化),这是因为devicePixelRatio的值不为1。
这个属性值的意思是说,当前显示设备的物理像素分辨率和CSS像素分辨率的比值,可以这么理解:一个CSS像素等于多少个物理像素大小,即需要多少个当前设备屏幕像素绘制一个CSS像素。
假设devicePixedlRatio为3.5的话,你绘制出来的图片在设备上显示跟实际屏幕像素就差了3.5倍,所以看着就模糊了。
所以在你用canvas绘制之前需要通过devicePixelRatio的值重新设置canvas在内存里大小:

let scale = windows.devicePixelRatio;
canvas.width *= scale;
cavans.height *= scale;

让canvas的内存大小变成之前的devicePixeloRatio倍,而且canvas的显示大小不要改变:

let scale = windows.devicePixelRatio;
canvas.style.width = canvas.width;// 微信小游戏没有style
cavnas.style.height = canvas.height;
canvas.width *= scale;
cavans.height *= scale;

而通常我们写代码的时候图形的大小都是根据canvas大小来设定的,而且是在canvas内存大小改变之前,所以如果你在代码中一直沿用了为改变内存大小的canvas长宽,那就还需要将context放大相应的倍数:

// 之前这么写的
let myFigure.width = canvas.width;
myFigure.draw();
.....
// 后来才改的内存大小
let scale = windows.devicePixelRatio;
canvas.style.width = canvas.width; // 微信小游戏没有style
cavnas.style.height = canvas.height;
canvas.width *= scale;
cavans.height *= scale;
// 那就让ctx放大scale倍
let ctx = canvas.getContext('2d');
ctx.scale(scale,scale);

这样就不会出现模糊的情况了。

图片尽量放在一起

我们绘制的图片经常都是一张一张的,实际上这样不好,尽量把他们都合到一张图片上,指定好source bounds然后绘制。
这样做一方面是为了节约空间,另一方面是为了方便加载,另外我在做webgl的时候,为了统一绘制,会将多个图片合并到一个texture中,但是不管怎么做都无法让这些图片铺满一个大的空间的,往往会生成多个texture(这也跟我的算法有关)。如果一开始要绘制的图片就是一个大的图片,就不需要来回切换了,一次性就能绘制成功。

建立对象池

如果游戏程序中会频繁生成新的图形对象,请建一个对象池,便于复用。
好多人说对象池的目的是为了减少创建对象时的开销,个人认为这是一方面。另外一方面是为了给GC省时间,太多的对象需要清扫的话GC花的时间很长,以至于出现游戏玩着玩着突然卡一下。

context 2d的clip方法少用为妙

这个方法我再web上倒没有发现问题,可是在微信小游戏里出现了性能爆降的情况。我觉得是微信小游戏的bug,于是在他们论坛提交了,不过过了很久都无人回复,我觉得很有可能是我自己代码烂造成的,于是就默默把提交的bug删了。
这是代码片段导入链接,试试在你的手机上卡不卡:
https://developers.weixin.qq.com/s/AQNhhimW7A4s

context 2d在移动端只适合简单的图形和游戏

我之前做了个打泡泡的微信小游戏,不包括背景图片,里面至少要绘制50个泡泡,而且还会动,起初在模拟器上运行得还好,而且在我的手机上运行也没问题;后来我把这个游戏移植到了web端,一到手机端FPS只有20,我忽略了一点,微信小游戏不是运行在浏览器上的,跟普通的web页面不是一回事。于是我不得不把整个游戏的绘制部分改成了webgl,花了一个月的时候才调试好。

在此提醒一下各位,如果你的程序主要是在移动端运行,开发的时候在模拟器(我是在Chrome上运行)上先将CPU降低到手机的标准,不然会误导各位,还以为程序运行得挺流畅,实际上卡成鬼。

所以说,如果只会画个报表啦,做个Diagram程序啦,canvas2d就够用了,但要想在手机端做出渲染操作比较多而且复杂的游戏(比如3d效果的),用canvas2d是不行的,请转投webgl。

游戏不分贵贱

游戏就是一个程序而已,有的凸出的是画面,有的是在音乐上下了大工夫,而有的却是在游戏剧情安排上很吸引人,各有各的受众。
有些人就喜欢绝地求生,我就不喜欢,而我很爱玩连连看,却被别人笑话幼稚。玩家即是如此,何况开发者:你用的Unreal引擎开发的3d游戏,我用的原生js开发的2d游戏,你的就要高一档次?
游戏不分贵贱的,只有喜欢和不喜欢

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容