前面花了幾個章節製作的康威生命遊戲,功能已經大致上完成了,不過我們還無法確定其效能是否還有可以優化的空間,必須要用一些方式來偵測才行。
偵測Canvas的FPS(Frames Per Second)
測量效能的指標,最常用的自然就是整個遊戲畫面的FPS了。
我們可以新增以下這個JavaScript模組,將其存檔為www/src/fps.js
。
const BUFFER_SIZE = 60;
export default class Fps {
constructor(element) {
this.fps = element;
this.frames = [];
this.lastFrameTimeStamp = performance.now();
this.fps.innerHTML = " FPS<br/>" + "Mean: " + 0 + "<br/>" + " Min: " + 0 + "<br/>" + " Max: " + 0;
}
render() {
const now = performance.now();
const interval = now - this.lastFrameTimeStamp;
this.lastFrameTimeStamp = now;
const fps = 1 / interval * 1000;
this.frames.push(fps);
if (this.frames.length > BUFFER_SIZE) {
this.frames.shift();
}
let min = Infinity;
let max = -Infinity;
let sum = 0;
for (let i = 0;i < this.frames.length;++i) {
sum += this.frames[i];
min = Math.min(this.frames[i], min);
max = Math.max(this.frames[i], max);
}
const mean = sum / this.frames.length;
this.fps.innerHTML = " FPS<br/>" + "Mean: " + Math.round(mean) + "<br/>" + " Min: " + Math.round(min) + "<br/>" + " Max: " + Math.round(max);
}
show() {
this.fps.classList.remove("fps-hide");
}
hide() {
this.fps.classList.add("fps-hide");
}
}
這個模組可以用來測量遊戲畫面的FPS,並且輸出到某個HTML元素中。
所以我們就來新增一個專門顯示FPS的元素到HTML網頁中吧!
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Conway's Game of Life</title>
<link href="./css/bundle.min.css" rel="stylesheet">
</head>
<body>
<div id="fps" class="fps-hide"></div>
<div id="controls">
<button id="play-pause">Play</button>
<button id="next">Next</button>
<button id="clear">Clear</button>
</div>
<canvas id="game-of-life-canvas"></canvas>
<script src="./js/bundle.min.js"></script>
</body>
</html>
SCSS/CSS中也要加入以下兩個元素:
#fps {
font-family: monospace, monospace;
color: #FFFFFF;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
bottom: 0;
left: 0;
padding: 5px;
}
.fps-hide {
display: none !important;
}
接著就是在原先的JavaScript程式中引用www/src/fps.js
模組,並建立Fps
的實體,在renderLoop
函數中呼叫Fps
的render
方法。當然,為了讓FPS偵測結果能夠在遊戲播放時顯示在HTML網頁上,播放與暫停按鈕的事件也要去呼叫show
和hide
方法。
程式碼修改如下:
import Fps from "./fps.js";
...
const fps = new Fps(document.getElementById("fps"));
const renderLoop = () => {
fps.render();
universe.tick();
drawCells();
if (!pause) {
requestAnimationFrame(renderLoop);
}
};
...
document.getElementById("play-pause").addEventListener("click", event => {
if (pause) {
pause = false;
requestAnimationFrame(renderLoop);
fps.show();
...
} else {
pause = true;
fps.hide();
...
}
});
修改完成後,在康威生命遊戲開始播放時,HTML網頁的左下角就會出現FPS的偵測結果了。
正常情況下,目前我們製作的康威生命遊戲的平均FPS是能達到60的!
測量某段程式的執行時間
利用console
提供的time
和timeEnd
函數,我們可以很輕易地測量某段程式的執行時間。
例如將JavaScript程式改寫如下:
const renderLoop = () => {
...
console.time("tick_tock");
universe.tick();
console.timeEnd("tick_tock");
...
};
這邊要注意的是,使用console
的time
和timeEnd
函數時可傳入一個標籤,用來分辨要用哪個計時器。
如下圖所示,當康威生命遊戲正在播放時,網頁瀏覽器的主控台中就會顯示出tick
方法的執行時間。
除此之外,在網頁瀏覽器提供的效能檢查工具中,也可以看到console
的time
和timeEnd
函數所測量到的執行時間。
找出使效能變差的程式碼
在32x32尺寸的培養皿下,執行一次requestAnimationFrame
函數的所需時間約為5毫秒,執行一次tick
所花的時間為1毫秒左右。為了使執行時間更容易觀察,我們先改變JavaScript程式,將培養皿的尺寸加到128x128,這樣執行一次requestAnimationFrame
函數的所需時間就大概是42毫秒。在這個尺寸的培養皿下,執行一次tick
所花的時間為4毫秒左右。所以我們從增長比例來判斷可以知道tick
不會是造成requestAnimationFrame
函數執行時間增長的主因。
那麼會大大影響執行速度的因素大概就是drawCells
函數了。經過不斷地修改程式和測量效能,我們可以發現,在迴圈中指派ctx.fillStyle
的值會嚴重地影響程式效能。所以drawCells
函數需要被改寫成以下這樣:
const drawCells = () => {
...
ctx.fillStyle = BLOCK_COLOR;
for (let row = 0;row < HEIGHT;row++) {
for (let column = 0;column < WIDTH;column++) {
const index = universe.get_index(row, column);
if (blocks[index] !== module.Block.NoCell) {
continue;
}
const x = (column * size) + blockSize + marginSize;
const y = (row * size) + blockSize + marginSize;
ctx.fillRect(
x, y, blockSize, blockSize
);
ctx.strokeRect(
x, y, blockSize, blockSize
);
}
}
ctx.fillStyle = CELL_COLOR;
for (let row = 0;row < HEIGHT;row++) {
for (let column = 0;column < WIDTH;column++) {
const index = universe.get_index(row, column);
if (blocks[index] !== module.Block.HasCell) {
continue;
}
const x = (column * size) + blockSize + marginSize;
const y = (row * size) + blockSize + marginSize;
ctx.fillRect(
x, y, blockSize, blockSize
);
ctx.strokeRect(
x, y, blockSize, blockSize
);
}
}
};
如此修改完之後,requestAnimationFrame
函數的執行所需時間只剩下28毫秒左右,已被顯著地優化!
限制FPS
由於我們使用requestAnimationFrame
函數來繪製畫面,這個函數會根據螢幕的刷新頻率來決定其執行的速度。一般螢幕的刷新頻率為60Hz,所以requestAnimationFrame
函數繪製畫面的速度也大概是60fps。不過在這個康威生命遊戲中,過快的FPS會導致遊戲畫面中的活細胞像是發瘋一般亂竄,因此最好加個機制來限制畫面的更新頻率。
將JavaScript程式改寫如下:
...
const MAX_FPS = 15;
...
const fpsInterval = 1 / MAX_FPS * 1000;
let startTime = Date.now();
const renderLoop = () => {
const now = Date.now();
if (now - startTime > fpsInterval) {
startTime = now;
fps.render();
universe.tick();
drawCells();
}
if (!pause) {
requestAnimationFrame(renderLoop);
}
};
將康威生命遊戲的最大FPS限制在15之後,遊戲體驗變得更好了呢!
總結
康威生命遊戲至此可以說是已經製作完成了,展示網頁的連結如下:
在下一個章節中,我們要來針對WebAssembly程式的大小進行深入的探討。
下一章:縮小WebAssembly程式。