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

语言: CN / TW / HK

theme: fancy highlight: a11y-dark


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

前言

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

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

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

创建场景

```js import * as me from "http://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: "http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33127b0ebc424d188c048574fa8f4dc0~tplv-k3u1fbpfcp-watermark.image?" }, { name: "jjj", type: "image", src: "http://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参数标识了对应资源的名称,后续引用资源不需要变量,可以直接使用名称就能找到对应资源。当资源加载完毕后,触发回调函数,回调中修改状态来开始游戏,并记录下一个时间戳,用于过程中统计游戏进行的时间。

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

游戏场景

js 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 聚光灯的类,实例化一个灯光系统设置进光源列表中,这样我们的场景中就有了一束光:

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

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

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

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

2022-09-09 09.41.42.gif

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

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

创建角色

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

js 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 原来是这个意思吗!~~):

js 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

结束场景

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

js 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,做出更好玩的东西~

最后建议码上掘金的编辑器后续可以考虑做个文件目录的功能,以支持组件/小文件引用。目前写写内容少的小网页还行,稍微复杂点的拆不了组件,做这类演示项目时资源也不知道要放哪里(比如引入图片、JSON文件啥的),本文中引用到的图片资源我还是传到文章里再隐藏的,哈哈。

image.png