Skip to content

6.7. fejezet: Időzítők és CallQueue ​


Bevezetés ​

A DayZ több mechanizmust biztosít a késleltetett és ismétlődő függvényhívásokhoz: ScriptCallQueue (az elsődleges rendszer), Timer, ScriptInvoker és WidgetFadeTimer. Ezek elengedhetetlenek a késleltetett logika ütemezéséhez, frissítési ciklusok létrehozásához és időzített események kezeléséhez a fő szál blokkolása nélkül. Ez a fejezet minden mechanizmust ismertet teljes API szignatúrákkal és használati mintákkal.


Hívási kategóriák ​

Minden időzítő és hívási sor rendszer egy hívási kategóriát igényel, amely meghatározza, hogy a késleltetett hívás mikor hajtódik végre a képkockán belül:

c
const int CALL_CATEGORY_SYSTEM   = 0;   // Rendszerszintű műveletek
const int CALL_CATEGORY_GUI      = 1;   // UI frissítések
const int CALL_CATEGORY_GAMEPLAY = 2;   // Játéklogika
const int CALL_CATEGORY_COUNT    = 3;   // Kategóriák összszáma

A sor elérése egy adott kategóriához:

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

ScriptCallQueue ​

Fájl: 2_GameLib/tools.c

Az elsődleges mechanizmus késleltetett függvényhívásokhoz. Támogatja az egyszeri késleltetéseket, ismétlődő hívásokat és azonnali következő-képkockás végrehajtást.

CallLater ​

c
void CallLater(func fn, int delay = 0, bool repeat = false,
               void param1 = NULL, void param2 = NULL,
               void param3 = NULL, void param4 = NULL);
ParaméterLeírás
fnA meghívandó függvény (metódushivatkozás: this.MyMethod)
delayKésleltetés milliszekundumban (0 = következő képkocka)
repeattrue = ismételt hívás delay időközönként; false = egyszeri hívás
param1..4Opcionális paraméterek, amelyeket a függvénynek adunk át

Példa --- egyszeri késleltetés:

c
// MyFunction hívása egyszer 5 másodperc múlva
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.MyFunction, 5000, false);

Példa --- ismétlődő hívás:

c
// UpdateLoop hívása minden 1 másodpercben, ismétlődően
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.UpdateLoop, 1000, true);

Példa --- paraméterekkel:

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

// Hívás paraméterekkel 2 másodperc múlva
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);

A függvényt a következő képkockán hajtja végre (delay = 0, nincs ismétlés). Rövidítés a CallLater(fn, 0, false) helyett.

Példa:

c
// Végrehajtás a következő képkockán
GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).Call(this.Initialize);

CallByName ​

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

Metódus hívása a string neve alapján a következő képkockán. Hasznos, ha a metódushivatkozás nem érhető el közvetlenül. Késleltetett vagy ismétlődő név szerinti híváshoz használd helyette a CallLaterByName-t:

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

Példa:

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

Remove ​

c
void Remove(func fn);

Egy ütemezett hívás eltávolítása. Elengedhetetlen az ismétlődő hívások leállításához és a megsemmisített objektumokon történő hívások megelőzéséhez.

Példa:

c
// Ismétlődő hívás leállítása
GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).Remove(this.UpdateLoop);

RemoveByName ​

c
void RemoveByName(Class obj, string fnName);

A CallByName-mel ütemezett hívás eltávolítása.

Tick ​

c
void Tick(float timeslice);

A motor minden képkockában belsőleg hívja. Soha nem szabad manuálisan meghívnod.


Timer ​

Fájl: 3_Game/tools/tools.c

Osztályalapú időzítő explicit start/stop életciklussal. Tisztább megoldás hosszú életű időzítőkhöz, amelyeket szüneteltetni vagy újraindítani kell.

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);
ParaméterLeírás
durationIdő másodpercben (nem milliszekundumban!)
objAz objektum, amelynek a metódusa meghívásra kerül
fn_nameMetódusnév stringként
paramsOpcionális Param objektum paraméterekkel
looptrue = ismétlés minden időtartam után

Példa --- egyszeri időzítő:

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("Időzítő befejeződött!");
}

Példa --- ismétlődő időzítő:

c
ref Timer m_UpdateTimer;

void StartUpdateLoop()
{
    m_UpdateTimer = new Timer(CALL_CATEGORY_GAMEPLAY);
    m_UpdateTimer.Run(1.0, this, "OnUpdate", null, true);  // Minden 1 másodpercben
}

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

Stop ​

c
void Stop();

Megállítja az időzítőt. Újraindítható egy újabb Run() hívással.

IsRunning ​

c
bool IsRunning();

true-t ad vissza, ha az időzítő jelenleg aktív.

Pause ​

c
void Pause();

Szünetelteti a futó időzítőt, megőrizve a hátralévő időt. Az időzítő a Continue()-val folytatható.

Continue ​

c
void Continue();

Folytatja a szüneteltetett időzítőt onnan, ahol megállt.

Példa --- szüneteltetés és folytatás:

A Timer-nek nincs IsPaused() metódusa. Mivel az IsRunning() csak akkor ad vissza true-t, amikor az időzítő aktív (és false-t, ha szünetel vagy leállt), használd ezt annak eldöntésére, hogy szüneteltetni vagy folytatni kell-e:

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

A hátralévő időt adja vissza másodpercben.

GetDuration ​

c
float GetDuration();

A Run() által beállított teljes időtartamot adja vissza.


ScriptInvoker ​

Fájl: 2_GameLib/tools.c

Egy esemény/delegált rendszer. A ScriptInvoker visszahívási függvények listáját tartja, és mindegyiket meghívja, amikor az Invoke() hívásra kerül. Ez a DayZ megfelelője a C# eseményeknek vagy a megfigyelő mintának.

Insert ​

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

Visszahívási függvény regisztrálása. Az opcionális flags argumentum elfogadja az EScriptInvokerInsertFlags.IMMEDIATE (alapértelmezett) vagy EScriptInvokerInsertFlags.UNIQUE értéket. Sikeres esetben true-t ad vissza.

Remove ​

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

Visszahívási függvény regisztrációjának törlése. Az opcionális flags argumentum alapértelmezett értéke EScriptInvokerRemoveFlags.ALL. Sikeres esetben true-t ad vissza.

Invoke ​

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

Az összes regisztrált függvény hívása a megadott paraméterekkel.

Count ​

c
int Count(func fn);

Azt adja vissza, hogy a megadott fn függvény hányszor van jelenleg regisztrálva az invokerben (nem az összes visszahívás teljes száma).

Clear ​

c
void Clear();

Az összes regisztrált visszahívás eltávolítása.

Példa --- egyéni eseményrendszer:

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

    void CompleteMission()
    {
        // Befejezési logika végrehajtása...

        // Összes figyelő értesítése
        m_OnMissionComplete.Invoke("MissionAlpha", 1500);
    }
}

class MyUI
{
    void Init(MyModule module)
    {
        // Feliratkozás az eseményre
        module.m_OnMissionComplete.Insert(this.OnMissionComplete);
    }

    void OnMissionComplete(string name, int reward)
    {
        Print(string.Format("Misszió %1 befejezve! Jutalom: %2", name, reward));
    }

    void Cleanup(MyModule module)
    {
        // Mindig iratkozz le a lógó hivatkozások megelőzése érdekében
        module.m_OnMissionComplete.Remove(this.OnMissionComplete);
    }
}

Frissítési sor ​

A motor képkockánkénti ScriptInvoker sorokat biztosít:

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

// Eltávolítás, ha kész
updater.Remove(this.OnFrame);

A frissítési sorba regisztrált függvények minden képkockában meghívódnak paraméterek nélkül. Ez hasznos képkockánkénti logikához az EntityEvent.FRAME használata nélkül.


WidgetFadeTimer ​

Fájl: 3_Game/tools/tools.c

Speciális időzítő widgetek be- és kihalványításához. A WidgetFadeTimer a TimerBase-ből származik, így örökli a Stop() és IsRunning() metódusokat.

c
class WidgetFadeTimer extends TimerBase
{
    void FadeIn(Widget w, float time, bool continue_ = false);
    void FadeOut(Widget w, float time, bool continue_ = false);
    // A Stop() és IsRunning() a TimerBase-ből öröklődik
}
ParaméterLeírás
wA halványítandó widget
timeA halványítás időtartama másodpercben
continue_Ha true, az aktuális alfa értéktől indul; egyébként 0-ról (behalványítás) vagy 1-ről (kihalványítás)

Használd az örökölt IsRunning() metódust annak ellenőrzésére, hogy egy halványítás éppen folyamatban van-e.

Példa:

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

    // Automatikus elrejtés 5 másodperc múlva
    GetGame().GetCallQueue(CALL_CATEGORY_GUI).CallLater(this.HideNotification, 5000, false);
}

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

GetRemainingTime (CallQueue) ​

A ScriptCallQueue lehetőséget biztosít arra is, hogy lekérdezd, mennyi idő van hátra (milliszekundumban) egy ütemezett hívásból. Két változata van --- az egyik függvényhivatkozással, a másik névvel kulcsozva:

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

Példa:

c
// Mennyi idő van hátra egy név szerint ütemezett hívásból
int remaining = GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).GetRemainingTimeByName(this, "MyCallback");
if (remaining > 0)
    Print(string.Format("A visszahívás %1 ms múlva fut le", remaining));

Gyakori minták ​

Időzítő akkumulátor (lassított OnUpdate) ​

Amikor képkockánkénti visszahívásod van, de lassabb ütemben szeretnéd futtatni a logikát:

c
class MyModule
{
    protected float m_UpdateAccumulator;
    protected const float UPDATE_INTERVAL = 2.0;  // Minden 2 másodpercben

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

        // Lassított logika itt
        DoPeriodicWork();
    }
}

Takarítási minta ​

Mindig távolítsd el az ütemezett hívásokat, amikor az objektumod megsemmisül, hogy megelőzd az összeomlásokat:

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()
    {
        // Periodikus munka
    }
}

Egyszeri késleltetett inicializálás ​

Gyakori minta rendszerek inicializálásához a világ teljes betöltése után:

c
void OnMissionStart()
{
    // Init késleltetése 1 másodperccel, hogy minden biztosan betöltődjön
    GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(this.DelayedInit, 1000, false);
}

void DelayedInit()
{
    // Biztonságosan hozzáférhetünk a világ objektumaihoz
}

Összefoglalás ​

MechanizmusFelhasználásIdőegység
CallLaterEgyszeri vagy ismétlődő késleltetett hívásokMilliszekundum
CallVégrehajtás a következő képkockánN/A (azonnali)
TimerOsztályalapú időzítő start/stop/hátralévő idővelMásodperc
ScriptInvokerEsemény/delegált (megfigyelő minta)N/A (kézi hívás)
WidgetFadeTimerWidget behalványítás/kihalványításMásodperc
GetUpdateQueue()Képkockánkénti visszahívás regisztrációN/A (minden képkocka)
FogalomLényeg
KategóriákCALL_CATEGORY_SYSTEM (0), GUI (1), GAMEPLAY (2)
Hívások eltávolításaMindig Remove() a destruktorban a lógó hivatkozások megelőzéséhez
Timer vs CallLaterTimer másodperc + osztályalapú; CallLater milliszekundum + funkcionális
ScriptInvokerInsert/Remove visszahívások, Invoke az összes kiváltásához

Legjobb gyakorlatok ​

  • Mindig hívd meg a Remove()-ot az ütemezett CallLater hívásokra a destruktorodban. Ha a tulajdonos objektum megsemmisül, miközben egy CallLater még függőben van, a motor egy törölt objektum metódusát hívja meg és összeomlik. Minden CallLater-nek kell egy megfelelő Remove() a destruktorban.
  • Használd a Timer-t (másodperc) hosszú életű időzítőkhöz szüneteltetéssel/folytatással, a CallLater-t (milliszekundum) tüzelj-és-felejtsd késleltetésekhez. A kettő keverése 1000-szeres időzítési hibákhoz vezet, mivel a Timer.Run() másodpercet, de a CallLater milliszekundumot használ.
  • Lassítsd az OnUpdate-et időzítő akkumulátorral az ismétlődő CallLater regisztrálása helyett. Az ismétlődő CallLater külön nyomon követett bejegyzést hoz létre a sorban, míg az akkumulátor minta (m_Acc += timeslice; if (m_Acc >= INTERVAL)) nulla terheléssel jár és könnyebben hangolható.
  • Iratkozz le a ScriptInvoker visszahívásokról, mielőtt a figyelő megsemmisül. A Remove() elfelejtése egy ScriptInvoker-en lógó függvényhivatkozást hagy hátra, ami összeomlást okoz, amikor az Invoke() kiváltódik.
  • Soha ne hívd meg manuálisan a Tick()-et a ScriptCallQueue-n. A motor automatikusan hívja minden képkockán. A manuális hívások duplán váltják ki az összes függőben lévő visszahívást.

Kompatibilitás és hatás ​

Mod kompatibilitás: Az időzítő rendszerek példányonkéntiák, így a modok ritkán ütköznek közvetlenül az időzítőkön. A kockázat a megosztott ScriptInvoker eseményeknél van, ahol több mod regisztrál visszahívásokat.

  • Betöltési sorrend: Az időzítő és CallQueue rendszerek betöltési sorrendtől függetlenek. Minden mod kezeli a saját időzítőit.
  • Modolt osztály ütközések: Nincs közvetlen ütközés, de ha két mod is felülírja az OnUpdate()-et ugyanazon az osztályon (pl. MissionServer) és az egyik elfelejti a super-t, a másik akkumulátor-alapú időzítői nem működnek.
  • Teljesítményhatás: Minden aktív CallLater repeat = true-val minden képkockán ellenőrzésre kerül. Több száz ismétlődő hívás rontja a szerver frissítési rátáját. Előnyben részesítsd a kevesebb időzítőt hosszabb időközökkel, vagy használd az akkumulátor mintát az OnUpdate-ben.
  • Szerver/Kliens: A CallLater és a Timer mindkét oldalon működik. Használd a CALL_CATEGORY_GAMEPLAY-t játéklogikához, a CALL_CATEGORY_GUI-t UI frissítésekhez (csak kliens), és a CALL_CATEGORY_SYSTEM-et alacsony szintű műveletekhez.

Valós modokban megfigyelt minták ​

Ezeket a mintákat professzionális DayZ modok forráskódjának tanulmányozásával igazoltuk.

MintaModFájl/Hely
Destruktor Remove() takarítás minden CallLater regisztrációhozCOTModulkezelő életciklus
ScriptInvoker eseménybusz modulok közötti értesítésekhezExpansionExpansionEventBus
Timer Pause()/Continue() használattal kilépési visszaszámláláshozVanillaMissionServer kilépési rendszer
Akkumulátor minta OnUpdate-ben 5 másodperces periodikus ellenőrzésekhezDabs FrameworkModul frissítés ütemezés

<< Előző: Értesítések | Időzítők és CallQueue | Következő: Fájl I/O és JSON >>

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