我用js做了个超级玛丽

5,826 阅读6分钟

前言

从刚接触编程起,作者就一直有一个做游戏的梦,奈何水平比较差,做不出啥太像样的游戏,最近趁着下班和周末时间折腾折腾,怀旧一下童年最爱玩的超级玛丽。本文将讲述如何用js写一个MVP版本的超级玛丽游戏,主要用到框架有leaferjs + vueleaferjs负责做图形渲染,vue负责界面和地图编辑,leaferapi用起来还是很舒服的,学习成本低,作者也非常热情,这里帮忙推荐一下~

本文会尽可能简单的少贴代码,并把核心的流程讲清楚,先上一张运行后的效果图,源码和体验地址在最下面

开始我们的游戏梦

1、创建背景

就是游戏背后的蓝天白云背景,背景素材如下

在游戏中,背景会随人物前进而后退,但是由于素材宽度有限,不可能无限后退,所以做了个逻辑,当背景的x到达边界的时候,把x设置成0,达到类似无限循环背景的效果

这里使用leaferGroup + Rect实现,由一个Background类来实现,用leafer实现简单很多

2、创建场景

场景用来承接和绘制游戏中的精灵,所以需要有个存放精灵的数组,以及一个run方法来绘制精灵(这里使用leaferCanvas实现),具体要绘制什么,解耦给各个精灵内部自由实现

class Scene {
  sprites = [];
  
  // 添加内容
  add(sprite) {
    this.sprites.push(sprite);
  }
  
  run() {
    // 绘制内容,传入canvas上下文
    this.sprites.forEach((sprite) => {
      sprite.draw(context)
    })
    ...
  }
}

3、创建精灵

精灵是指场景中的元素,比如玛丽、砖块、成长蘑菇、敌人蘑菇等,我们可以为这些精灵创建一个基类Sprite,有xy表示位置,有widthheight表示宽高,vxvy表示水平和垂直方向速度

class Sprite {
  constructor(options) {
    const { x, y, width, height, vx, vy } = options;
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.width = width;
    this.height = height;
  }
}

场景中所有精灵都可以继承于他,同时上面也说到了绘制精灵精灵自身来决定,所以有个draw方法,且由于受到速度的影响,精灵的xy需要做更新

class SpriteMario extends Sprite {
  constructor(options) {
    super(options);
    // ...
  }
  
  draw(context) {
    this.x += this.vx;
    this.y += this.vy;
    context.drawImage(this.resource, this.x, this.y, this.width, this.height)
  }
}

精灵需要根据自身的状态在当前类内维护自己的动画帧,比如当玛丽的vx > 0,要绘制的图片就是玛丽向右走的动画帧,实现后,效果如下(这里我降低了gif的帧数,实际运行是很流畅的,下面的gif也是)

然后我们依次绘制出其他砖块、石块、道具等精灵,这里先提前说一下,背景的运动、场景的绘制、包括底下的相机跟随、物理引擎都在每一帧即requestAnimationFrame中执行,代码如下

run() {
    // 运行物理引擎
    this._physicsEngine.run({
      camera: this.camera,
      scene: this.scene,
    });

    // 运行场景中的精灵
    this.scene.run();

    // 运行背景
    this._background.run();

    // 相机跟随玛丽
    this.camera.x =
        this._mario.x < MARIO_VIEW_OFFSET
          ? 0
          : this._mario.x - MARIO_VIEW_OFFSET;

    requestAnimationFrame(this.run.bind(this));
  }

实现后,效果如下

看起来很有感觉有没有!接下来开始做物理引擎

4、物理引擎 - 重力

最先要实现的当然就是重力拉,运用初中物理知识,使用公式套进去

class PhysicsEngine {
  run (options) {
    // ...
    
    // 为了防止过快给了个10的速度阈值
    sprite.vy = Math.min(10, sprite.vy + G);
  }
}

效果如下

5、物理引擎 - 碰撞检测

加上重力后,玛丽会往下掉,然后来实现碰撞检测,这里的思路比较简单,用到的是矩形之间的碰撞检测,先校验两个矩形是否发生碰撞,如果发生碰撞在校验碰撞来源的方向,并根据方向做位置修补,防止碰撞后嵌入,感兴趣可以看看源码,加上之后的效果如下

6、物理引擎 - 跳跃

跳跃套用上抛运动公式实现,给他一个默认的初速度v0

  sprite.v0 -= G;
  sprite.vy = -sprite.v0;

然后当长按"上键"的时候,增大v0,达到按得越久跳的越高的效果,效果如下

7、物理引擎 - 其他精灵间的碰撞

这里就挑几个主要的来讲,其他实现都大同小异

7.1、人物顶到问号

问号中的道具会缓缓升起,前者的实现逻辑是给个参数active来指定当前道具精灵状态,当activefalse不会受到物理引擎影响,所以我们可以在问号被碰撞后,让道具y值不断变小,当完全露出时设置其activetrue,并给道具一个vx,效果如下

顺便说一下,当玛丽的头碰撞到建筑的底部时,会取消其上抛运动状态,使其只受重力影响

7.2、人物吃成长蘑菇

这个就比较简单了,当人物和蘑菇发生碰撞,将蘑菇销毁,人物的height变高,然后更新人物动画帧,效果如下

7.3、人物顶碎砖块

当人物的顶部碰撞到砖块的底部时,砖块销毁,然后将砖块拆成4个,向四周做抛物线运动,抛物线轨迹实现主要用一元二次方程实现,这里贴下核心代码

  this.animatedX += 2;
  this.animatedY = 0.1 * this.animatedX * this.animatedX - b * this.animatedX;

运行后,效果如下

8、引入相机

这里的相机不是webgl的相机,可以理解成视图窗口,引入这个概念的目的是为了让绘制和物理引擎的执行只有在视口内才会执行,即只会绘制相机视口的内容,然后我们让相机的x跟随玛丽,这样就能达到跟随人物前进的效果

  camera.x = mario.x

效果如下

a.gif

9、 引入分数系统

击杀怪物、吃道具、顶碎石砖,会出现数字,数字会跟随对应精灵,这里用的是leaferText很轻松就能实现,效果如下

a.gif

10、增加胜利机制

创建旗帜,当人物和旗帜发生碰撞后,判定胜利,效果如下

a.gif

11、编辑地图

游戏做完后,要开始做地图了,由于地图要一个个手动去敲xy很麻烦,就用vue做了个地图编辑器,可以通过鼠标来创建精灵自定义关卡,效果如下

a.gif

结语

上面只是一个超级玛丽的MVP版本,还有挺多没实现的,比如音乐、乌龟等等,胜利的动画也比较简陋,后续如果有空再慢慢完善他,源码已经发布github,这里是源码地址体验地址,喜欢的话求个star~

这是作者的两个公众号,喜欢可以关注下哦~

  • 生活小懒 - 用来领外卖、打车红包和网购查券的生活返利机器人
  • 前端不只是切图 - 前端公众号,不定时更新些原创文章