几十行JS代码简单编写一个小游戏「寻找掘金酱」

1,724

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

前言

如你所见,这是一个萌系休闲类小游戏,应该非常适合在深夜里一个人打发寂寞时光!(查询作者精神状态

游戏是这样的,通过控制鼠标可以在这个被黑夜笼罩的都市中打开一束光,照亮某片区域,玩家要尽可能快地寻找到 掘金酱 的身影,鼠标只要命中即为游戏结束,此时如果继续滑动鼠标则会看到 掘金酱 向你鬼畜而来.....(期初可能只是一个BUG,但我觉得挺有趣的就保留了下来,我们通常应该可以将此类事件称之为——"创意")

本游戏采用 melonJS 2 进行开发,melonJS 2melonJS 游戏引擎的现代版本。它几乎完全使用 ES6 的类、继承和语义等进行了重建,并使用 Rollup 打包以提供现代功能。了解更多可以查看我的这篇文章: 全新轻量级 2D 开源游戏引擎,采用现代化构建,只需要会使用 JS(ES6语法) 即可开始编写游戏,接下来进入正题。

创建场景

import * as me from "https://esm.run/melonjs";

me.device.onReady(function () {
    // 初始化
    if (!me.video.init(728, 360, { parent: "screen", scaleMethod: "flex-width", renderer: me.video.WEBGL })) {
        return;
    }
    // 注册事件
    me.state.set(me.state.PLAY, new PlayScreen());
    me.state.set(me.state.GAME_END, new EndingScreen());
    // 加载资源
    me.loader.crossOrigin = "anonymous" // 这里因为我加载的是网络资源
    me.loader.preload(resource, () => {
        me.state.change(me.state.PLAY);
        startTime = new Date().getTime()
    });
});

var resource = [{ name: "background", type: "image", src: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33127b0ebc424d188c048574fa8f4dc0~tplv-k3u1fbpfcp-watermark.image?" }, { name: "jjj", type: "image", src: "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf2e426ed75a4df099433c8a169cf029~tplv-k3u1fbpfcp-watermark.image?" }]
var isEnd = false // 结束标识
var startTime = 0 // 开始时间戳
var endTime = 0 // 结束时间戳

在设备与引擎准备完毕时,会触发 onReady 回调,这里我们先初始化一个画布,renderer 可以改变渲染器的方式,默认是 Canvas,因为我使用到了2D点光源的效果,所以改成在 WebGL 渲染更好。

me.state 命名空间是重要的一个概念,它用来设置和改变游戏中的生命周期状态,比如 暂停游戏开始/结束游戏进入菜单等等,这里通过 set 方法分别设置了游戏启动时的场景实例和游戏结束时的场景实例。

loader.preload 是用于预加载资源的方法,资源通过对象数组注入,其中name参数标识了对应资源的名称,后续引用资源不需要变量,可以直接使用名称就能找到对应资源。当资源加载完毕后,触发回调函数,回调中修改状态来开始游戏,并记录下一个时间戳,用于过程中统计游戏进行的时间。

下面我们开始为游戏编写第一个场景。

游戏场景

class PlayScreen extends me.Stage {
    onResetEvent() {
        // 背景元素
        var bg_sprite = new me.Sprite( me.game.viewport.width / 2, me.game.viewport.height / 2, { image: "background", anchorPoint: { x: 0.5, y: 0.5 }} );
        // 添加目标
        var target_sprite = new me.Sprite(point.x, point.y, { image: "jjj" });
        // 添加元素进画布
        me.game.world.addChild(bg_sprite);
        me.game.world.addChild(target_sprite);
    }
};

通过 Sprite 对象创建精灵图,在2D游戏中,通常以一张顺序包含帧动画的图片来制作动态的图像,没错,就跟CSS精灵技术是同种原理,不过这里我们并不做到那么复杂,只是静态显示。

image.png

我们继续丰富场景,作为游戏中的上帝,怎么能没有光呢?场景继承的基类 Stage 中有一个 lights 属性用于设置光源列表,我们找到一个 Light2d 聚光灯的类,实例化一个灯光系统设置进光源列表中,这样我们的场景中就有了一束光:

// 灯光系统
var whiteLight = new me.Light2d(0, 0, 100, 70, "#fff", 0.7);
// 设置灯光
this.lights.set("whiteLight", whiteLight);

现在该让光束随着鼠标移动起来了,你完全可以使用 DOM 的监听事件来做,当然melonJS下同样内置了许多输入监听事件,这里的 pointermove 事件是不是跟 document 中的 mousemove 事件很类似?只不过它以传入第二个参数的方式来设置监听范围:

// 光随着鼠标事件移动
me.input.registerPointerEvent("pointermove", me.game.viewport, (event) => {
    whiteLight.centerOn(event.gameX, event.gameY);
});

动起来了,是不是很简单?

2022-09-09 09.41.42.gif

最后为场景添加一个纯黑遮罩,营造出一点氛围感~就是开头看到的效果

this.ambientLight.parseCSS("#000");

创建角色

上面我们往游戏中添加了静态的精灵图,但是游戏需要交互动作才能进行下去,这时我们就需要创建一个新的类继承精灵图,就叫它 Actor 好了,接着扩展一下这个类,这里我们使用游戏引擎提供的物理模型 Ellipse 对象,只是单纯为了添加一个椭圆作为物理身体,参数比较随意,然后设置了这个类的碰撞事件,在触发碰撞检测时执行游戏结束的相关动作。

class Actor extends me.Sprite {
    constructor() {
        super(me.Math.random(-15, me.game.viewport.width), me.Math.random(-15, me.game.viewport.height), { image: "jjj" });
        // 为角色设置身体
        this.body = new me.Body(this, new me.Ellipse(6, 6, this.width - 6, this.height - 6));
        this.body.gravityScale = 0; // 消除掉重力
    }
    onCollision() {
        // 标记游戏结束,在鼠标移动事件中会读取该全局变量进行判断
        isEnd = true
        // 记录下游戏结束时间,计算游戏时长
        endTime = new Date().getTime()
        // 改变游戏场景,进入 GAME END 游戏结束场景
        me.state.change(me.state.GAME_END)
        return false;
    }
};

对于游戏引擎中的物理模型来说,通常都会有一个重力属性,我们的游戏本质还是静态的角色,所以这里需要把重力 gravityScale 设置为 0 ,否则我们的 掘金酱 会像这样掉下去(原谅我不厚道地笑了):

2022-09-09 12.52.51.gif

由于我们的光源并没有物理模型,那要怎么让鼠标和掘金酱之间产生碰撞呢?这里我取巧了一下,利用 Actor 类,创建了一个"小掘金酱",让它跟随鼠标移动,然后隐藏它,这样就能触发物理碰撞的判定了(画外音:这个类取名 Actor 原来是这个意思吗!):

const point_sprite = me.game.world.addChild(new Actor());
point_sprite.scale(0.5) // 缩小一点
point_sprite.setOpacity(0) // 变成透明
// 鼠标移动事件:
me.input.registerPointerEvent("pointermove", me.game.viewport, (event) => {
    if (!isEnd) {
        // 移动光源
        whiteLight.centerOn(event.gameX, event.gameY);
        // 移动透明的物理模型,把它当成鼠标指针
        point_sprite.centerOn(event.gameX + 22, event.gameY + 22);
    } else {
        target_sprite.setOpacity(0.7)
        target_sprite.scale(1.02)
    }
});

image.png

结束场景

这个场景就蛮简单的了,就是输出文字内容,代码很好理解:

class EndingScreen extends me.Stage {
    onResetEvent() {
        me.game.world.addChild(new me.Text(me.game.viewport.width / 2, me.game.viewport.height / 2 - 20, {
            font: "Arial",
            size: 50,
            fillStyle: "#FFFFFF",
            textAlign: "center",
            text: "恭喜你找到了掘金酱!\n通关时间:" + ((endTime - startTime) / 1000).toFixed(2) + '秒'
        }));
    }
}

完整的代码和游戏演示

完整的代码和游戏演示(由于引用资源第一次加载可能需要等待时间),因为懒没有做游戏界面,所以猛戳上面的 ○ 运行 按钮来重复开始游戏:

试玩一下吧!看看你最快多少秒可以抓住掘金酱?

结束

总结一下,利用 melonJS 我们仅用了几十行代码就完成了一个小游戏,虽然这个游戏并不复杂,即使用原生 JS 可能也不难实现,但你却很难自己轻易实现一个 WebGL / Canvas 级别的渲染器,使用游戏引擎可以做到更多,这里只是现学现卖小试了一下牛刀,顺便也可以练习 ES6 语法,如果你感兴趣,也可以仔细参阅官方的API文档和Demo,做出更好玩的东西~