ゲームのアニメーションについて、fpsを中心に話をします。

● fps

fpsは、frames per second(フレーム毎秒)の略です。1秒間に、どれだけの回数描画されるかという単になります。

PCゲームで難しいのは、マシンの性能によって1秒間に描画できる回数が違うことです。

どのスペックのマシンを切るか、あるいは、どこまで対応するか。また、低スペックのマシンで描画方式を切り替えるかなど、考えるべきことは多いです。

● サンプルコード

ファイル構成は、以下の通りです。index.htmlを開くと、4:3の比率の描画領域がブラウザ内に現れます。複数の四角を回転描画します。fpsが表示されます。クリックするごとに、停止/再開します。

ChromeやFirefoxといったモダンブラウザ用のサンプルコードです。IEには対応していません。

index.html
css/main.css
js-game/game.anim.js
js-game/game.canvas.js
js-game/game.core.js
js-game/game.view.js
js-lib/jquery-3.3.1.min.js
js-main/main.js

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, user-scalable=0" />
    <title></title>
    <link rel="stylesheet" href="css/main.css">

    <!-- /* jQuery, other... */ -->
    <script>
        (function() {
            // Electron or Browser
            const pJQ = './js-lib/jquery-3.3.1.min.js';
            if (window.require) {return window.jQuery = window.$ = require(pJQ)}
            document.writeln(`<script src="${pJQ}"></${``}script>`);
        })();
    </script>

    <!-- /* Lib. Game */ -->
    <script src="js-game/game.anim.js"></script>
    <script src="js-game/game.canvas.js"></script>
    <script src="js-game/game.core.js"></script>
    <script src="js-game/game.view.js"></script>

    <!-- /* Lib. Main */ -->

    <!-- /* Start Main */ -->
    <script src="js-main/main.js"></script>

  </head>
  <body>
    <div id="app" class="app"></div>
  </body>
</html>

js-game/game.anim.js
/**
 * @license com.crocro.game v4.0.0
 * (c) 2013-2018 Masakazu Yanai https://crocro.com/
 * License: MIT / Date: 2018-05-06
 */

'use strict';

// ライブラリ用のオブジェクトの作成
((o, p) => p.split('.').forEach(k => o = !o[k] ? o[k]={} : o[k]))
(window, 'com.crocro.game.anim');

//------------------------------------------------------------
(function() {
    // ショートカットの作成
    const game = window.com.crocro.game;
    const _t = game.anim;

    //------------------------------------------------------------
    // 変数の初期化
    _t.lowCpuMode = false;  // 低CPUモード
    _t.frmRt = 30;          // 低CPUモード時のフレームレート

    //------------------------------------------------------------
    // アニメーション実行/停止用関数
    _t.rqstAnmFrm = function(cb) {
        // 低CPUモード
        if (_t.lowCpuMode) {
            const id = window.setTimeout(cb, 1000 / _t.frmRt);
            return id;
        }

        // 通常モード
        const id = (
            window.requestAnimationFrame       ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function(cb) {
                return window.setTimeout(cb, 1000 / 60);
            })(cb);
        return id;
    };

    _t.cnclAnmFrm = function(id) {
        // 低CPUモード
        if (_t.lowCpuMode) {
            window.clearTimeout(id);
            return;
        }

        // 通常モード
        (window.cancelAnimationFrame              ||
         window.webkitCancelRequestAnimationFrame ||
         window.mozCancelRequestAnimationFrame    ||
         window.oCancelRequestAnimationFrame      ||
         window.msCancelRequestAnimationFrame     ||
         window.clearTimeout)(id)
    };

    //------------------------------------------------------------
    // アニメーション用変数
    _t.anmId = null;
    _t.updtArr = {default: [], static: []};
            // defaultは名前無しで追加削除可能。staticは削除しないアニメ用の配列。
            // 各配列の要素は {nm: 名前, fnc: 関数}
    _t.tm = {sum: 0, old: null, now: 0, dif: 0};
    _t.flgStop = false;     // 停止フラグ

    //------------------------------------------------------------
    // アニメーションの開始
    _t.strt = function() {
        // 変数の初期化
        _t.flgStop = false;
        _t.tm.old = new Date();

        // アニメーション ループ
        const anmFnc = function() {
            _t.updt();
            if (_t.flgStop) {return}
            _t.anmId = _t.rqstAnmFrm(anmFnc);
        };
        anmFnc();
    };

    //------------------------------------------------------------
    // アニメーションの停止
    _t.stop = function() {
        if (_t.anmId === null) {return}
        _t.cnclAnmFrm(_t.anmId);
        _t.flgStop = true;
    };

    //------------------------------------------------------------
    // アニメーションの更新
    _t.updt = function() {
        // 差分時間と経過時間を計算
        _t.tm.now = new Date();
        _t.tm.dif =  _t.tm.old == null ? 0 : (_t.tm.now - _t.tm.old);
        _t.tm.sum += _t.tm.dif;
        _t.tm.old =  _t.tm.now;

        // 更新配列を全て実行
        for (let key in _t.updtArr) {
            _t.updtArr[key].forEach(x => {x.fnc(_t.tm)});
        }
    };

    //------------------------------------------------------------
    // アニメーションの追加
    _t.add = function(nm, fnc, trgt) {
        const arr = _t.updtArr[trgt === undefined ? 'default' : trgt];
        if (arr === undefined) {arr[trgt] = []}     // 配列の作成
        arr.push({nm: nm, fnc: fnc});
    };

    // アニメーションの削除
    _t.rmv = function(nm, trgt) {
        const arr = _t.updtArr[trgt === undefined ? 'default' : trgt];
        if (arr === undefined) {return}
        for (let i = arr.length - 1; i >= 0; i --) {
            if (nm == arr[i].nm) {arr.splice(i, 1)}
        }
    };

    // アニメーションの全削除
    _t.rmvAll = function(trgt) {
        const arr = _t.updtArr[trgt === undefined ? 'default' : trgt];
        if (arr === undefined) {return}
        arr.splice(0, arr.length);
    };

})();

js-game/game.core.js 差分
    //------------------------------------------------------------
    // Xorshift
    _t.Xors = function(n) {
        let x, y, z, w;

        // シード
        this.seed = function(n) {
            x = 123456789; y = 362436069; z = 521288629; w = 88675123;
            if (typeof n === "number") {w = n;}
        }

        // ランダム
        this.rnd = function() {
            const t = x ^ (x << 11);
            x = y; y = z; z = w;
            return w = (w^(w>>19))^(t^(t>>8));
        }

        // 初回実行
        this.seed(n);
    };

js-main/main.js
/**
 * @license com.crocro.game Sample
 * (c) 2018 Masakazu Yanai https://crocro.com/
 * License: MIT / Date: 2018-03-15
 */

'use strict';

// 開始
$(function() {
    // ショートカットの作成
    const game = window.com.crocro.game;

    //------------------------------------------------------------
    // レイヤー情報の初期化
    const layerBg   = 0;
    const layerDraw = 1;
    const layerFps  = 2;
    const layerMax  = 3;

    // キャンバス情報とキャンバス配列の初期化
    const cnvsInf = new game.canvas.CnvsInf({n: layerMax, bg: '#ffc'});
    const cnvsArr = game.canvas.initCnvsArr(cnvsInf);

    // 画面の初期化
    game.canvas.clearCnvsArr(cnvsArr);  // キャンバスのクリア
    game.view.init(cnvsInf);            // 表示の初期化

    // タップ位置の描画
    game.view.addTap('tapAndDraw', null, (x, y) => {
        // アニメーション停止/再開
        game.anim.flgStop = ! game.anim.flgStop;
        if (! game.anim.flgStop) {game.anim.strt()}
    });

    //------------------------------------------------------------
    // アニメーション
    const xors = new game.core.Xors();
    const w = cnvsInf.w;
    const h = cnvsInf.h;
    const fntSz = 12;

    // アニメーションの登録 描画領域のクリア
    game.anim.add('clearCnvs', tm => {
        const cntx = cnvsArr[layerDraw].cntx;
        cntx.clearRect(0, 0, w, h);
    });

    // アニメーションの登録 fpsの描画
    let dtOld = +new Date();
    let fpsArrIndx = 0;
    const fpsArr = [];
    const fpsArrMax = 20;

    game.anim.add('drawFps', tm => {
        // 変数の初期化
        const dtNow = +new Date();
        const dtDiff = dtNow - dtOld;
        const fps = 1000 / dtDiff | 0;

        fpsArr[fpsArrIndx] = fps;

        const cntx = cnvsArr[layerFps].cntx;
        cntx.font = `${fntSz}px sans-serif`;
        cntx.textBaseline = 'middle';
        cntx.strokeStyle = 'white';
        cntx.lineWidth = 4;

        cntx.clearRect(0, 0, w, h);

        // fpsの描画
        for (let i = 0; i < fpsArr.length; i ++) {
            // 変数の初期化
            const txt = 'fps : ' + fpsArr[i];
            const x = fntSz / 2;
            const y = fntSz * (i + 1) * 1.5;
            cntx.fillStyle = (i === fpsArrIndx) ? 'red' : 'black';

            // 描画
            cntx.strokeText(txt, x, y);
            cntx.fillText(txt, x, y);
        }

        // 値の更新
        dtOld = dtNow;
        fpsArrIndx = (fpsArrIndx + 1) % fpsArrMax;
    });

    // アニメーションの生成
    const genAnmObj = function() {
        // 変数の初期化
        const anmId = 'obj' + (+new Date());
        const strt = game.anim.tm.sum;
        let   endFlg = false;   // 最後に1回描画するため
        const anmLen = 1000;
        const x = xors.rnd() % w;
        const y = xors.rnd() % h;
        const sqLen = 100;
        const colorArr = ['faa', 'afa', 'aaf', 'ff8', 'f8f', '8ff'];
        const color = colorArr[xors.rnd() % colorArr.length];
        const cntx = cnvsArr[layerDraw].cntx;

        // アニメーションの登録
        game.anim.add(anmId, tm => {
            // 終了管理
            if (endFlg) {
                game.anim.rmv(anmId);   // アニメ除去
                return;
            }
            if (tm.sum >= strt + anmLen) {endFlg = true}

            // 変数の初期化
            const rate = game.core.minMax(0, (tm.sum - strt) / anmLen, 1);

            // 描画処理
            cntx.save();
            cntx.fillStyle = `#${color}`;
            cntx.globalAlpha = Math.sin(Math.PI * rate);
            cntx.translate(x, y);
            cntx.rotate(2 * Math.PI * rate);
            cntx.fillRect(- sqLen / 2, - sqLen / 2, sqLen, sqLen);
            cntx.restore();
        });
    };

    // アニメーションの登録
    (function() {
        // 変数の初期化
        const strt = game.anim.tm.sum;
        let   last = strt;
        const intrvl = 50;

        game.anim.add('mngObj', tm => {
            const now = tm.sum;
            if (((now - strt) / intrvl | 0) > ((last - strt) / intrvl | 0)) {
                genAnmObj();
                last = now;
            }
        });
    })();

    // アニメーション開始
    game.anim.strt();
});

初Steamゲーム 8bit風RTS『TinyWar high-speed』発売中です。
その他の同人ゲームや同人技術書はこちら。
作成:2018/05/07  更新:2018/05/30  [Permalink]