關注 Matter.js 好久了,但一直沒有機會使用過,恰好最近《合成大西瓜》這個遊戲比較熱門,就用 Matter.js 來試着實現了一下。javascript
點擊試玩 或者掃描下方二維碼瀏覽:html
《合成大西瓜》這款小遊戲,核心的原理就是物理引擎的使用。而 Matter.js 的簡介是這樣的:java
Matter.js 是一個2D 剛體物理引擎。git
Matter.js 引擎內置了物體的運動規律和碰撞檢測,所以經過它來實現這個遊戲,也僅僅就是一個 API 使用的過程。github
在實現的過程當中,我將功能分紅了五部分,分別爲:場景初始化、建立小球、給小球添加事件、碰撞檢測以及遊戲結束的檢測。canvas
核心功能僅展現核心代碼,完整代碼在文末附出。數組
場景初始化這部分,主要是學習 Matter.js 的大框架,按照官網的指引分別配置 Engine
、Render
和 World
:markdown
Engine
是 Matter.js 中物理引擎的配置部分,初始化它,也就給物體添加好了引擎;Render
和其餘引擎相似,是畫布的渲染,畫布的尺寸、背景顏色等繪製相關的內容再次配置;World
在 Matter.js 中是相似與舞臺,全部要展示出來的內容,都須要添加到 World
當中。// Engine 初始化
this.engine = Matter.Engine.create({
enableSleeping: true // 在遊戲結束檢測的時候,用到該 sleep 功能,enableSleeping 爲 true 能夠檢測到小球中止運動的狀態,從而方便進行遊戲結束的檢測。
});
// World 初始化
this.world = this.engine.world;
this.world.bounds = { min: { x: 0, y: 0}, max: { x: window.innerWidth, y: window.innerHeight } };
// Render 初始化
this.render = Matter.Render.create({
canvas: document.getElementById('canvas'),
engine: this.engine,
options: {
width: window.innerWidth,
height: window.innerHeight,
wireframes: false, // 設爲 false 後,才能夠展示出添加到小球的紋理
background :"#ffe89d",
showSleeping: false, // 隱藏 sleep 半透明的狀態
},
});
// 這裏使用內置的長方形物體建立了遊戲的牆壁和地面
// 建立地面
const ground = Matter.Bodies.rectangle(window.innerWidth / 2, window.innerHeight - 120 / 2, window.innerWidth, 120, {
isStatic: true, // true 可將物體做爲牆壁或者地面,將不會有重力等物理屬性
render: {
fillStyle: '#7b5438', // 地面背景顏色
}
});
// 左牆
const leftWall = Matter.Bodies.rectangle(-10/2, canvasHeight/2, 10, canvasHeight, { isStatic: true });
// 右牆
const rightWall = Matter.Bodies.rectangle(10/2 + psdWidth, canvasHeight/2, 10, canvasHeight, { isStatic: true });
// 將建立的物體添加到 World
Matter.World.add(this.world, [ground, leftWall, rightWall]);
// 運行引擎與渲染器
Matter.Engine.run(this.engine);
Matter.Render.run(this.render);
複製代碼
在遊戲當中,小球默認懸浮在頁面最上面,點擊或者左右滑動到指定位置,小球脫落,脫落後延時一段時間再次建立一個小球。框架
在這裏,能夠將建立一個方法:建立一個球形物體,指定他出現的位置,並賦予貼圖。dom
// 球體半徑的合集
const radius = [52/2, 80/2, 108/2, 118/2, 152/2, 184/2, 194/2, 258/2, 308/2, 310/2, 408/2];
// 小球紋理數組
const assets = ['./assets/1.png',.....];
// 小球出現的次數,能夠根據次數的累計增長遊戲難度,或者用於計算分數。
let circleAmount = 0;
// 添加球體
addCircle(){
// 隨機一個半徑
const radiusTemp = radius.slice(0, 6);
const index = circleAmount === 0 ? 0 : (Math.random() * radiusTemp.length | 0);
const circleRadius = radiusTemp[index];
// 建立一個球體
this.circle = Matter.Bodies.circle(
window.innerWidth /2, // 小球的 x 座標,這裏是根據小球的圓心來定位的
circleRadius + 30, // 小球的 y 座標,將初始化的小球安置在水平居中,距離頂部 30 像素的位置
circleRadius, {
isStatic: true, // 首先設置爲 true ,在觸發事件以後再改成 false ,給小球添加下落動做
restitution: 0.2, // 設置小球彈性
render: {
sprite: {
texture: assets[index], // 給小球設置紋理
}
}
}
);
// 將小球添加到 World
Matter.World.add(this.world, this.circle);
// 遊戲狀態檢測,後續會說明
this.gameProgressChecking(this.circle);
circleAmount++;
}
複製代碼
初始化後的小球,有兩種狀況下落,一種是點擊任意一處,根據 x 座標下落,二是手指觸摸,滑動小球到指定位置,手指擡起下落。
// 使用 Matter.js 內置的 MouseConstraint 和 Events 就能夠實現 touch 事件
const mouseconstraint = Matter.MouseConstraint.create(this.engine);
// touchmove 事件
Matter.Events.on(mouseconstraint, "mousemove", (e)=>{
if(!this.circle || !this.canPlay) return; // this.canPlay 判斷遊戲是否結束
this.updateCirclePosition(e); // 在 touchmove 中更新小球的 x 座標
})
// touchend 事件
Matter.Events.on(mouseconstraint, "mouseup", (e)=>{
if(!this.circle || !this.canPlay) return;
this.updateCirclePosition(e);
Matter.Sleeping.set(this.circle, false); // 接觸小球的 sleep 模式,以便添加物理屬性
Matter.Body.setStatic(this.circle, false ); // 給小球激活物理屬性,小球會由於重力自動落下
this.circle = null;
setTimeout(()=>{ // 延遲 1s 後再次建立小球
this.addCircle();
}, 1000);
});
// 更新小球的 x 座標
updateCirclePosition(e){
const xTemp = e.mouse.absolute.x;
const radius = this.circle.circleRadius;
Matter.Body.setPosition(this.circle, {x: xTemp < radius ? radius : xTemp + radius > psdWidth ? psdWidth - radius : xTemp, y: radius + 30});
}
複製代碼
遊戲中最吸引人的部分,就是兩個相同的水果接觸會變成一個更大的水果。在此功能部分,咱們以來 Matter.js 內置的碰撞檢測,只需判斷碰撞的兩個小球半徑是否一致便可,若是半徑一致,就變成一個更大半徑的小球。
Matter.Events.on(this.engine, "collisionStart", e => this.collisionEvent(e)); // 下落的小球剛碰撞在一塊兒的事件
Matter.Events.on(this.engine, "collisionActive", e => this.collisionEvent(e)); // 其餘被動的小球相互碰撞的事件
collisionEvent(e){
if(!this.canPlay) return;
const { pairs } = e; // pairs 爲全部小球碰撞的集合,經過遍歷該集合中參與碰撞的小球半徑,就完成了邏輯判斷
Matter.Sleeping.afterCollisions(pairs); // 將參與碰撞的小球從休眠中激活
for(let i = 0; i < pairs.length; i++ ){
const {bodyA, bodyB} = pairs[i]; // 拿到參與碰撞的小球
if(bodyA.circleRadius && bodyA.circleRadius == bodyB.circleRadius){ // 小球半徑一致,變成更大的小球
const { position: { x: bx, y: by }, circleRadius, } = bodyA; // 獲取兩個相同半徑的小球,取中間位置合成大球
const { position: { x: ax, y: ay } } = bodyB;
const x = (ax + bx) / 2;
const y = (ay + by) / 2;
const index = radius.indexOf(circleRadius)+1;
const circleNew = Matter.Bodies.circle(x, y, radius[index],{ // 建立大的球
restitution: 0.2,
render: {
sprite: {
texture: this.assets[index],
}
}
});
Matter.World.remove(this.world, bodyA); // 移除兩個碰撞的小球
Matter.World.remove(this.world, bodyB);
Matter.World.add(this.world, circleNew); // 將生成的大球加入到 World
this.gameProgressChecking(circleNew); // 判斷遊戲的狀態
}
}
}
複製代碼
Matter.js 中提供了小球是否運動中止,也就是 Sleep
狀態,咱們只需判斷最近添加到 World
的小球位置,是否溢出了遊戲區域便可,若是 y 座標溢出了遊戲區域,則遊戲就結束了。
// gameProgressChecking 在上文小球開始掉落的時候開始觸發
gameProgressChecking(body){
Matter.Events.on(body, 'sleepStart', (event)=> {
if (!event.source.isStatic && event.source.position.y <= 300) { // 若是小球靜止時,y 座標移除遊戲區域,遊戲結束
this.gameOver();
}
})
}
複製代碼
以上,就完成了《合成大西瓜》的核心功能,藉助 Matter.js
,讓咱們節省了大量的時間去研究小球間的物理關係,讓咱們站在巨人的肩膀上快速的完成了遊戲的開發。
TypeScript 源碼:github.com/ningbonb/de…
// JS 源碼
// 重命名
const Engine = window['Matter'].Engine,
Render = window['Matter'].Render,
World = window['Matter'].World,
Bodies = window['Matter'].Bodies,
Body = window['Matter'].Body,
MouseConstraint = window['Matter'].MouseConstraint,
Sleeping = window['Matter'].Sleeping,
Events = window['Matter'].Events;
// 基本數據
const psdWidth = 750,
canvasHeight = window.innerHeight * psdWidth / window.innerWidth,
radius = [52/2, 80/2, 108/2, 118/2, 152/2, 184/2, 194/2, 258/2, 308/2, 310/2, 408/2];
export default class MatterClass{
constructor(prop) {
this.canvas = prop.canvas;
this.assets = prop.assets;
this.gameOverCallback = prop.gameOverCallback;
this.circle = null;
this.circleAmount = 0;
this.canPlay = true;
this.init();
this.addCircle();
this.addEvents();
}
// 場景初始化
init(){
this.engine = Engine.create({
enableSleeping: true
});
this.world = this.engine.world;
this.world.bounds = { min: { x: 0, y: 0}, max: { x: psdWidth, y: canvasHeight } };
this.render = Render.create({
canvas: this.canvas,
engine: this.engine,
options: {
width: psdWidth,
height: canvasHeight,
wireframes: false,
background :"#ffe89d",
showSleeping: false,
},
});
const ground = Bodies.rectangle(psdWidth / 2, canvasHeight - 120 / 2, psdWidth, 120, { isStatic: true,
render: {
fillStyle: '#7b5438',
}
});
const leftWall = Bodies.rectangle(-10/2, canvasHeight/2, 10, canvasHeight, { isStatic: true });
const rightWall = Bodies.rectangle(10/2 + psdWidth, canvasHeight/2, 10, canvasHeight, { isStatic: true });
World.add(this.world, [ground, leftWall, rightWall]);
Engine.run(this.engine);
Render.run(this.render);
}
// 添加球體
addCircle(){
const radiusTemp = radius.slice(0, 6);
const index = this.circleAmount === 0 ? 0 : (Math.random() * radiusTemp.length | 0);
const circleRadius = radiusTemp[index];
this.circle = Bodies.circle(psdWidth /2, circleRadius + 30, circleRadius, {
isStatic: true,
restitution: 0.2,
render: {
sprite: {
texture: this.assets[index],
}
}
}
);
World.add(this.world, this.circle);
this.gameProgressChecking(this.circle);
this.circleAmount++;
}
// 添加事件
addEvents(){
const mouseconstraint = MouseConstraint.create(this.engine);
Events.on(mouseconstraint, "mousemove", (e)=>{
if(!this.circle || !this.canPlay) return;
this.updateCirclePosition(e);
})
Events.on(mouseconstraint, "mouseup", (e)=>{
if(!this.circle || !this.canPlay) return;
this.updateCirclePosition(e);
Sleeping.set(this.circle, false);
Body.setStatic(this.circle, false );
this.circle = null;
setTimeout(()=>{
this.addCircle();
}, 1000);
});
Events.on(this.engine, "collisionStart", e => this.collisionEvent(e));
Events.on(this.engine, "collisionActive", e => this.collisionEvent(e));
}
// 碰撞檢測
collisionEvent(e){
if(!this.canPlay) return;
const { pairs } = e;
Sleeping.afterCollisions(pairs);
for(let i = 0; i < pairs.length; i++ ){
const {bodyA, bodyB} = pairs[i];
if(bodyA.circleRadius && bodyA.circleRadius == bodyB.circleRadius){
const { position: { x: bx, y: by }, circleRadius, } = bodyA;
const { position: { x: ax, y: ay } } = bodyB;
const x = (ax + bx) / 2;
const y = (ay + by) / 2;
const index = radius.indexOf(circleRadius)+1;
const circleNew = Bodies.circle(x, y, radius[index],{
restitution: 0.2,
render: {
sprite: {
texture: this.assets[index],
}
}
});
World.remove(this.world, bodyA);
World.remove(this.world, bodyB);
World.add(this.world, circleNew);
this.gameProgressChecking(circleNew);
}
}
}
// 更新小球位置
updateCirclePosition(e){
const xTemp = e.mouse.absolute.x * psdWidth / window.innerWidth;
const radius = this.circle.circleRadius;
Body.setPosition(this.circle, {x: xTemp < radius ? radius : xTemp + radius > psdWidth ? psdWidth - radius : xTemp, y: radius + 30});
}
// 遊戲狀態檢測
gameProgressChecking(body){
Events.on(body, 'sleepStart', (event)=> {
if (!event.source.isStatic && event.source.position.y <= 300) {
this.gameOver();
}
})
}
// 遊戲結束
gameOver(){
this.canPlay = false;
this.gameOverCallback();
}
}
複製代碼
使用方法:
import MatterClass from './matter.js';
const matterObj = new MatterClass({
canvas: document.getElementById('canvas'), // canvas 元素
assets: ['../assets/0.png',...], // 紋理合集
gameOverCallback: ()=>{ // 失敗回調
}
});
複製代碼