今天!!是令人興奮的實作!!不再是單純呼叫API文件的範例碼,而是具備意義/情境的程式。之後只要出現這個標題,表示我們要用學到的知識分階段來完成我們的彈珠台。
記得我們 Day3 的時候曾經開過一系列的需求嗎?沒關係,我相信快一個禮拜的現在,大家都忘了差不多了,我們這邊幫大家 Recap 一下需求:
沒錯,粗體的就是我們今天要來完成的項目!好的,我們話不多說,直接開始吧!
今天會專注在世界的基礎建造,也就是初始化。
一開始的共用模組跟一些常數我會統一宣告在外層全域。
// Global Settings / Variables
var Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
Bodies = Matter.Bodies,
Composite = Matter.Composite,
Events = Matter.Events,
BodyM = Matter.Body,
Vertices = Matter.Vertices;
var engine;
var render;
var runner;
const canvasWidth = 400;
const canvasHeigh = 500;
const blockSize = 20;
const mainBallRadius = 15;
const minimumBlockGenerateX = 50;
const minimumBlockGenerateY = 100;
const blockSeparateX = canvasWidth - minimumBlockGenerateX - 50;
const blockSeparateY = canvasHeigh - minimumBlockGenerateY - 100;
const minimumDistanceBetweenBlocks = 60;
再來過往我們直接做的初始化世界,我們用一個 init 的 function 來包住,這邊筆者多設想了一個重置世界的 case,所以才會選擇包起來。
function init()
{
engine = Engine.create();
render = Render.create({
element: document.body,
engine: engine,
options:{
wireframes:false,
showIds:true,
background: '#bfe9f5', //"#bfe9f5"
width:canvasWidth,
height:canvasHeigh
}
});
formHiddenWall();
formMainBall();
formRandomBlocks(15);
Render.run(render);
runner = Runner.create();
}
~~好的!我們今天完工了!~~我們今天本來就是初始化世界就差不多完成需求了,所以其實所有要做的事情都寫在 init 裡了,我們帶大家看一下我們怎麼初始世界的,在 init 的流程如果用白話來說會是:
創建引擎與渲染相關物件 → [[加入引擎中的世界] 加入左右兩側的牆防止球跑出去 → 加入主要的球體 → 加入隨機產生的柱子] → 讓渲染跑起來 → 預先宣告runner但不讓它跑
完成以上後應該會看到如同我們今天一開始貼的圖,會是靜止的球在空中,當你按下 Run the runner 的按鈕,球就會掉下來了!
我們從牆的創建開始看 - formHiddenWall()
function formHiddenWall()
{
var wallLeft = Bodies.rectangle(-21, canvasHeigh/2, 40, canvasHeigh, { isStatic: true });
var wallRight = Bodies.rectangle(canvasWidth+21, canvasHeigh/2, 40, canvasHeigh, { isStatic: true });
Composite.add(engine.world, [wallLeft,wallRight]);
}
牆的創建算是最單純的,基本上就是參考 canvas 的屬性來決定牆的位置與長寬,位置要注意因為給的座標是牆的中心,所以會用這種取半的寫法來讓牆不要顯示在畫面內。
另外因為是一道不能移動的牆,我們在 options 中會設定 isStatic: true ,來表示它是一個靜止的物體。
最後再加入 world 中就大功告成了!
第二個是我們的主角,球 - formMainBall()
function formMainBall()
{
const mainBallInitX = canvasWidth/2;
const mainBallInitY = 50;
var mainBall = Bodies.circle(mainBallInitX, mainBallInitY, mainBallRadius, options = {
restitution: 1,
render:{
fillStyle:"#FFFFFF"
}
}, 100);
Composite.add(engine.world, [mainBall]);
}
球的話要注意的是球的初始位置,我們會放在畫面的中上方,render 因為是會被看到的,我們設成白色的,另外,因為我們預期球是會發生彈跳的,這邊我們給 options 中的 restitution 值,可以依據個人喜好決定彈性碰撞的程度,範例中我們用 1 來做完全彈性碰撞。
最後是比較麻煩的亂數生成方塊 - formRandomBlocks(blockCount)
function formRandomBlocks(blockCount)
{
var blockOptions ={
render : {
fillStyle : "#569cd8",
},
isStatic : true,
angle : getRadiusByDegree(45)
};
var blockCoordinateList = [];
for(var i=0; i<blockCount; i++)
{
var blockCoordinate = getRandomCoordinateForBlocks(blockCoordinateList);
var block = Bodies.rectangle(blockCoordinate.x, blockCoordinate.y, blockSize, blockSize, blockOptions);
blockCoordinateList.push(blockCoordinate);
Composite.add(engine.world, [block]);
}
}
方塊本身的建立是相對單純的,麻煩的會是考量彈珠台的實作,球不能被卡在方塊中間,所以我們會定義一個方塊最小相距常數,在亂數產生的函式中( getRandomCoordinateForBlocks )會讓方塊彼此間的距離不要太近。另外在 options 比較多的時候,我們可以像這裡一樣把 options 抽成單獨一個變數,會比較乾淨也能和其他物體共用(如果有需要的話啦),我們這邊設定的 options依序是藍色填充、靜止物體、旋轉角度45度。
流程上來說會是
設定方塊創建選項 → 宣告一個具有所有方塊座標的陣列(用於檢查距離) → [[迴圈產生指定數量的方塊] → 隨機產生符合距離規範的方塊座標 → 用 Bodies 中的方法以及參數創建方塊 → 將方塊加入世界中]
細部的函示筆者就不走進去細節了,大家可以先看看理解一下,或是嘗試自己實作。
完成你的彈珠台後,可以按下 Run the runner 的按鍵,檢視你今天的傑作!
嘿,球動起來了!它和那些方塊碰碰撞撞!
我們最後加碼一個,如果我們要 reset 的話會怎麼做?我們看到 reInit 這個函式:
function reInit()
{
event.preventDefault();
Engine.clear(engine);
Render.stop(render);
Runner.stop(runner);
render.canvas.remove();
render.canvas = null;
render.context = null;
render.textures = {};
init();
}
這邊依序是終止了發生中事件、清除引擎、停止渲染、停止跑動迴圈,移除對應的canvas與紋理 ─ 最後,我們在執行一次我們一開始抽出來的初始化函式!
嘿!按下 Re Init的按鈕,世界就這樣又重新來過了!你可以一次次的讓球重新掉落、重新生成碰撞方塊了!
我們的彈珠台看起來已經有模有樣了,不是嗎?明天,讓我們來熟悉其他的模組,一起為完成彈珠台繼續努力!