ゲームのアニメーションについて、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
<!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>
/**
* @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);
};
})();
//------------------------------------------------------------
// 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);
};
/**
* @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();
});