开发环境
VUE - 前端框架
PIXI - canvas绘制库
Dust - PIXI粒子效果库
Matter - 物理系统
PIXI 和 Matter 安装 npm 库
"dependencies": {
"matter-js": "^0.18.0",
"vue": "^3.2.13",
"vue-pixi-wrapper": "^2.3.5",
"vue-router": "^4.1.6"
},
Dust 直接引入 script 文件
<html lang="">
<head>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body style="margin: 0; position: absolute; width: 100%; height: 100%;">
<div id="app"></div>
</body>
<script src="https://www.kkkk1000.com/js/dust.js"></script>
</html>
玩法实现
实现合成大西瓜玩法的实现主要参考:使用Matter.js合成大西瓜
PIXI+Matter集成方案参考:Army老师实现的Demo
由于上文仅实现了玩法部分,没有添加例子效果和音效,所以本文主要讲一下如何集成:PIXI(绘制库) + Dust(粒子效果库) + Matter(物理系统)来实现更完整的功能。
- 游戏初始化
/**
* 初始化游戏物理世界
*/
initGame() {
PIXI.particles = {
ParticleContainer: PIXI.ParticleContainer
}
this.dust = new Dust(PIXI);
this.app = new PIXI.Application({
width: this.psdWidth, // window.innerWidth
height: this.canvasHeight, // window.innerHeight
antialias: true, // default: false 反锯齿
transparent: false, // default: false 透明度
resolution: 1, // default: 1 分辨率
backgroundAlpha: 0, // 设置背景颜色透明度 0是透明
});
this.setBackground(this.app);
// 将创建好的canvas添加到元素当中去
document.getElementById('canvas').appendChild(this.app.view);
// 初始化粒子系统
this.particleContainer = new PIXI.ParticleContainer(
1500,
{ alpha: true, scale: true, rotation: true, uvs: true }
);
this.app.stage.addChild(this.particleContainer);
// 初始化物理系统
this.engine = Matter.Engine.create({}); // 物理引擎
this.world = this.engine.world; // 物理世界,也是所有物理对象的容器
this.world.bounds = { min: { x: 0, y: 0 }, max: { x: this.psdWidth, y: this.canvasHeight } };
this.runner = Matter.Runner.create(); // 物理世界,循环状态检测
// this.render = Matter.Render.create({}); // 不需要 Matter 的渲染部分
this.mouseconstraint = Matter.MouseConstraint.create(this.engine); // 物理世界,事件监听
Matter.Engine.run(this.engine);
this.resetGame(); // 其他游戏逻辑,如设置具体监听事件,初始化第一个水果等
},
- 绑定物理系统和渲染系统
/**
* 生成新的水果
* @param {*} type - 水果种类
* @param {*} x - 水果坐标x
* @param {*} y - 水果坐标y
*/
createFruit(type, x, y) {
// 每次新增1~5号水果中的一个
const fruitNum = type || random(0, 5); // fruitNum = [0, 5)
const fruitRadius = this.fruits[fruitNum].radius; // this.fruits 存储 1~10号水果的信息
const fruit = Matter.Bodies.circle(
x || this.psdWidth /2, // this.psdWidth = 750
y || fruitRadius + 10,
fruitRadius,
{
isStatic: type ? false : true, // 合并生成的水果指定了type, isStatic 为 false; 下一个下落水果是随机type, isStatic 为 true
restitution: 0.2,
// render: {} // 不使用 Matter 的渲染库
}
);
// 把渲染系统的 PIXI.Sprite 绑定到物理系统的 fruit - Matter.Bodies.circle 上,以 fruit.pixiSprite 为链接
// 将坐标对齐
fruit.pixiSprite = PIXI.Sprite.from(this.fruits[fruitNum].image)
fruit.pixiSprite.x = fruit.position.x;
fruit.pixiSprite.y = fruit.position.y;
fruit.pixiSprite.anchor.set(0.5, 0.5); // 设置精灵的锚点居中, 使得 PIXI.Sprite.x 与 Matter.Bodies.circle.x 相同
this.app.stage.addChild(fruit.pixiSprite); // 渲染系统添加水果
fruit.fruitId = uuid(); // 当前水果ID,用于合并后销毁水果的判断
fruit.fruitType = fruitNum; // 水果类型,用于合并时获取下个类型的水果
Matter.World.add(this.world, fruit); // 物理系统添加水果
this.fruitsOnScreen.push(fruit); // 收集所以物理系统中的水果
return fruit;
},
- 循环更新
由于我们要替换 Matter 内置的绘制库为 PIXI,所以无法利用 Matter 内部的循环渲染,需要自己实现循环检测游戏状态实时更新渲染的功能。
/**
* 游戏循环
*/
gameLoop() {
// 1. 物理系统和渲染系统的位置对齐
// this.fruitsOnScreen 存储 Matter 物理系统生成的每个水果
// Matter 物理系统在循环的每个时间段会更新系统中水果因重力和碰撞产生的坐标变化
// 所以循环的每个时间段都将物理系统中的坐标更新到 PIXI 渲染系统
// 游戏界面的水果就会产生物理运动
for(let i = 0; i < this.fruitsOnScreen.length; i++) {
let fruit = this.fruitsOnScreen[i];
fruit.pixiSprite.x = fruit.position.x;
fruit.pixiSprite.y = fruit.position.y;
}
// 2. 更新例子粒子系统
this.dust.update();
// 每帧时间差
let now = Date.now();
let diff = now - this.lastTime;
this.lastTime = now;
// 3. 物理引擎更新时间状态
Matter.Runner.tick(this.runner, this.engine, diff);
// 4. 继续循环
window.requestAnimationFrame(this.gameLoop);
},
- 合并粒子效果
/**
* 碰撞事件
*/
collisionEvent(e){
if (this.status !== -1) return;
const { pairs } = e;
for(let i = 0; i < pairs.length; i++ ){
const { bodyA, bodyB } = pairs[i];
// 检测水果 bodyA 运动后的游戏状态是否已达成结束条件
this.gameProgressChecking(bodyA);
// 检测水果 bodyB 运动后的游戏状态是否已达成结束条件
this.gameProgressChecking(bodyB);
// bodyA 和 bodyB 两个水果符合合并条件,类型相同且碰撞
if(bodyA?.fruitType >= 0 && bodyA.fruitType === bodyB.fruitType && !bodyA.hasDeleted && !bodyB.hasDeleted){
const type = bodyA.fruitType + 1;
if (type >= this.fruits.length) {
this.status = 1;
return;
}
const targetType = bodyA.fruitType;
// 如果下落水果的类型和要碰撞的左右两个水果相同
// 避免下落水果同时和左右两个相同水果碰撞,导致两个水果同时变成新的水果
// 由于 bodyA 和 bodyB 即将销毁,而下落水果仍要存在参与后面的计算
// 所以修改 bodyA 和 bodyB 的类型以避免上述情况
bodyA.fruitType = -2;
bodyB.fruitType = -2;
const { position: { y: ay } } = bodyA;
const { position: { y: by } } = bodyB;
let target = null;
if (ay > by) {
target = bodyA;
} else {
target = bodyB;
}
const { position: { x: targetX, y: targetY } } = target;
// 爆炸例子效果
this.dust.create(
targetX, // x start position
targetY, // y start position
() => new PIXI.Sprite.from(this.fruits[targetType].fragment), // Sprite function
this.particleContainer, // Container for particles
15, // Number of particles
0.05, // Gravity
false, // Random spacing
0, 6.28, // Min/max angle
this.fruits[targetType].radius, this.fruits[targetType].radius + 30, // Min/max size
1, 2, // Min/max speed
);
const radius = this.fruits[type].radius;
const targetRadius = this.fruits[targetType].radius;
const x = targetX;
const y = targetY - (radius - targetRadius);
this.createBom(targetType, x, y); // 爆炸音效和爆炸背景现实 this.combineMusic.play();
// 从游戏存在的水果组 this.fruitsOnScreen 中删除合并的两个水果
this.fruitsOnScreen = this.fruitsOnScreen.filter(f => f.fruitId !== bodyA.fruitId && f.fruitId !== bodyB.fruitId);
// 从 PIXI 渲染系统中删除合并的两个水果
this.app.stage.removeChild(bodyA.pixiSprite);
this.app.stage.removeChild(bodyB.pixiSprite);
// 从 Matter 物理系统中删除合并的两个水果
Matter.World.remove(this.world, bodyA);
Matter.World.remove(this.world, bodyB);
}
}
},
其余的游戏玩法实现可以参考上面引用文章的实现。
其他
Demo体验地址:www.miraclegarden.cn/#/fruit
Demo代码实现:github.com/theforeverh…