前面花了幾個章節製作的康威生命遊戲,功能已經大致上完成了,不過我們還無法確定其效能是否還有可以優化的空間,必須要用一些方式來偵測才行。
偵測Canvas的FPS(Frames Per Second)
測量效能的指標,最常用的自然就是整個遊戲畫面的FPS了。
我們可以新增以下這個TypeScript模組,將其存檔為www/src/fps.ts
。
const BUFFER_SIZE = 60;
export class Fps {
private readonly frames: number[] = [];
private lastFrameTimeStamp: number = performance.now();
constructor(private readonly element: HTMLElement) {
this.update();
}
private update(numbers = { mean: 0, min: 0, max: 0 }) {
this.element.innerHTML = `  FPS<br/>Mean: ${Math.round(numbers.mean)}<br/> Min: ${Math.round(numbers.min)}<br/> Max: ${Math.round(numbers.max)}`;
}
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 (const frame of this.frames) {
sum += frame;
min = Math.min(frame, min);
max = Math.max(frame, max);
}
const mean = sum / this.frames.length;
this.update({ mean, min, max });
}
show() {
this.element.classList.remove("fps-hide");
}
hide() {
this.element.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;
}
接著就是在www/src/lib.ts
中引用www/src/fps.ts
模組,並建立Fps
的實體,在renderLoop
函數中呼叫Fps
的render
方法。當然,為了讓FPS偵測結果能夠在遊戲播放時顯示在HTML網頁上,播放與暫停按鈕的事件也要去呼叫show
和hide
方法。
程式碼修改如下:
...
import { Fps } from "./fps.js";
...
export const init = () => {
...
const fps = new Fps(document.getElementById("fps") as HTMLDivElement);
const renderLoop = () => {
fps.render();
universe.tick();
drawCells();
if (!pause) {
requestAnimationFrame(renderLoop);
}
};
...
buttonPlayPause.addEventListener("click", () => {
if (pause) {
pause = false;
requestAnimationFrame(renderLoop);
fps.show();
...
} else {
pause = true;
fps.hide();
...
}
});
};
修改完成後,在康威生命遊戲開始播放時,HTML網頁的左下角就會出現FPS的偵測結果了。
正常情況下,目前我們製作的康威生命遊戲的平均FPS是能達到60的!
測量某段程式的執行時間
利用console
提供的time
和timeEnd
函數,我們可以很輕易地測量某段程式的執行時間。
例如將TypeScript程式改寫如下:
...
export const init = () => {
...
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
函數需要被改寫成以下這樣:
...
export const init = () => {
const drawCells = () => {
...
ctx.strokeStyle = BORDER_COLOR;
ctx.lineWidth = borderSize;
const drawRect = (fillStyle: string, block: Block) => {
ctx.fillStyle = fillStyle;
for (let row = 0;row < HEIGHT;row++) {
for (let column = 0;column < WIDTH;column++) {
const index = universe.get_index(row, column);
if (blocks[index] === block) {
const x = (column * size) + borderSize + marginSize;
const y = (row * size) + borderSize + marginSize;
ctx.fillRect(x, y, blockSize, blockSize);
ctx.strokeRect(x, y, blockSize, blockSize);
}
}
}
};
drawRect(BLOCK_COLOR, Block.NoCell);
drawRect(CELL_COLOR, Block.HasCell);
};
};
如此修改完之後,requestAnimationFrame
函數的執行所需時間只剩下28毫秒左右,已被顯著地優化!
限制FPS
由於我們使用requestAnimationFrame
函數來繪製畫面,這個函數會根據螢幕的刷新頻率來決定其執行的速度。一般螢幕的刷新頻率為60Hz,所以requestAnimationFrame
函數繪製畫面的速度也大概是60fps。不過在這個康威生命遊戲中,過快的FPS會導致遊戲畫面中的活細胞像是發瘋一般亂竄,因此最好加個機制來限制畫面的更新頻率。
將TypeScript程式改寫如下:
...
const WIDTH = 32;
const HEIGHT = 32;
const MAX_FPS = 15;
...
export const init = () => {
...
const fps = new Fps(document.getElementById("fps") as HTMLDivElement);
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程式。