This document is about: QUANTUM 3
SWITCH TO

Frame TimerとGame Timer

Quantumには、スキルのクールダウン・リスポーン時間・シミュレーション中の定期的チェックなど、タイマーに基づくゲームプレイを実装するための組み込みタイマーが2つ用意されています:

  • FrameTimer:シミュレーションフレームをカウントし、常にシミュレーションレートで動作します。
  • GameTimer:ゲームティック(Frame.GameTick)をカウントします。これはフレームごとにFrame.GameTimeScale分だけ進むため、ゲーム進行に合わせて一時停止したり速度が低下したりします。

タイマーの使い分け

FrameTimer GameTimer
カウント フレーム(Frame.Number) ゲームティック(Frame.GameTick)
Frame.GameTimeScale 無視される 0で一時停止、1以下で遅く、1以上で早くなる
保存形式 2 x int 2 x FP
期限チェック HasStoppedThisFrame HasStoppedThisTick
用途 一時停止やスローモーションに関係なく進行する時間(例:試合のカウントダウン・AFKキック・再接続の猶予時間) 一時停止やスローモーションになるゲームプレイ(例:クールダウン・バフ・リスポーン時間・敵出現ウェーブ)

GameTimeScaleを変更しないゲームの場合、両方のタイマーは同じように動作します。

FrameTimer

秒数/ティック数を指定して、タイマーを開始します。

C#

FrameTimer timerA = FrameTimer.FromFrames(frame, 60)
FrameTimer timerB = FrameTimer.FromSeconds(frame, 2)
FrameTimer timerC = default(FrameTimer);

timerAとtimerBは実行状態になりますが、timerCはまだ設定されていない状態です。

上記で設定したタイマーについて、FrameTimerの状態をチェックする例は次の通りです。

C#

timerA.IsRunning(frame); // returns TRUE
timerB.IsRunning(frame); // returns TRUE
timerC.IsRunning(frame); // returns FALSE

timerA.IsSet; // returns TRUE
timerB.IsSet; // returns TRUE
timerC.IsSet; // returns FALSE

FrameTimerは目標フレームを保持し、タイマーが停止した瞬間を問い合わせることができます。また、経過時間や残り時間を返すことができるので、例えばゲームUIに表示するのにも便利です。

C#

if (timer.HasStoppedThisFrame(frame)) {
    // ちょうどタイマーが停止したフレームでのみ実行される
}
else {
    var ticksRemaining = timer.RemainingFrames(frame);
    var secondsRemaining = timer.RemainingSeconds(frame);

    var ticksElapsed = timer.ElapsedFrames(frame);
    var elapsedSeconds = timer.ElapsedSeconds(frame);
}

秒単位の時間は、シミュレーションレート(Frame.UpdateRate)に変換されるため、1秒の値はフレームレートに相当します。FromSecondsは、結果をフレーム単位に切り上げます。

Durationはタイマーの経過時間をフレーム単位で返し、DurationNormalizedは0~1(開始直後~期限切れ)までの進行度を返します。(例:UIのクールダウンアイコンの表示に使用できます)

C#

var progress = timer.DurationNormalized(frame);

FrameTimerは、DSL内コンポーネントやフレームのグローバル変数に追加することができます。(例:ゲームが一時停止している間も動作し続ける試合のカウントダウンに使用できます)

Qtn

component Match {
    FrameTimer Countdown;
}

global {
    FrameTimer MatchTimer;
}

GameTimer

GameTimerのAPIはFrameTimerと同じですが、フレームではなくゲームティックをカウントします。ゲームティックは0から始まり、各フレーム開始時にFrame.GameTimeScale分だけ進みます。タイムスケールが1(デフォルト)の場合は1フレームごとに1ティック進み、0の場合はゲーム時間が一時停止し、0.5の場合は半分の速度で進行します。タイムスケールの変更は、次のフレームから適用されます。

C#

frame.GameTimeScale = FP._0_50;

GameTimer timerA = GameTimer.FromTicks(frame, 60);
GameTimer timerB = GameTimer.FromSeconds(frame, 2); // 半分の速度で4秒間続く

GameTimerはDSL内で使用可能(Ticks/Secondsの両方)で、FrameTimerと同じようにプロトタイプと組み合わせて使用できます。

Qtn

component Character {
    GameTimer SkillCooldown;
}

次の例では、コンポーネントのGameTimerを使用して、スキルのクールダウン(ゲームの進行に合わせて一時停止したり遅くなったりする)を管理しています。

C#

namespace Quantum
{
  using Photon.Deterministic;
  using UnityEngine.Scripting;
  
  [Preserve]
  public unsafe class CharacterSystem : SystemMainThreadFilter<CharacterSystem.Filter> {
    public struct Filter {
      public EntityRef Entity;
      public Character* Character;
    }

    public override void Update(Frame frame, ref Filter filter) {
        var character = filter.Character;
        if (character->SkillCooldown.IsRunning(frame) == false) {
            // スキルを実行し、クールダウンをリセットする

            // 新しいタイマーを作成して、クールダウンをリセットする
            character->SkillCooldown = GameTimer.FromSeconds(frame, 2);

            // タイマーをリスタートして、クールダウンをリセットする
            // ただし、タイマーは以前に設定されている(例:コンポーネントに追加する際)必要があります
            character->SkillCooldown.Restart(frame);
        }
    }
  }
}

時間の確認や取得はFrameTimerと同様に動作します。秒数はゲーム時間の秒数で、シミュレーションレートに変換されます:

C#

if (timer.HasStoppedThisTick(frame)) {
    // このフレーム中にタイマーが切れた場合のみ実行される
}
else {
    var ticksRemaining = timer.RemainingTicks(frame);
    var secondsRemaining = timer.RemainingSeconds(frame);
    var progress = timer.DurationNormalized(frame);
}

実際の秒数を求めるには、秒数をFrame.GameTimeScale(値が0より大きい場合のみ)で割ってください。ただし、これはタイマーが切れるまでタイムスケールが変わらない場合のみ正確になります。

タイムスケールには、2のべき乗の分数(例:FP._0_50/FP._0_25/FP._1 * 3 / 8)を使用すると、ゲームティックが正確な整数ティックに収まります。FP型は、ほとんどの小数を正確に保存できません(例:FP._0_10)。

タイマーの繰り返し

ゲームティックは、スローダウン時に整数ステップで進まないため、ターゲットのティックに正確に一致することはほぼありません。OvershootTicks/OvershootSecondsは、ゲームティックがターゲットをどれだけ超過したかを返します。(例:期限切れのタイミングでスポーンする場合などに使用できます)

ズレ無しに繰り返しタイマーを再起動するには、RestartにkeepOvershoot: trueを渡してください。これによって、新しいインターバルは、現在のゲームティックではなく、前回のターゲットから開始されます。タイムスケールが1以上の場合、ゲームティックは1フレーム内で複数のインターバルを通過する可能性があるため、すべてのインターバルを処理するためにループ内で再起動を行ってください:

C#

while (spawner->Interval.HasStoppedThisTick(frame)) {
    Spawn(frame, spawner->Interval.OvershootSeconds(frame));
    spawner->Interval.Restart(frame, keepOvershoot: true);
}

Delta Time

Frame.DeltaTimeも同様にタイムスケールに従う(GameTimeScale / UpdateRate)ため、Deltatimeで積分しているシステムは、ゲームの進行に合わせて一時停止したり速度が低下したりします。Frame.UnscaledDeltaTimeは、タイムスケールを無視したDeltaTimeを返します。Deltatimeを設定すると、かわりにタイムスケールが設定されます。

Deltatimeをタイムスケールから独立させて、ゲームティックのみをスケールするには、SimulationConfig.GameTimeScaleAffectsDeltaTimeを無効にしてください。

Last updated on

Back to top