これまでゲームの更新処理をするときにsetInterval関数を使ってきました。ただ困ったことにブラウザによって更新間隔が違ってくるという問題が起きるのです。鳩でもわかるC#管理人はFirefoxというブラウザを使っています。できあがったゲームをGoogle Chromeなどの他のブラウザで動かして「あれ?動作速度が違う」ということがよくおきるのです。これはFirefox特有の問題なのでしょうか? Microsoft EdgeだとGoogle Chromeとほとんどかわりません。
Contents
FirefoxでsetInterval関数を実行するとおかしい?
以下のコードを書いて各ブラウザで実行してみるとFirefoxだけ遅いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>FirefoxでsetInterval関数を実行するとおかしい</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> </head> <body> <div id = "result1"></div> <div id = "result2"></div> <script> const $result1 = document.getElementById('result1'); const $result2 = document.getElementById('result2'); let updateCount = 0; setInterval(() => { updateCount++; $result1.innerText = updateCount; }, 1000 / 30); // 1秒間に30回 setTimeout(() => { $result2.innerText = `10秒間で ${updateCount} 回の更新がおこなわれました`; }, 1000 * 10); </script> </body> </html> |
Microsoft EdgeだとGoogle Chromeで上記のコードを実行すると「10秒間で 303 回の更新がおこなわれました」と300回前後の値が表示されます。1秒あたり30回なのでsetInterval関数の引数とだいたい合っています。ところがFirefoxだと「10秒間で 214 回の更新がおこなわれました」と全2者と比べると低い値が出てきます。
requestAnimationFrame関数じゃダメなの?
ではrequestAnimationFrame関数を使えばどうなるでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>requestAnimationFrame関数を使えばどうなる?</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> </head> <body> <div id = "result1"></div> <div id = "result2"></div> <script> const $result1 = document.getElementById('result1'); const $result2 = document.getElementById('result2'); let updateCount = 0; update(); function update(){ updateCount++; $result1.innerText = updateCount; requestAnimationFrame(update); } setTimeout(() => { $result2.innerText = `10秒間で ${updateCount} 回の更新がおこなわれました`; }, 1000 * 10); </script> </body> </html> |
この場合はFirefoxでもGoogle ChromeでもMicrosoft Edgeでも「10秒間で 602 回の更新がおこなわれました」(多少のズレはあり)と1秒間に60回の更新処理がおこなわれていることが確認できます。
だったらsetTimeout関数ではなくrequestAnimationFrame関数のほうがよいのではないかと思われるのですが、これだと更新処理の間隔を自分で指定することができない ・・・ そう思い込んでいました。
requestAnimationFrame関数だと時間間隔を指定できない?
ところがrequestAnimationFrame関数の実行間隔を擬似的に指定することは可能です。requestAnimationFrame関数が呼び出されたらその時刻を記録しておき、1000 / 30ミリ秒経過した場合だけ更新処理をおこなうようにすればよいのです。
MyIntervalクラスを定義する
グローバル変数ばっかりにしたくないのでクラスにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class MyInterval{ constructor(interval){ this.Interval = interval; this.NextTime; this.ID = null; } SetInterval(interval) { this.Interval = interval; if(this.ID != null) cancelAnimationFrame(this.ID); this.NextTime = new Date().getTime() + this.Interval; this.ID = this.FrameProc(); } FrameProc(){ // 現在時刻を取得し、 // 現在時刻がthis.NextTimeを過ぎていたときだけOnUpdated関数を呼び出す const curTime = new Date().getTime(); if(this.NextTime < curTime){ this.NextTime += this.Interval; this.OnUpdated(); } this.ID = requestAnimationFrame(() => this.FrameProc()); } OnUpdated(){ // このクラスを継承してここにthis.Intervalおきに実行したい処理を書く } } |
MyIntervalクラスを使ってみる
MyIntervalクラスを継承してOnUpdated関数をオーバーライドすることで1000 / 60ミリ秒おきではなく1000 / 30ミリ秒おきに更新処理をおこなうことができるようになります。これだとブラウザに関係なく1秒間に30回の更新処理がおこなわれます。
ただし、普通のモニターのリフレッシュレートは60Hzなので、これよりも短い間隔で更新処理をしようとするとこの方法は使えません。引数のintervalの値は 1000 / 60かそれより大きな値でなければなりません(interval = 1000 / 100みたいなのは不可)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>requestAnimationFrame関数の実行間隔を擬似的に指定する</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link rel = "stylesheet" href = "./style.css" type = "text/css" media = "all"> </head> <body> <div id = "result1"></div> <div id = "result2"></div> <button onclick="run(1000 / 30)">1秒間に30回更新を実行</button> <button onclick="run(1000 / 60)">1秒間に60回更新を実行</button> <script src = "./myinterval.js"></script> <script> const $result1 = document.getElementById('result1'); const $result2 = document.getElementById('result2'); let updateCount = 0; class MyIntervalEx extends MyInterval{ OnUpdated(){ update(); } } const myIntervalEx = new MyIntervalEx(); let timeoutID = null; function run(interval){ updateCount = 0; myIntervalEx.SetInterval(interval); clearTimeout(timeoutID); timeoutID = setTimeout(() => { // ためしに10秒後の更新回数を表示させてみる $result2.innerText = `10秒間で ${updateCount} 回の更新がおこなわれました`; }, 1000 * 10); } function update(){ updateCount++; $result1.innerText = updateCount; $result2.innerText = myIntervalEx.CallCount; } </script> </body> </html> |