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

本文介绍了如何在微信小游戏上实现小鸟飞过障碍物的游戏,包括最基础的碰撞判定、Figure类的优化、性能优化等。通过Bounds接触判定实现小鸟与树干的碰撞检测,并通过优化绘制和碰撞测试提高游戏性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

本文不允许任何形式的转载!
阅读提示

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

  • 想要学习利用游戏引擎开发游戏的朋友。本文不会涉及任何第三方游戏引擎。
  • 不具备面向对象编程经验的朋友。本文的语言主要是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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值