几十行JS代码简单编写一个小游戏「寻找掘金酱」
我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
前言
如你所见,这是一个萌系休闲类小游戏,应该非常适合在深夜里一个人打发寂寞时光!(查询作者精神状态)
游戏是这样的,通过控制鼠标可以在这个被黑夜笼罩的都市中打开一束光,照亮某片区域,玩家要尽可能快地寻找到 掘金酱 的身影,鼠标只要命中即为游戏结束,此时如果继续滑动鼠标则会看到 掘金酱 向你鬼畜而来.....(期初可能只是一个BUG,但我觉得挺有趣的就保留了下来,我们通常应该可以将此类事件称之为——"创意")
本游戏采用 melonJS 2 进行开发,melonJS 2 是 melonJS 游戏引擎的现代版本。它几乎完全使用 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精灵技术是同种原理,不过这里我们并不做到那么复杂,只是静态显示。
我们继续丰富场景,作为游戏中的上帝,怎么能没有光呢?场景继承的基类 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);
});
动起来了,是不是很简单?
最后为场景添加一个纯黑遮罩,营造出一点氛围感~就是开头看到的效果
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 ,否则我们的 掘金酱 会像这样掉下去(原谅我不厚道地笑了):
由于我们的光源并没有物理模型,那要怎么让鼠标和掘金酱之间产生碰撞呢?这里我取巧了一下,利用 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)
}
});
结束场景
这个场景就蛮简单的了,就是输出文字内容,代码很好理解:
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,做出更好玩的东西~