前面花了幾個章節製作的康威生命遊戲,功能已經大致上完成了,不過我們還無法確定其效能是否還有可以優化的空間,必須要用一些方式來偵測才行。



偵測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 = "&nbsp;&nbsp;FPS<br/>" + "Mean:&nbsp;" + 0 + "<br/>" + "&nbsp;Min:&nbsp;" + 0 + "<br/>" + "&nbsp;Max:&nbsp;" + 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 = "&nbsp;&nbsp;FPS<br/>" + "Mean:&nbsp;" + Math.round(mean) + "<br/>" + "&nbsp;Min:&nbsp;" + Math.round(min) + "<br/>" + "&nbsp;Max:&nbsp;" + 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函數中呼叫Fpsrender方法。當然,為了讓FPS偵測結果能夠在遊戲播放時顯示在HTML網頁上,播放與暫停按鈕的事件也要去呼叫showhide方法。

程式碼修改如下:

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的偵測結果了。

rust-webassembly-conways-game-of-life-performance

正常情況下,目前我們製作的康威生命遊戲的平均FPS是能達到60的!

測量某段程式的執行時間

利用console提供的timetimeEnd函數,我們可以很輕易地測量某段程式的執行時間。

例如將JavaScript程式改寫如下:

const renderLoop = () => {
    ...

    console.time("tick_tock");

    universe.tick();

    console.timeEnd("tick_tock");

    ...
};

這邊要注意的是,使用consoletimetimeEnd函數時可傳入一個標籤,用來分辨要用哪個計時器。

如下圖所示,當康威生命遊戲正在播放時,網頁瀏覽器的主控台中就會顯示出tick方法的執行時間。

rust-webassembly-conways-game-of-life-performance

除此之外,在網頁瀏覽器提供的效能檢查工具中,也可以看到consoletimetimeEnd函數所測量到的執行時間。

rust-webassembly-conways-game-of-life-performance

找出使效能變差的程式碼

在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之後,遊戲體驗變得更好了呢!

rust-webassembly-conways-game-of-life-performance

總結

康威生命遊戲至此可以說是已經製作完成了,展示網頁的連結如下:

在下一個章節中,我們要來針對WebAssembly程式的大小進行深入的探討。

下一章:縮小Web­Assembly程式