Skip to content

Kapitel 6.7: Timer und CallQueue ​


Einführung ​

DayZ bietet mehrere Mechanismen für verzögerte und wiederholende Funktionsaufrufe: ScriptCallQueue (das primäre System), Timer, ScriptInvoker und WidgetFadeTimer. Diese sind unverzichtbar für die Planung verzögerter Logik, die Erstellung von Update-Schleifen und die Verwaltung zeitgesteuerter Ereignisse, ohne den Haupt-Thread zu blockieren. Dieses Kapitel behandelt jeden Mechanismus mit vollständigen API-Signaturen und Nutzungsmustern.


Aufrufkategorien ​

Alle Timer- und CallQueue-Systeme erfordern eine Aufrufkategorie, die bestimmt, wann der verzögerte Aufruf innerhalb des Frames ausgeführt wird:

c
const int CALL_CATEGORY_SYSTEM   = 0;   // Operationen auf Systemebene
const int CALL_CATEGORY_GUI      = 1;   // UI-Updates
const int CALL_CATEGORY_GAMEPLAY = 2;   // Gameplay-Logik
const int CALL_CATEGORY_COUNT    = 3;   // Gesamtanzahl der Kategorien

Zugriff auf die Warteschlange einer Kategorie:

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

ScriptCallQueue ​

Datei: 2_GameLib/tools.c

Der primäre Mechanismus für verzögerte Funktionsaufrufe. Unterstützt einmalige Verzögerungen, wiederholende Aufrufe und sofortige Ausführung im nächsten Frame.

CallLater ​

c
void CallLater(func fn, int delay = 0, bool repeat = false,
               void param1 = NULL, void param2 = NULL,
               void param3 = NULL, void param4 = NULL);
ParameterBeschreibung
fnDie aufzurufende Funktion (Methodenreferenz: this.MyMethod)
delayVerzögerung in Millisekunden (0 = nächster Frame)
repeattrue = wiederholter Aufruf im delay-Intervall; false = einmaliger Aufruf
param1..4Optionale Parameter, die an die Funktion übergeben werden

Beispiel --- einmaliger verzögerter Aufruf:

c
// MyFunction einmal nach 5 Sekunden aufrufen
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.MyFunction, 5000, false);

Beispiel --- wiederholender Aufruf:

c
// UpdateLoop jede Sekunde aufrufen, wiederholend
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.UpdateLoop, 1000, true);

Beispiel --- mit Parametern:

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

// Mit Parametern nach 2 Sekunden aufrufen
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(
    this.ShowMessage, 2000, false, "Hallo!", ARGB(255, 255, 0, 0)
);

Call ​

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

Führt die Funktion im nächsten Frame aus (delay = 0, keine Wiederholung). Kurzform für CallLater(fn, 0, false).

Beispiel:

c
// Im nächsten Frame ausführen
GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).Call(this.Initialize);

CallByName ​

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

Ruft eine Methode über ihren String-Namen im nächsten Frame auf. Nützlich, wenn die Methodenreferenz nicht direkt verfügbar ist. Für einen verzögerten oder wiederholenden Aufruf über den Namen verwenden Sie stattdessen CallLaterByName:

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

Beispiel:

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

Remove ​

c
void Remove(func fn);

Entfernt einen geplanten Aufruf. Unverzichtbar zum Stoppen wiederholender Aufrufe und zur Verhinderung von Aufrufen auf zerstörten Objekten.

Beispiel:

c
// Einen wiederholenden Aufruf stoppen
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).Remove(this.UpdateLoop);

RemoveByName ​

c
void RemoveByName(Class obj, string fnName);

Entfernt einen über CallByName geplanten Aufruf.

Tick ​

c
void Tick(float timeslice);

Wird intern von der Engine jeden Frame aufgerufen. Sie sollten dies niemals manuell aufrufen müssen.


Timer ​

Datei: 3_Game/tools/tools.c

Ein klassenbasierter Timer mit explizitem Start/Stopp-Lebenszyklus. Übersichtlicher für langlebige Timer, die pausiert oder neu gestartet werden müssen.

Konstruktor ​

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);
ParameterBeschreibung
durationZeit in Sekunden (nicht Millisekunden!)
objDas Objekt, dessen Methode aufgerufen wird
fn_nameMethodenname als String
paramsOptionales Param-Objekt mit Parametern
looptrue = nach jeder Dauer wiederholen

Beispiel --- einmaliger Timer:

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 abgeschlossen!");
}

Beispiel --- wiederholender Timer:

c
ref Timer m_UpdateTimer;

void StartUpdateLoop()
{
    m_UpdateTimer = new Timer(CALL_CATEGORY_GAMEPLAY);
    m_UpdateTimer.Run(1.0, this, "OnUpdate", null, true);  // Jede Sekunde
}

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

Stop ​

c
void Stop();

Stoppt den Timer. Kann mit einem weiteren Run()-Aufruf neu gestartet werden.

IsRunning ​

c
bool IsRunning();

Gibt true zurück, wenn der Timer aktuell aktiv ist.

Pause ​

c
void Pause();

Pausiert einen laufenden Timer unter Beibehaltung der verbleibenden Zeit. Der Timer kann mit Continue() fortgesetzt werden.

Continue ​

c
void Continue();

Setzt einen pausierten Timer an der Stelle fort, an der er gestoppt wurde.

Beispiel --- Pause und Fortsetzen:

Timer hat keine IsPaused()-Methode. Da IsRunning() nur dann true zurückgibt, während der Timer aktiv ist (und false, sobald er pausiert oder gestoppt wurde), verwenden Sie diese Methode, um zu entscheiden, ob pausiert oder fortgesetzt werden soll:

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();

Gibt die verbleibende Zeit in Sekunden zurück.

GetDuration ​

c
float GetDuration();

Gibt die gesamte durch Run() gesetzte Dauer zurück.


ScriptInvoker ​

Datei: 2_GameLib/tools.c

Ein Ereignis-/Delegaten-System. ScriptInvoker hält eine Liste von Callback-Funktionen und ruft alle auf, wenn Invoke() aufgerufen wird. Dies ist DayZ's Äquivalent zu C#-Events oder dem Beobachter-Muster.

Insert ​

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

Registriert eine Callback-Funktion. Das optionale Argument flags akzeptiert EScriptInvokerInsertFlags.IMMEDIATE (Standard) oder EScriptInvokerInsertFlags.UNIQUE. Gibt bei Erfolg true zurück.

Remove ​

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

Deregistriert eine Callback-Funktion. Das optionale Argument flags ist standardmäßig EScriptInvokerRemoveFlags.ALL. Gibt bei Erfolg true zurück.

Invoke ​

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

Ruft alle registrierten Funktionen mit den übergebenen Parametern auf.

Count ​

c
int Count(func fn);

Gibt zurück, wie oft die angegebene Funktion fn aktuell im Invoker registriert ist (nicht eine Gesamtanzahl aller Callbacks).

Clear ​

c
void Clear();

Entfernt alle registrierten Callbacks.

Beispiel --- benutzerdefiniertes Ereignissystem:

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

    void CompleteMission()
    {
        // Abschlusslogik durchführen...

        // Alle Listener benachrichtigen
        m_OnMissionComplete.Invoke("MissionAlpha", 1500);
    }
}

class MyUI
{
    void Init(MyModule module)
    {
        // Ereignis abonnieren
        module.m_OnMissionComplete.Insert(this.OnMissionComplete);
    }

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

    void Cleanup(MyModule module)
    {
        // Immer abmelden, um hängende Referenzen zu verhindern
        module.m_OnMissionComplete.Remove(this.OnMissionComplete);
    }
}

Update-Warteschlange ​

Die Engine stellt Frame-weise ScriptInvoker-Warteschlangen bereit:

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

// Entfernen wenn fertig
updater.Remove(this.OnFrame);

Funktionen, die in der Update-Warteschlange registriert sind, werden jeden Frame ohne Parameter aufgerufen. Dies ist nützlich für Frame-weise Logik ohne Verwendung von EntityEvent.FRAME.


WidgetFadeTimer ​

Datei: 3_Game/tools/tools.c

Ein spezialisierter Timer zum Ein- und Ausblenden von Widgets. WidgetFadeTimer erweitert TimerBase, daher erbt er Stop() und 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() und IsRunning() werden von TimerBase geerbt
}
ParameterBeschreibung
wDas Widget zum Ein-/Ausblenden
timeDauer der Blende in Sekunden
continue_Wenn true, vom aktuellen Alpha-Wert starten; sonst bei 0 (Einblenden) oder 1 (Ausblenden) beginnen

Verwenden Sie das geerbte IsRunning(), um zu prüfen, ob aktuell eine Blende läuft.

Beispiel:

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);

    // Nach 5 Sekunden automatisch ausblenden
    GetGame().GetCallQueue(CALL_CATEGORY_GUI).CallLater(this.HideNotification, 5000, false);
}

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

GetRemainingTime (CallQueue) ​

Die ScriptCallQueue bietet auch eine Möglichkeit, die verbleibende Zeit (in Millisekunden) eines geplanten Aufrufs abzufragen. Es gibt zwei Varianten --- eine über die Funktionsreferenz und eine über den Namen:

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

Beispiel:

c
// Verbleibende Zeit eines über den Namen geplanten Aufrufs abfragen
int remaining = GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).GetRemainingTimeByName(this, "MyCallback");
if (remaining > 0)
    Print(string.Format("Callback feuert in %1 ms", remaining));

Häufige Muster ​

Timer-Akkumulator (Gedrosseltes OnUpdate) ​

Wenn Sie einen Frame-weisen Callback haben, die Logik aber langsamer ausführen möchten:

c
class MyModule
{
    protected float m_UpdateAccumulator;
    protected const float UPDATE_INTERVAL = 2.0;  // Alle 2 Sekunden

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

        // Gedrosselte Logik hier
        DoPeriodicWork();
    }
}

Aufräum-Muster ​

Entfernen Sie geplante Aufrufe immer, wenn Ihr Objekt zerstört wird, um Abstürze zu verhindern:

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()
    {
        // Periodische Arbeit
    }
}

Einmaliges verzögertes Init ​

Ein häufiges Muster zum Initialisieren von Systemen, nachdem die Welt vollständig geladen ist:

c
void OnMissionStart()
{
    // Init um 1 Sekunde verzögern, um sicherzustellen, dass alles geladen ist
    GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(this.DelayedInit, 1000, false);
}

void DelayedInit()
{
    // Sicherer Zugriff auf Weltobjekte möglich
}

Zusammenfassung ​

MechanismusAnwendungsfallZeiteinheit
CallLaterEinmalige oder wiederholende verzögerte AufrufeMillisekunden
CallIm nächsten Frame ausführenN/A (sofort)
TimerKlassenbasierter Timer mit Start/Stopp/VerbleibendSekunden
ScriptInvokerEreignis/Delegat (Beobachter-Muster)N/A (manueller Aufruf)
WidgetFadeTimerWidget Ein-/AusblendenSekunden
GetUpdateQueue()Frame-weise Callback-RegistrierungN/A (jeden Frame)
KonzeptKernpunkt
KategorienCALL_CATEGORY_SYSTEM (0), GUI (1), GAMEPLAY (2)
Aufrufe entfernenImmer Remove() im Destruktor, um hängende Referenzen zu verhindern
Timer vs CallLaterTimer ist Sekunden + klassenbasiert; CallLater ist Millisekunden + funktional
ScriptInvokerInsert/Remove für Callbacks, Invoke zum Auslösen aller

Bewährte Praktiken ​

  • Entfernen Sie geplante CallLater-Aufrufe immer in Ihrem Destruktor mit Remove(). Wenn das besitzende Objekt zerstört wird, während ein CallLater noch aussteht, ruft die Engine eine Methode auf einem gelöschten Objekt auf und stürzt ab. Jedes CallLater muss ein passendes Remove() im Destruktor haben.
  • Verwenden Sie Timer (Sekunden) für langlebige Timer mit Pause/Fortsetzung, CallLater (Millisekunden) für Fire-and-Forget-Verzögerungen. Verwechslungen führen zu 1000-fachen Timing-Fehlern, da Timer.Run() Sekunden, aber CallLater Millisekunden verwendet.
  • Drosseln Sie OnUpdate mit einem Timer-Akkumulator anstatt ein wiederholendes CallLater zu registrieren. Ein CallLater mit Wiederholung erstellt einen separaten verfolgten Eintrag in der Warteschlange, während ein Akkumulator-Muster (m_Acc += timeslice; if (m_Acc >= INTERVAL)) null Overhead hat und leichter anzupassen ist.
  • Melden Sie ScriptInvoker-Callbacks ab, bevor der Listener zerstört wird. Vergessenes Aufrufen von Remove() auf einem ScriptInvoker hinterlässt eine hängende Funktionsreferenz, die beim Auslösen von Invoke() abstürzt.
  • Rufen Sie niemals Tick() manuell auf ScriptCallQueue auf. Die Engine ruft es automatisch jeden Frame auf. Manuelle Aufrufe lösen alle ausstehenden Callbacks doppelt aus.

Kompatibilität und Auswirkungen ​

Mod-Kompatibilität: Timer-Systeme sind pro Instanz, daher kollidieren Mods selten direkt bei Timern. Das Risiko liegt bei gemeinsamen ScriptInvoker-Ereignissen, bei denen mehrere Mods Callbacks registrieren.

  • Ladereihenfolge: Timer- und CallQueue-Systeme sind ladereihenfolge-unabhängig. Jede Mod verwaltet ihre eigenen Timer.
  • Modded-Class-Konflikte: Keine direkten Konflikte, aber wenn zwei Mods beide OnUpdate() auf derselben Klasse (z.B. MissionServer) überschreiben und eine super vergisst, funktionieren die akkumulator-basierten Timer der anderen nicht mehr.
  • Leistungsauswirkung: Jedes aktive CallLater mit repeat = true wird jeden Frame geprüft. Hunderte wiederholende Aufrufe verschlechtern die Server-Tickrate. Bevorzugen Sie weniger Timer mit längeren Intervallen, oder verwenden Sie das Akkumulator-Muster in OnUpdate.
  • Server/Client: CallLater und Timer funktionieren auf beiden Seiten. Verwenden Sie CALL_CATEGORY_GAMEPLAY für Spiellogik, CALL_CATEGORY_GUI für UI-Updates (nur Client) und CALL_CATEGORY_SYSTEM für Low-Level-Operationen.

In echten Mods beobachtet ​

Diese Muster wurden durch das Studium des Quellcodes professioneller DayZ-Mods bestätigt.

MusterModDatei/Ort
Destruktor-Remove()-Aufräumung für jede CallLater-RegistrierungCOTModul-Manager-Lebenszyklus
ScriptInvoker-Ereignisbus für modulübergreifende BenachrichtigungenExpansionExpansionEventBus
Timer mit Pause()/Continue() für Logout-CountdownVanillaMissionServer-Logout-System
Akkumulator-Muster in OnUpdate für 5-Sekunden-PeriodikprüfungenDabs FrameworkModul-Tick-Planung

<< Zurück: Benachrichtigungen | Timer und CallQueue | Weiter: Datei-E/A und JSON >>

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