Skip to content

第6.7章: タイマーとCallQueue ​


はじめに ​

DayZは遅延呼び出しおよび繰り返し関数呼び出しのためのいくつかのメカニズムを提供しています:ScriptCallQueue(主要なシステム)、Timer、ScriptInvoker、WidgetFadeTimerです。これらは、メインスレッドをブロックせずに遅延ロジックのスケジューリング、更新ループの作成、タイマーイベントの管理に不可欠です。この章では、各メカニズムの完全なAPIシグネチャと使用パターンをカバーします。


コールカテゴリ ​

すべてのタイマーとコールキューシステムは、フレーム内での遅延呼び出しの実行タイミングを決定するコールカテゴリを必要とします:

c
const int CALL_CATEGORY_SYSTEM   = 0;   // システムレベルの操作
const int CALL_CATEGORY_GUI      = 1;   // UI更新
const int CALL_CATEGORY_GAMEPLAY = 2;   // ゲームプレイロジック
const int CALL_CATEGORY_COUNT    = 3;   // カテゴリの総数

カテゴリのキューにアクセスする方法:

c
ScriptCallQueue  queue   = GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY);
ScriptInvoker    updater = GetGame().GetUpdateQueue(CALL_CATEGORY_GAMEPLAY);
TimerQueue       timers  = GetGame().GetTimerQueue(CALL_CATEGORY_GAMEPLAY);

ScriptCallQueue ​

ファイル: 2_GameLib/tools.c

遅延関数呼び出しの主要なメカニズムです。ワンショット遅延、繰り返し呼び出し、即時次フレーム実行をサポートしています。

CallLater ​

c
void CallLater(func fn, int delay = 0, bool repeat = false,
               void param1 = NULL, void param2 = NULL,
               void param3 = NULL, void param4 = NULL);
パラメータ説明
fn呼び出す関数(メソッド参照: this.MyMethod)
delayミリ秒単位の遅延(0 = 次のフレーム)
repeattrue = delay間隔で繰り返し呼び出し; false = 1回だけ呼び出し
param1..4関数に渡されるオプションパラメータ

例 --- ワンショット遅延:

c
// 5秒後にMyFunctionを1回呼び出す
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.MyFunction, 5000, false);

例 --- 繰り返し呼び出し:

c
// 1秒ごとにUpdateLoopを繰り返し呼び出す
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.UpdateLoop, 1000, true);

例 --- パラメータ付き:

c
void ShowMessage(string text, int color)
{
    Print(text);
}

// 2秒後にパラメータ付きで呼び出す
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(
    this.ShowMessage, 2000, false, "Hello!", ARGB(255, 255, 0, 0)
);

Call ​

c
void Call(func fn, void param1 = NULL, void param2 = NULL,
          void param3 = NULL, void param4 = NULL);

次のフレームで関数を実行します(delay = 0、繰り返しなし)。CallLater(fn, 0, false)の省略形です。

例:

c
// 次のフレームで実行
GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).Call(this.Initialize);

CallByName ​

c
void CallByName(Class obj, string fnName, Param params = NULL);

文字列名でメソッドを次のフレームで呼び出します。メソッド参照が直接利用できない場合に便利です。遅延または繰り返しの名前指定呼び出しには、代わりにCallLaterByNameを使用してください:

c
void CallLaterByName(Class obj, string fnName, int delay = 0, bool repeat = false,
                     Param params = NULL);

例:

c
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLaterByName(
    myObject, "OnTimerExpired", 3000, false
);

Remove ​

c
void Remove(func fn);

スケジュールされた呼び出しを削除します。繰り返し呼び出しの停止や、破棄されたオブジェクトでの呼び出しを防ぐために不可欠です。

例:

c
// 繰り返し呼び出しを停止する
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).Remove(this.UpdateLoop);

RemoveByName ​

c
void RemoveByName(Class obj, string fnName);

CallByNameでスケジュールされた呼び出しを削除します。

Tick ​

c
void Tick(float timeslice);

エンジンによって各フレームで内部的に呼び出されます。手動で呼び出す必要はありません。


Timer ​

ファイル: 3_Game/tools/tools.c

明示的なスタート/ストップのライフサイクルを持つクラスベースのタイマーです。一時停止や再開が必要な長期間のタイマーに最適です。

コンストラクタ ​

c
void Timer(int category = CALL_CATEGORY_SYSTEM);

Run ​

c
void Run(float duration, Managed obj, string fn_name, Param params = NULL, bool loop = false);
パラメータ説明
duration秒単位の時間(ミリ秒ではありません!)
objメソッドが呼び出されるオブジェクト
fn_name文字列としてのメソッド名
paramsパラメータを持つオプションのParamオブジェクト
looptrue = 各duration後に繰り返し

例 --- ワンショットタイマー:

c
ref Timer m_Timer;

void StartTimer()
{
    m_Timer = new Timer(CALL_CATEGORY_GAMEPLAY);
    m_Timer.Run(5.0, this, "OnTimerComplete", null, false);
}

void OnTimerComplete()
{
    Print("Timer finished!");
}

例 --- 繰り返しタイマー:

c
ref Timer m_UpdateTimer;

void StartUpdateLoop()
{
    m_UpdateTimer = new Timer(CALL_CATEGORY_GAMEPLAY);
    m_UpdateTimer.Run(1.0, this, "OnUpdate", null, true);  // 1秒ごと
}

void StopUpdateLoop()
{
    if (m_UpdateTimer && m_UpdateTimer.IsRunning())
        m_UpdateTimer.Stop();
}

Stop ​

c
void Stop();

タイマーを停止します。別のRun()呼び出しで再開できます。

IsRunning ​

c
bool IsRunning();

タイマーが現在アクティブな場合にtrueを返します。

Pause ​

c
void Pause();

実行中のタイマーを一時停止し、残り時間を保持します。Continue()で再開できます。

Continue ​

c
void Continue();

一時停止されたタイマーを中断した場所から再開します。

例 --- 一時停止と再開:

TimerにはIsPaused()メソッドはありません。IsRunning()はタイマーがアクティブな間のみtrueを返す(一時停止または停止するとfalseになる)ため、これを使って一時停止するか再開するかを判断してください:

c
ref Timer m_Timer;

void StartTimer()
{
    m_Timer = new Timer(CALL_CATEGORY_GAMEPLAY);
    m_Timer.Run(10.0, this, "OnTimerComplete", null, false);
}

void TogglePause()
{
    if (m_Timer.IsRunning())
        m_Timer.Pause();
    else
        m_Timer.Continue();
}

GetRemaining ​

c
float GetRemaining();

秒単位の残り時間を返します。

GetDuration ​

c
float GetDuration();

Run()で設定された総時間を返します。


ScriptInvoker ​

ファイル: 2_GameLib/tools.c

イベント/デリゲートシステムです。ScriptInvokerはコールバック関数のリストを保持し、Invoke()が呼び出されるとすべてを実行します。これはDayZにおけるC#イベントやオブザーバーパターンに相当するものです。

Insert ​

c
bool Insert(func fn, int flags = EScriptInvokerInsertFlags.IMMEDIATE);

コールバック関数を登録します。オプションのflags引数はEScriptInvokerInsertFlags.IMMEDIATE(デフォルト)またはEScriptInvokerInsertFlags.UNIQUEを受け付けます。成功時にtrueを返します。

Remove ​

c
bool Remove(func fn, int flags = EScriptInvokerRemoveFlags.ALL);

コールバック関数の登録を解除します。オプションのflags引数はデフォルトでEScriptInvokerRemoveFlags.ALLです。成功時にtrueを返します。

Invoke ​

c
void Invoke(void param1 = NULL, void param2 = NULL,
            void param3 = NULL, void param4 = NULL);

提供されたパラメータで登録されたすべての関数を呼び出します。

Count ​

c
int Count(func fn);

指定した関数fnが現在インボーカーに登録されている回数を返します(すべてのコールバックの合計数ではありません)。

Clear ​

c
void Clear();

登録されたすべてのコールバックを削除します。

例 --- カスタムイベントシステム:

c
class MyModule
{
    ref ScriptInvoker m_OnMissionComplete = new ScriptInvoker();

    void CompleteMission()
    {
        // 完了ロジックを実行...

        // すべてのリスナーに通知
        m_OnMissionComplete.Invoke("MissionAlpha", 1500);
    }
}

class MyUI
{
    void Init(MyModule module)
    {
        // イベントをサブスクライブ
        module.m_OnMissionComplete.Insert(this.OnMissionComplete);
    }

    void OnMissionComplete(string name, int reward)
    {
        Print(string.Format("Mission %1 complete! Reward: %2", name, reward));
    }

    void Cleanup(MyModule module)
    {
        // ダングリング参照を防ぐため、常にサブスクライブを解除する
        module.m_OnMissionComplete.Remove(this.OnMissionComplete);
    }
}

Update Queue ​

エンジンはフレームごとのScriptInvokerキューを提供します:

c
ScriptInvoker updater = GetGame().GetUpdateQueue(CALL_CATEGORY_GAMEPLAY);
updater.Insert(this.OnFrame);

// 完了したら削除
updater.Remove(this.OnFrame);

更新キューに登録された関数は、パラメータなしで毎フレーム呼び出されます。これはEntityEvent.FRAMEを使用せずにフレームごとのロジックに便利です。


WidgetFadeTimer ​

ファイル: 3_Game/tools/tools.c

ウィジェットのフェードイン/フェードアウト用の特殊なタイマーです。WidgetFadeTimerはTimerBaseを継承しているため、Stop()とIsRunning()を継承します。

c
class WidgetFadeTimer extends TimerBase
{
    void FadeIn(Widget w, float time, bool continue_ = false);
    void FadeOut(Widget w, float time, bool continue_ = false);
    // Stop()とIsRunning()はTimerBaseから継承されます
}
パラメータ説明
wフェードするウィジェット
timeフェードの持続時間(秒)
continue_trueの場合、現在のアルファから開始。それ以外の場合は0(フェードイン)または1(フェードアウト)から開始

フェードが現在進行中かどうかを確認するには、継承されたIsRunning()を使用してください。

例:

c
ref WidgetFadeTimer m_FadeTimer;
Widget m_NotificationPanel;

void ShowNotification()
{
    m_NotificationPanel.Show(true);
    m_FadeTimer = new WidgetFadeTimer;
    m_FadeTimer.FadeIn(m_NotificationPanel, 0.3);

    // 5秒後に自動的に非表示
    GetGame().GetCallQueue(CALL_CATEGORY_GUI).CallLater(this.HideNotification, 5000, false);
}

void HideNotification()
{
    m_FadeTimer.FadeOut(m_NotificationPanel, 0.5);
}

GetRemainingTime(CallQueue) ​

ScriptCallQueueは、スケジュールされた呼び出しの残り時間(ミリ秒単位)を照会する方法も提供しています。2つのバリアントがあり、1つは関数参照でキー指定し、もう1つは名前でキー指定します:

c
int GetRemainingTime(func fn);
int GetRemainingTimeByName(Class obj, string fnName);

例:

c
// 名前でスケジュールされた呼び出しの残り時間を取得
int remaining = GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).GetRemainingTimeByName(this, "MyCallback");
if (remaining > 0)
    Print(string.Format("Callback fires in %1 ms", remaining));

一般的なパターン ​

タイマーアキュムレータ(スロットリングされたOnUpdate) ​

フレームごとのコールバックがあるが、より遅いレートでロジックを実行したい場合:

c
class MyModule
{
    protected float m_UpdateAccumulator;
    protected const float UPDATE_INTERVAL = 2.0;  // 2秒ごと

    void OnUpdate(float timeslice)
    {
        m_UpdateAccumulator += timeslice;
        if (m_UpdateAccumulator < UPDATE_INTERVAL)
            return;
        m_UpdateAccumulator = 0;

        // スロットリングされたロジックをここに
        DoPeriodicWork();
    }
}

クリーンアップパターン ​

クラッシュを防ぐために、オブジェクトが破棄される時に常にスケジュールされた呼び出しを削除してください:

c
class MyManager
{
    void MyManager()
    {
        GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.Tick, 1000, true);
    }

    void ~MyManager()
    {
        GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).Remove(this.Tick);
    }

    void Tick()
    {
        // 定期的な作業
    }
}

ワンショット遅延初期化 ​

ワールドが完全に読み込まれた後にシステムを初期化するための一般的なパターンです:

c
void OnMissionStart()
{
    // すべてが読み込まれたことを確認するために、初期化を1秒遅延
    GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(this.DelayedInit, 1000, false);
}

void DelayedInit()
{
    // ワールドオブジェクトに安全にアクセスできるようになりました
}

まとめ ​

メカニズム使用ケース時間単位
CallLaterワンショットまたは繰り返しの遅延呼び出しミリ秒
Call次のフレームで実行N/A(即時)
Timerスタート/ストップ/残り時間を持つクラスベースのタイマー秒
ScriptInvokerイベント/デリゲート(オブザーバーパターン)N/A(手動Invoke)
WidgetFadeTimerウィジェットのフェードイン/フェードアウト秒
GetUpdateQueue()フレームごとのコールバック登録N/A(毎フレーム)
概念キーポイント
カテゴリCALL_CATEGORY_SYSTEM (0)、GUI (1)、GAMEPLAY (2)
呼び出しの削除ダングリング参照を防ぐため、常にデストラクタでRemove()を行う
Timer vs CallLaterTimerは秒+クラスベース; CallLaterはミリ秒+関数型
ScriptInvokerコールバックをInsert/Remove、Invokeですべて発火

ベストプラクティス ​

  • デストラクタでスケジュールされたCallLater呼び出しを常にRemove()してください。 所有オブジェクトがCallLaterがまだ保留中の間に破棄されると、エンジンは削除されたオブジェクトのメソッドを呼び出してクラッシュします。すべてのCallLaterにはデストラクタで対応するRemove()が必要です。
  • 一時停止/再開が必要な長期間のタイマーにはTimer(秒)を、ファイア・アンド・フォーゲットの遅延にはCallLater(ミリ秒)を使用してください。 これらを混同すると、Timer.Run()は秒を使用しますがCallLaterはミリ秒を使用するため、1000倍のタイミングバグにつながります。
  • 繰り返しのCallLaterを登録する代わりに、タイマーアキュムレータでOnUpdateをスロットリングしてください。 repeatのCallLaterはキューに別の追跡エントリを作成しますが、アキュムレータパターン(m_Acc += timeslice; if (m_Acc >= INTERVAL))はオーバーヘッドがゼロで、チューニングが容易です。
  • リスナーが破棄される前にScriptInvokerコールバックのサブスクライブを解除してください。 ScriptInvokerでRemove()の呼び出しを忘れると、Invoke()が発火した時にクラッシュするダングリング関数参照が残ります。
  • ScriptCallQueueのTick()を手動で呼び出さないでください。 エンジンが各フレームで自動的に呼び出します。手動呼び出しはすべての保留中のコールバックを二重に発火させます。

互換性と影響 ​

Modの互換性: タイマーシステムはインスタンスごとであるため、Modがタイマーで直接競合することはほとんどありません。リスクは、複数のModがコールバックを登録する共有ScriptInvokerイベントにあります。

  • 読み込み順序: タイマーとCallQueueシステムは読み込み順序に依存しません。各Modが独自のタイマーを管理します。
  • modded classの競合: 直接的な競合はありませんが、2つのModが同じクラス(例:MissionServer)のOnUpdate()をオーバーライドし、一方がsuperを忘れると、もう一方のアキュムレータベースのタイマーが停止します。
  • パフォーマンスへの影響: repeat = trueのアクティブな各CallLaterは毎フレームチェックされます。数百の繰り返し呼び出しはサーバーのティックレートを低下させます。より長い間隔のタイマーを少なくするか、OnUpdateでアキュムレータパターンを使用してください。
  • サーバー/クライアント: CallLaterとTimerは両側で動作します。ゲームロジックにはCALL_CATEGORY_GAMEPLAY、UI更新にはCALL_CATEGORY_GUI(クライアントのみ)、低レベル操作にはCALL_CATEGORY_SYSTEMを使用してください。

実際のModで確認されたパターン ​

これらのパターンはプロのDayZ Modのソースコードを調査して確認されました。

パターンModファイル/場所
すべてのCallLater登録に対するデストラクタでのRemove()クリーンアップCOTモジュールマネージャーのライフサイクル
クロスモジュール通知用のScriptInvokerイベントバスExpansionExpansionEventBus
ログアウトカウントダウンでのPause()/Continue()を持つTimerバニラMissionServerのログアウトシステム
5秒周期チェック用のOnUpdateでのアキュムレータパターンDabs Frameworkモジュールティックスケジューリング

<< 前へ: 通知 | タイマーとCallQueue | 次へ: ファイルI/OとJSON >>

Released under CC BY-SA 4.0 | Code examples under MIT License