その昔、Win32 でゲームを作ってた頃に timeGetTime() と sleep() で正確に 60 FPS にするには各フレームでどれだけ時間をとればいいのか考えた事があった。
(ちなみに、確か Direct2D に画面フリップまでブロックする処理があって、そっちを使った方が画面は崩れなかった気がする)
今は可変 FPS の時代だし、あんまり意味はなさそうだけど、なんかの参考になるかもしれないのでメモっておく。
大枠の考え方としては「きれいにバラけるように事前に表を作っておいて、各フレームではその表の時間だけ使っていいとする」みたいな感じ。
その表を作る部分は当時「こういう表が欲しいわけだから……」というところから逆算して、これでいけるな!と思った方法という、説明しがたいやつだったが、今回 HTML にべた書きした JavaScript でその表を計算して表示するので、実行して存分に堪能して欲しい。
<html>
<head>
<title>ideal frame duration calculation</title>
<script>
(function(global){
// 二つの数の最大公約数 (greatest common divisor) を求めます.
const gcd2 = function(value1, value2) {
if ((value1 == 0) || (value2 == 0)) {
// 想定外の引数
throw "illegal argument";
}
// ユーグリッドの互除法 (Euclidean Algorithm)
while (value2 != 0) {
const tmpValue = value2;
value2 = value1 % value2;
value1 = tmpValue;
};
return value1;
};
// // 二つの数の最小公倍数 (least common multiple) を求めます.
// const lcm2 = function(value1, value2) {
//
// if ((value1 == 0) || (value2 == 0)) {
// // 想定外の引数
// throw "illegal argument";
// }
//
// const v1 = value1 * value2;
// const v2 = gcd2(value1, value2);
// const result = parseInt(v1 / v2);
// return result;
// };
// `createCycleTable` 理想 FPS の為に各フレームで待つべき時間の表を作成します.
// `waitsPerSecond` 一秒辺りの待ち時間 (ミリ秒単位なら 1000, マイクロ秒単位なら 1000000)
// `fpsToDesire` 理想秒間フレーム数
const createCycleTable = function(waitsPerSecond, fpsToDesire) {
const baseTime = parseInt(waitsPerSecond / fpsToDesire);
const remaindered = waitsPerSecond % fpsToDesire;
if (remaindered == 0) {
return [baseTime];
}
const gcdValue = gcd2(fpsToDesire, remaindered);
const cycleItemCount = parseInt(fpsToDesire / gcdValue);
const longItemCount = parseInt(remaindered / gcdValue);
var cycleTable = [];
const mag = 65536;
const toDecrease = parseInt(mag * longItemCount / cycleItemCount);
var counter = mag * longItemCount;
for (var i = 0; i < cycleItemCount; i += 1)
{
const prev = parseInt(counter / mag);
counter -= toDecrease;
const curr = parseInt(counter / mag);
if (prev != curr) {
cycleTable[i] = baseTime + 1;
} else {
cycleTable[i] = baseTime;
}
}
// 確かめ算 verification of figures
var cntForVerification = 0;
var pos = 0;
for (var i = 0; i < fpsToDesire; i += 1) {
cntForVerification += cycleTable[pos];
pos += 1;
pos %= cycleItemCount;
}
if (cntForVerification != waitsPerSecond) {
throw `verification failed. expects: ${waitsPerSecond}, but actual: ${cntForVerification}`;
}
return cycleTable;
};
document.addEventListener('DOMContentLoaded', function() {
const waitsPerSecondBox = document.querySelector('#waits-per-second-box');
const fpsToDesireBox = document.querySelector('#fps-to-desire-box');
const calcTableButton = document.querySelector('#calc-table-button');
const resultPane = document.querySelector('#result-pane');
calcTableButton.addEventListener('click', function(event){
const waitsPerSecond = parseInt(waitsPerSecondBox.value, 10);
const fpsToDesire = parseInt(fpsToDesireBox.value, 10);
const cycleTable = createCycleTable(waitsPerSecond, fpsToDesire);
const cycleItemCount = cycleTable.length;
// 結果の表を作る (ここは出りゃいいでしょという適当なやり方)
const tableElm = document.createElement('table');
for (var i = 0; i < cycleItemCount; i = i + 1) {
const cycleItem = cycleTable[i];
const td1Elm = document.createElement('td');
td1Elm.innerText = `${i + 1}`;
const td2Elm = document.createElement('td');
td2Elm.innerText = `${cycleItem}`;
const trElm = document.createElement('tr');
trElm.appendChild(td1Elm);
trElm.appendChild(td2Elm);
tableElm.appendChild(trElm);
}
resultPane.innerHTML = '';
resultPane.appendChild(tableElm);
tickCycleTable = cycleTable;
tickCycleTableIndex = 0;
});
onTick();
});
const getTickCount = function() {
return (new Date()).getTime();
};
var progress = 0;
var increment = 15;
var max = 360;
var tickCycleTable = [1000];
var tickCycleTableIndex = 0;
var tickCount = getTickCount();
const onTick = function() {
const thisTickStartedAt = getTickCount();
const interval = tickCycleTable[tickCycleTableIndex];
tickCycleTableIndex += 1;
tickCycleTableIndex %= tickCycleTable.length;
// console.log(tickCycleTableIndex);
const nextTickShouldStartAt = tickCount + interval;
tickCount += interval;
// ----
const hoge = Math.sin(progress * (Math.PI / 180));
const demoCanvas = document.querySelector('#demo-canvas');
const ctx2d = demoCanvas.getContext('2d');
ctx2d.fillStyle = 'white';
ctx2d.fillRect(0, 0, 100, 100);
ctx2d.fillStyle = 'blue';
ctx2d.beginPath();
ctx2d.arc(50, 50 + (10 * hoge), 20, 0, Math.PI * 2, true);
ctx2d.closePath();
ctx2d.fill();
// console.log(`${progress} ${mage}`);
progress += increment;
progress %= max;
// ----
const tickProcessEndedAt = getTickCount();
var delay = nextTickShouldStartAt - tickProcessEndedAt;
if (delay < 0) {
console.log('間に合わない');
tickCount = tickProcessEndedAt;
delay = interval;
}
setTimeout(onTick, delay);
};
})(this);
</script>
<style>
table { border: 1px solid; }
td { border: 1px solid; }
</style>
</head>
<body>
<div>
<label for="waits-per-second-box">Waits per second</label>
<input id="waits-per-second-box" type="text" id="" value="1000" />
<span>一秒辺りの待ち時間 (ミリ秒単位なら 1000, マイクロ秒単位なら 1000000)</span>
</div>
<div>
<label for="fps-to-desire-box">FPS to desire</label>
<input id="fps-to-desire-box" type="text" id="" value="60" />
<span>理想秒間フレーム数</span>
</div>
<div>
<button id="calc-table-button">Calculate</button>
</div>
<div>
<label for="demo-canvas">demo</label>
<canvas id="demo-canvas" width="100" height="100"></canvas>
</div>
<div id="result-pane">
</div>
</body>
</html>
「数学は無理、算数の時点で超苦手」という自分が何故最大公約数にたどり着いたのか、本当に覚えてない。
(でも一人でゼロから売り物の 3D のアクション RPG (BREW by KDDI な奴のアプリ) を書いた事あるんですよ!描画はライブラリがやってくれたけど、内部計算はガチで自力で全部やったの!ってか malloc/free 相当のやつも自作した。自分でも信じられない!)