matter.js 是什麼?
matter.js 是一個輕量級的 2D 物理引擎,它可以幫助你在網頁上製作出各種有趣的物理效果,例如碰撞、重力、彈力等等。這個物理引擎的特色在於它的易用性,只需要幾行程式碼,就可以讓你的網頁元素擁有物理效果,並且可以透過它的 API 來控制物理效果的各種參數。
西瓜遊戲是什麼?
西瓜遊戲是一款簡單的休閒遊戲,玩家的目標是通過將相同大小的西瓜合成,來製作出越來越大的西瓜。玩家只需拖動或點擊螢幕來控制小西瓜落下的位置,當兩個相同大小的西瓜碰撞時,就會合成為一個更大的西瓜。
這是一個成品的DEMO,你可以點擊這個連結來體驗一下這款遊戲,或是到GitHub上下載這個遊戲的原始碼。
製作遊戲素材
在製作西瓜遊戲之前,需要先製作遊戲的素材,而我在這裡使用了 Recraft: AI Image Generator 來產生遊戲素材,再將素材使用這個網站將圖片裁切成圓形後,套用到遊戲中。
遊戲介面
這個遊戲的介面很單純,只有一個遊戲畫面和一個分數顯示區域。可以參考以下的HTML來製作遊戲的介面。
1 2 3 4 5 6 7 8
| <body> <p id="score"> <span>SCORE:</span> <span class="num">0</span> </p> <div id="scene"></div> </body>
|
安裝 matter.js
首先,需要安裝 matter.js,可以透過 npm 來安裝 matter.js。
你可以使用vite、webpack等工具來建立專案,這裡使用 vite 來建立專案。
建立vite專案請參考vite官方文檔。
在進入點main.js中引入matter.js
1
| import Matter from 'matter-js';
|
引入遊戲素材
將遊戲素材引入到專案中,並使用預加載的方式來引入圖片。在這裡我使用了11不同尺寸的圖片,分別對應遊戲內11種不同的圓形的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import t1 from './assets/images/t1.png'; import t2 from './assets/images/t2.png'; import t3 from './assets/images/t3.png'; import t4 from './assets/images/t4.png'; import t5 from './assets/images/t5.png'; import t6 from './assets/images/t6.png'; import t7 from './assets/images/t7.png'; import t8 from './assets/images/t8.png'; import t9 from './assets/images/t9.png'; import t10 from './assets/images/t10.png'; import t11 from './assets/images/t11.png';
const imgUrl = [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11];
imgUrl.forEach((url) => { const img = new Image(); img.src = url; });
|
初始化 matter.js
在初始化 matter.js 之前,需要先了解 matter.js 的一些基本概念。matter.js 中有許多重要的物件,例如 Engine、World、Bodies、Body 等等,這些物件都是 matter.js 中的重要概念,需要了解這些物件的作用,才能夠正確地使用 matter.js 來製作遊戲。
在這個遊戲中,使用到 Engine, Render, Composite, Bodies, Body, Runner, Events 這些物件來製作遊戲,以下是這些物件的簡單介紹。
- Engine: 用來創建一個物理引擎,並且可以透過它來控制物理效果的各種參數。
- Render: 用來創建一個渲染器,並且可以透過它來渲染物理效果。
- Composite: 用來創建一個物理世界,並且可以透過它來控制物理世界中的各種物體。
- Bodies: 用來創建各種不同形狀的物體,例如圓形、矩形、多邊形等等。
- Body: 用來控制物體的各種參數,例如位置、速度、角度等等。
- Runner: 用來創建一個運行器,並且可以透過它來運行物理引擎。
- Events: 用來創建一個事件監聽器,並且可以透過它來監聽物理世界中的各種事件。
在一開始,需要先使用Engine來創建一個物理引擎,接著使用Render來創建一個渲染器,然後使用Composite來創建一個物理世界,最後使用Runner來創建一個運行器,並且將物理引擎和渲染器連接起來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| const width = 900; const height = 900; const limitY = 140;
const { Engine, Render, Composite, Bodies, Body, Runner, Events, } = Matter; let isGameOver = false;
const score = { num: 0, get value() { return this.num; }, set value(newValue) { this.num = newValue; document.querySelector('#score .num').textContent = this.num; document.querySelector('#gameOverScore').textContent = this.num; }, };
const engine = Engine.create();
Engine.update(engine, 10); const { world } = engine;
world.gravity.y = 1.5;
const render = Render.create({ element: scene, engine, options: { width, height, wireframes: false, background: 'transparent', }, });
|
建立遊戲物體
在 matter.js 中,可以使用Bodies來創建各種不同形狀的物體,例如圓形、矩形、多邊形等等。在這個遊戲中,需要創建放置球體的牆面,以及球體本身。可以使用Bodies.rectangle創建牆面,並且使用Bodies.circle創建球體。
除此之外,還需要使用collisionFilter來設定碰撞的分組,讓最上方的球體不會與其他球體碰撞。
首先使用Bodies.rectangle,建立左、右、底部的牆面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| const categoryOn = 0x0001; const categoryOff = 0x0002;
const wallLeft = Bodies.rectangle(-10, height / 2, 20, height, { isStatic: true, render: { fillStyle: '#000000', }, collisionFilter: { group: 1, category: categoryOn, mask: categoryOn, }, }); const wallRight = Bodies.rectangle(width + 10, height / 2, 20, height, { isStatic: true, render: { fillStyle: '#000000', }, collisionFilter: { group: 1, category: categoryOn, mask: categoryOn, }, }); const wallBottom = Bodies.rectangle(width / 2, height + 10, width, 20, { isStatic: true, render: { fillStyle: '#000000', }, collisionFilter: { group: 1, category: categoryOn, mask: categoryOn, }, });
Composite.add(world, [wallLeft, wallRight, wallBottom]);
|
接著使用Bodies.circle創建球體的function,用來創造不同等級或狀態的球體。
這個function的參數有level、isStatic、x、y、canCollision,分別代表球體的等級、是否靜態、x軸位置、y軸位置、是否可以碰撞。
物體建立後,執行渲染器和運行器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
|
const createBall = (level, isStatic = true, x = 0, y = null, canCollision = true) => { const scale = 0.85; const sizes = [30, 45, 60, 80, 100, 120, 140, 160, 180, 200, 220]; const size = sizes[level] * scale; const ball = Bodies.circle(x, y ?? limitY, size, { label: 'ball', restitution: 0.1, density: 0.005, airFriction: 0, mass: 5, level, render: { sprite: { texture: imgUrl[level], xScale: scale, yScale: scale, }, }, collisionFilter: { group: canCollision ? 1 : -1, category: canCollision ? categoryOn : categoryOff, mask: canCollision ? categoryOn : categoryOff, }, });
Composite.add(world, ball);
if (isStatic) { Body.setStatic(ball, isStatic); }
return ball; };
let holdBall = createBall(0, true, render.options.width / 2, limitY, false);
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
|
遊戲事件
在遊戲中,需要監聽一些事件,例如滑鼠拖動、滑鼠點擊、碰撞等等。
matter.js 提供了Events物件,可以用來監聽物體碰撞、畫布更新等等事件。
首先,為了讓玩家能夠操作,需要先註冊移動、放開事件的事件,詳情如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
const handleMoveEvents = (e) => { if (!holdBall) return; e.preventDefault(); const x = e.offsetX === undefined ? e.touches[0].clientX : e.offsetX; Body.setPosition(holdBall, { x, y: limitY }); };
const handleDropEvents = (e) => { if (!holdBall) return; Body.set(holdBall, 'collisionFilter', { group: 1, category: categoryOn, mask: categoryOn, }); Body.setStatic(holdBall, false); holdBall.updateTs = Date.now(); holdBall = null; const x = e.offsetX === undefined ? e.changedTouches[0].clientX : e.offsetX; setTimeout(() => { if (isGameOver) return; const level = Math.floor(Math.random() * 5); holdBall = createBall(level, true, x, null, false); }, 500); };
scene.addEventListener('mousemove', handleMoveEvents, false); scene.addEventListener('touchmove', handleMoveEvents); scene.addEventListener('mouseup', handleDropEvents, false); scene.addEventListener('touchend', handleDropEvents);
|
接著,需要監聽以下事件並執行對應的邏輯。
- collisionStart: 球體碰撞事件,用來合成球體
- afterUpdate: 檢測球體是否超出遊戲畫面,若超出則結束遊戲
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| Events.on(engine, 'collisionStart', (event) => { const { pairs } = event; let validPairs = new Set(); for (let i = 0; i < pairs.length; i += 1) { const pair = pairs[i]; if (!pair.bodyA.isStatic && !pair.bodyB.isStatic && pair.bodyA.level < 10 && pair.bodyA.level === pair.bodyB.level && !validPairs.has(pair.bodyA.id) && !validPairs.has(pair.bodyB.id) ) { const x = (pair.bodyA.position.x + pair.bodyB.position.x) / 2; const y = (pair.bodyA.position.y + pair.bodyB.position.y) / 2; Composite.remove(engine.world, [pair.bodyA, pair.bodyB]); const ball = createBall(pair.bodyA.level + 1, false, x, y); ball.updateTs = Date.now(); score.value += 10 * (pair.bodyA.level + 1); validPairs = new Set([...validPairs, pair.bodyA.id, pair.bodyB.id]); } } });
Events.on(engine, 'afterUpdate', () => { const balls = Composite.allBodies(engine.world).filter((body) => body.label === 'ball' && body.collisionFilter.group === 1 && body.collisionFilter.category === categoryOn && Date.now() - body.updateTs > 1000); isGameOver = balls.some((ball) => ball.bounds.min.y < limitY); if (isGameOver) { Runner.stop(runner); alert('獲得分數: ' + score.value); } });
|
最後,若玩家希望重新開始遊戲,可以在介面上多一顆重新開始的按鈕,並使用以下程式碼,來重新開始遊戲。
1 2 3 4 5 6 7 8 9 10 11 12 13
| document.querySelector('#restart').addEventListener('click', () => { score.value = 0; isGameOver = false; Composite.allBodies(engine.world).forEach((body) => { if (body.label === 'ball') { Composite.remove(engine.world, body); } }); holdBall = createBall(0, true, render.options.width / 2, limitY, false); Runner.run(runner, engine); });
|
結語
matter.js 真的很適合用來做2D物理遊戲,只需要短短幾行程式碼,就可以做出許多有趣的物理效果,更多物理效果的使用方式可以參考 matter.js 官方文檔。
這個遊戲的原始碼已經上傳到 GitHub ,你可以到這裡下載這個遊戲的原始碼,並且自己動手來製作一個西瓜遊戲。