Ergebnis 1 bis 9 von 9

Thema: Theorie : Wie gescriptete Ereignisse in Spiel einbauen?

  1. #1

    Theorie : Wie gescriptete Ereignisse in Spiel einbauen?

    Hallöchen,

    Jetzt sind mal wieder die Denker unter euch gefragt, es geht um einen Denkansatz für ein Problem.
    Ich schreibe gerade ein 2D Spiel in C++ und OpenGL. Dazu habe ich einen Karteneditor geschrieben und die Karten und die Spiel-Engine zum Laden der Level und zum Rumlaufen samt Kollision funktioniert soweit schon.
    Jetzt sollen erste anklickbare Gegenstände hinzukommen, z.B. Schalter, die ein bestimmtes Ereignis auslösen.
    Dabei tritt die Frage auf, wie realisiert man sowas am besten? Im Moment mache ich es so, dass ich im Karteneditor den Schalter platziere und ihm eine einmalige ID vergebe. Diese kann ich in der Hauptschleife der Spiel-Engine abfragen und damit herausfinden, welches Ereignis (also eine gescriptete Funktion) sich bei Druck auf "Enter" im Sichtbereich des Spielers befinden und ausgelöst werden sollen. Soweit ist alles klar. Die Problematik ist jetzt, wie lasse ich diese Funktionen auslösen und wie verfahre ich bei Script-Ereignissen die einige Variablen benötigen und eventuell eine andere Grafik für das Objekt laden. (z.B. Schalter ein und aus-Grafik)
    Ich hab's testweise jetzt so gemacht, dass ich ein Bool-Array mit je einem Element pro Ereignis erstellt habe. Der Wert wird beim Auslösen auf True gesetzt. Dann wird in der Hauptschleife des Spiels einfach dieser Wert jedes Array-Elements abgefragt und der Scriptinhalt des Events ausgeführt, sobald dort ein True auftritt. Bei einfachen Events, die nur schnell ein paar Variablen ändern ist das kein Problem, doch wenn sich die Grafik des Objekts ändern soll, dann kommt das Problem auf, dass bei erneutem Laden des Levels (nach verlassen und wieder betreten) die ursprüngliche Objekt-Grafik geladen wird. Es wird eben wieder alles auf Normalzustand zurückgesetzt und die Scriptänderungen verfallen, denn die Objekte werden ja aus der Karte wieder in den Ur-Zustand geladen. Und ein Kartenwechsel ist bei dieser Art spiel leider sehr häufig gegeben.
    Da kommt die Frage auf, ob diese Events nach dem Aktivieren immer im Speicher verbleiben müssen und ständig "aktiv", um beim Level-Laden dafür sorgen zu können, dass sie immer noch ausgeführt sind. Oder eben, wie man es sonst geschickt lösen könnte. Schließlich verbrauchen diese Events dann auch Rechenleistung und wenn sie ewig im Speicher verbleiben wird dieser auch irgendwann damit überfüllt sein. Und wenn gegen Ende des Spiels ewig noch 1000 Schleifen mit Scriptinhalt von Schaltern aus vergangenen Karten ausgeführt werden, geht das sicher extrem in die darzustellenden Frames pro Sekunde.
    Manche Scripte sollen auch einen Timer vorhersehen, dass erst nach X Sekunden etwas passiert. Daher brauchen sie gültige Variablen über längere Zeit hinweg, die danach aber aus dem Speicher gelöscht werden können, nachdem das Event beendet ist. Das bedeutet, die Funktion muss sich selbst über mehrere Durchläufe halten können, einige Variablen errechnen, und sich danach selbst abstellen und den Speicher freigeben. Als kleinen erschwerenden Zusatz darf der Schalter, der den Timer gesetzt hat, dann auch noch nur einmalig ausgeführt werden. Damit muss sich das Event nach Beenden und Speicherfreigeben also doch irgendwie merken, dass es bereits ausgeführt worden ist und nicht nochmal starten darf.
    Also wie könnte man diese ganze Script-Event-Sache realisieren? Vielleicht bin ich mit dieser Script-Bool-Array-Sache ja total auf dem Holzweg und es gibt viel effizientere Wege, so etwas zu machen?
    Ich bin für jeden Pseudocode oder für Denkansätze dankbar. Es muss keine direkte Lösung sein, einfach etwas, was mich irgendwie weiterhelfen könnte.

    Grüße,

    Ynnus

  2. #2
    Du könntest eine Art Diff der Objekte im Speicher halten - sprich: Wenn du die Map verläßt werden die Daten gesichert, die gegenüber den Standarddaten verändert wurden. Damit würdest du den Speicherbedarf reduzieren können.

    Das könnte man so realisieren, daß du einen Queue von Pointern auf Structs mit Diffs hast, wobei jeder Diff angibt, zu welchem Objekt er gehört. Beim Laden der Map wird dann der Queue durchlaufen und die Objekte werden initialisiert. Die einzelnen Queues sind in einem Baum organisiert oder liegen als Member der Mapobjekte vor.
    Falls das zu viel Aufwand auf einmal ist und die Maps hinreichend groß sind könnte man auch die Map partitionieren und die Objekte erst nach und nach initialisieren (alternativ könnte man auch Precaching betreiben und alle Objekte aus anliegenden Maps/Partitionen initialisieren, bevor der Spieler sie betritt; das braucht aber natürlich mehr Ressourcen). Allerdings muß man beachten, daß einige Events nur dann Sinn machen, wenn sie gleich aktiv sind.

    Das mit den Timern könnte man dadurch lösen, daß man eine globale Liste (vermutlich Multimap) mit ausstehenden Events macht, in der sich die Events mit einem Zeitstempel (bspw. time()+300 für "in fünf Minuten") eintragen. Wann immer ein Stempel niedriger als time() ist wird die zugehörige Funktion ausgeführt. So muß man nur über eine Liste iterieren, anstatt 1000 Warteschleifen laufen zu haben. (Hmm, gibt's da nicht eine effizientere Wartemethode? Threads können ziemlich efffizient warten...)

    Die Markierung, daß das Event durch ist, könnte man so machen: Wenn das Event in der Zeitliste gestartet wird wird es aus selbiger entfernt - wenn es wieder was getimert haben will muß es sich neu eintragen (oder man benutzt einen Befehl, um den Timer explizit zu beenden). Damit das Ding beim Betreten der Map nicht neu ausgelöst wird macht es im entsprechenden diff einen Eintrag, daß es bereits gelaufen ist.


    Ich weiß nicht, ob dich diese Ideen weiterbringen, aber hey, du hast darum gebeten...

  3. #3
    Zitat Zitat von Jesus_666 Beitrag anzeigen
    Das mit den Timern könnte man dadurch lösen, daß man eine globale Liste (vermutlich Multimap) mit ausstehenden Events macht, in der sich die Events mit einem Zeitstempel (bspw. time()+300 für "in fünf Minuten") eintragen. Wann immer ein Stempel niedriger als time() ist wird die zugehörige Funktion ausgeführt. So muß man nur über eine Liste iterieren, anstatt 1000 Warteschleifen laufen zu haben. (Hmm, gibt's da nicht eine effizientere Wartemethode? Threads können ziemlich efffizient warten...)
    Timestamps könnte allerdings beim Speichern ziemliche Probleme machen, nicht? Wenn man z.B. (habe ja keine Ahnung, was das für ein Spiel ist) einen Schalter betätigt, und dann innerhalb von 5 Minuten aus dem Gebäude fliehen muss (Pfeil- und Kugelfallen optional), und man während dem Countdown speichert, wäre er automatisch abgelaufen, wenn man nach mehr als 5 Minuten wieder lädt. :-/
    Man müsste dann beim Abspeichern die aktiven Timer mit timestamp - time () abspeichern, und beim Laden dann wieder die aktuelle Zeit dazuaddieren.

    Sorry, dass ich nicht mehr beitragen kann, kenne mich da nicht wirklich aus.

  4. #4
    Das mit dem Zeit messen hab ich so gelöst:
    Code:
    LARGE_INTEGER begin,end,frq;
    double time;
    //im Initialisierungsteil:
    if( QueryPerformanceFrequency(&frq) ==0)
    	{
    		MessageBox(hWnd,"Konnte TimeCounter nicht initialisieren!","Abbrechen",MB_ICONINFORMATION);
    		return false;
    	}
    
    //am Anfang:
    QueryPerformanceCounter(&begin);
    //zum überprüfen
    QueryPerformanceCounter(&end);
    			time=static_cast<double>(end.QuadPart  - begin2.QuadPart ) / static_cast<double>(frq.QuadPart);
    Du könntest also time inititialisieren, z.B. mit 0 und dann immer dazu rechnen

    Code:
    LARGE_INTEGER begin,end,frq;
    double time = 0.0;
    //im Initialisierungsteil:
    if( QueryPerformanceFrequency(&frq) ==0)
    	{
    		MessageBox(hWnd,"Konnte TimeCounter nicht initialisieren!","Abbrechen",MB_ICONINFORMATION);
    		return false;
    	}
    
    //am Anfang:(kann am Anfang der Schleife sein)
    QueryPerformanceCounter(&begin);
    //zum erneuern der time-Variable(z.B. am Ende der Schleife)
    QueryPerformanceCounter(&end);
    			time+=static_cast<double>(end.QuadPart  - begin2.QuadPart ) / static_cast<double>(frq.QuadPart);
    oder eben begin erst beim klicken initialisieren und dann aber wieder
    Code:
    time=static_cast<double>(end.QuadPart  - begin2.QuadPart ) / static_cast<double>(frq.QuadPart);
    würde aber wahrscheinlich nicht so gut mit speichern klappen.

    PS: Ist der Typenumwandler im C++ static_cast<double>() oder (double) ?

    Geändert von Drakes (02.02.2007 um 16:51 Uhr)

  5. #5
    Ist beides möglich, wobei (double) ein C-Cast ist und als veraltet gilt. Allerdings mach ichs aus Gewohnheit auch so.

  6. #6
    Zitat Zitat von drunken monkey Beitrag anzeigen
    Timestamps könnte allerdings beim Speichern ziemliche Probleme machen, nicht? Wenn man z.B. (habe ja keine Ahnung, was das für ein Spiel ist) einen Schalter betätigt, und dann innerhalb von 5 Minuten aus dem Gebäude fliehen muss (Pfeil- und Kugelfallen optional), und man während dem Countdown speichert, wäre er automatisch abgelaufen, wenn man nach mehr als 5 Minuten wieder lädt. :-/
    Man müsste dann beim Abspeichern die aktiven Timer mit timestamp - time () abspeichern, und beim Laden dann wieder die aktuelle Zeit dazuaddieren.
    Natürlich muß man den Kram schon auf eine sinnvolle Art speichern. Esv kann auch gut andere Methoden geben, die gut funktionieren; das mit time() ist nur eine simple Mathode, den Kram umzusetzen.
    Man könnte sich auch die ganzen Aufrufe sparen, indem man eine globale Variable nimmt und die irgendwo inkrementiert (sich also seine eigenen Zeitscheiben definiert), aber da muß man aufpassen, daß die Zeitscheiben auch wirklich eine konstante, bekannte Länge haben.

  7. #7
    Hm, okay, ich werd's mal so mit einer art Differenz der geladenen Daten zu den veränderten Daten probieren. Reduzieren würde man den Speicheraufwand jedenfalls schonmal. Hoffe dass es auch im Effektiven Bereich ist.

    ----

    Ich hab dann noch eine andere Frage bezüglich C / C++. Ich würde gerne eine Funktion vervielfältigen, dass ich sie per Funktionsname[3] einzeln aufrufen kann. Diese Funktionen sollen natürlich einzeln definiert werden können.
    Ein Pointer-Array auf verschiedene Funktionen (die dann zwar mit einem Pointer aus dem Array ansprechbar, aber mit verschiedene Namen definiert werden) hilft mir also nicht weiter.

    Also die eigentliche Definition der Funktionen soll in etwa so aussehen können:

    Code:
    void funktion[0] (void) {
      //...
    }
    
    void funktion[1] (void) {
      //...
    }
    
    void funktion[2] (void) {
      //...
    }
    Ansprechbar dann über funktion[2]() oder einen ähnlichen Ausdruck.

    Gibt's da eine Möglichkeit, das irgendwie zu machen?

  8. #8
    Ich hab zwar nicht verstanden, warum Pointer nicht in Ordnung sind, aber ansonsten seh ich zwei Möglichkeiten, was ähnliches zu realisieren:

    Code:
    void funktion_0 (void) {
      //...
    }
    
    void funktion_1 (void) {
      //...
    }
    
    void funktion_2 (void) {
      //...
    }
    oder das Gleiche erweitert durch folgende Funktion:
    Code:
    void funktion(int number)
    {
      switch(number)
      {
        case 0:
          funktion_0();
          break;
        case 1:
          // ...
      }
    }

  9. #9
    Also ich wuerde das Problem wahrscheinlich so angehen ..

    Ich denke, dass es das beste waere, alle Objekte die ganze Zeit im Speicher zu haben. Bei den heutigen Systemen spielt das auch keine allzugrosse Rolle. Sollte es dabei viel Redundanz geben (z.B. bei Schaltern) koennte man gruppenbasierte Metaobjekte erzeugen und Einzelobjekte simulieren. Wird ein Objekt nicht mehr benötigt, wird es einfach als Speicher freigegeben. Wird ein Objekt noch fuer spaeter benoetigt, wird es im Speicher gehalten.

    Was die Sache mit den Scripts angeht, so ist dies eventuell auch recht einfach. Jedes Objekt bekommt eine Eigenschaft, auf welcher Map (oder welchen Maps) es verankert ist. Ist es auf keiner Map verankert, so gibt es dafuer eine spezielle ID. Zudem gibt es eine Messagequeue zum Senden von Botschaften an alle eingetragenen Funktionen. Beim ersten Initialisieren eines Objektes hat das Objekt nun die Moeglichkeit 2 Call-Back-Funktionen als Handler in den Queues einzutragen. So ist es bei den meisten Objekten sinnvoll, dass in die All-Object-Queue ein Handler fuer ein Enter_Map_Event und ein Leave_Map_Event eingetragen wird. Diese Handler pruefen dann die aktuelle Map_ID und registrieren bzw. entfernen weitere Messagehandler in der Queue. Somit brauchen die Scripte nur genau dann Rechenleistung, wenn man sich auf einer Map befindet, auf der sie die Messages ueberhaupt abfragen muessen. Dass dennoch fuer fast jedes Objekt eine CallBack Funktion durchlaufen wird, wenn die Map verlassen oder betreten wird, sollte verschmerzbar sein, da hierbei ohnehin externe Resourcen geladen werden muessen. Die Geschwindigkeit der Messagequeue kann zudem optimiert werden, wenn die Message nicht in den Handlern auf den korrekten Typ ueberprueft wird, was der Fall waere, wuerde die Queue aus Funktionspointern bestehen, sondern wenn stattdessen Structs aus Funktionspointer und einer Liste/einem Array akzeptierter Messages gespeichert wuerden. So lassen sich unnoetige Funktionsspruenge vermeiden. Nicht die IFs, sondern die Calls fressen Resourcen, da damit der Instructioncache des Prozessors unbrauchbar wird.

    Was dein anderes Problem angeht, hab ich keine Ahnung, was du genau machen willst. Laut deiner Beschreibung iist doch ein Array von Funktionspointern auf verschiedene Funktionen ganz genau das, was du haben willst. Wieso nutzt dir das nichts ? Oder willst du etwa zur Laufzeit neue Funktionen schreiben und dann deinem Array hinzufuegen ? Dann musst du wohl oder uebel auf dynamisches Linken zurueckgreifen und mit Dlls bzw .so arbeiten. Gib uns am besten mal ein konkretes Beispiel, wofuer du das brauchst. Sicherlich gibt es fuer dich eine elegantere Loesung, z.B. Arrays von Funktionspointern oder Templates ...

Berechtigungen

  • Neue Themen erstellen: Nein
  • Themen beantworten: Nein
  • Anhänge hochladen: Nein
  • Beiträge bearbeiten: Nein
  •