Tworzenie gry w C# z użyciem silnika Ogre - cz.2

10.09.2010 - Mateusz Osowski
TrudnośćTrudność


Kontrolowanie kontaktów

Działanie naszego wyzwalacza obszarowego jest w tej chwili dalekie od oczekiwań. Postać koliduje z obiektem wyzwalacza. Było to do przewidzenia, utworzyliśmy bowiem standardowe ciało Newtona w żaden sposób nie wskazując, że chcielibyśmy, by nie stanowiło ono blokady. Aby przepuścić postać przez wyzwalacz musimy przejąć kontrolę nad wykrywaniem kolizji.

Przyjrzyjmy się architekturze wykorzystywanego przez nas silnika fizycznego. Do tej pory wykorzystywaliśmy ciała, obiekty kolizji. Na szczególną uwagę zasługują jednak wywołania zwrotne. Newton daje nam możliwość kontrolowania kolizji poprzez implementację własnych wywołań zwrotnych wywoływanych w momencie nastąpienia kolizji. Takie wywołanie nie jest jednak wiązane z konkretnym ciałem, jak to było w przypadku aplikacji sił i transformacji węzła sceny, lecz z parą materiałów. Z każdym ciałem silnika Newton możemy powiązać materiał (MaterialID). Obiekt ten może być wykorzystywany przez wiele ciał, jednak sam w sobie nie stanowi żadnej wartości. Materiały stanowią wierzchołki pewnego grafu, którego krawędzie stanowią kluczowe elementy - w Newtonie reprezentowane przez klasę MaterialPair. Obiekt pary materiałów pozwala określić skutki kolizji dwóch obiektów o określonych materiałach, między innymi współczynniki tarcia i sprężystość. Interesującą nas właściwością pary jest jednak wywołanie zwrotne contact callback pozwalające określić metody, które zostaną wywołane w momencie kolizji ciał. Przykładowo, może ono być wykorzystywane do odgrywania odpowiedniego dźwięku w momencie kontaktu ciała stalowego i drewnianego.

Wprowadzenie materiałów do silnika gry zmusza nas do dokonania pewnych zmian w istniejącym kodzie. Dobrze będzie stworzyć menadżer, który będzie implementować wywołania zwrotne dla różnych par materiałów, a także będzie przechowywać same identyfikatory materiałów. Zacznijmy go pisać - otwórz więc plik o nazwie MaterialManager.cs.
Dodajmy pola:
1
2
3
4
5
6
class MaterialManager {
  public MaterialID LevelMaterialID;
  public MaterialID CharacterMaterialID;
  public MaterialID TriggerVolumeMaterialID;
 
  MaterialPair TriggerVolumeCharacterPair;
MaterialManager.cs


Potrzebujemy osobny materiał dla ciała postaci, poziomu i obszaru wyzwalacza, a także pary łączącej interesujące nas materiały.
Stwórzymy teraz metodę inicjującą menadżera:
1
2
3
4
5
6
7
8
9
10
  public void Initialise()
  {
    LevelMaterialID = new MaterialID(Engine.Singleton.NewtonWorld);
    CharacterMaterialID = new MaterialID(Engine.Singleton.NewtonWorld); 
    TriggerVolumeMaterialID = new MaterialID(Engine.Singleton.NewtonWorld);   
    
    TriggerVolumeCharacterPair = new MaterialPair(
      Engine.Singleton.NewtonWorld,
      CharacterMaterialID,
      TriggerVolumeMaterialID);
MaterialManager.cs


Po utworzeniu obiektów materiałów konstruujemy obiekt pary odpowiadający za reakcję kolizji materiałów postaci i wyzwalacza, nad którym pracujemy.
1
2
    TriggerVolumeCharacterPair.SetContactCallback(new TriggerVolumeGameObjectCallback());
  }
MaterialManager.cs


Wywołanie zwrotne kontaktu musi być implementowane poprzez klasę dziedziczącą po ContactCallback, toteż tworzymy obiekt takiej klasy i podłączamy go jako contact callback pary materiałów. Przystąpmy do definicji tej klasy:
1
2
3
4
5
6
7
8
  class TriggerVolumeGameObjectCallback : ContactCallback
  {
    public override int UserAABBOverlap(
      ContactMaterial material, 
      Body body0, 
      Body body1, 
      int threadIndex)
    {
MaterialManager.cs


Klasa TriggerVolumeGameObjectCallback jest zagnieżdżoną i prywatną klasą menadżera materiałów, bowiem nie będzie potrzeby odwoływania się od niej z zewnątrz menadżera. Przeciążamy metodę UserAABBOverlap. Będzie ona wywoływana, gdy prostopadłościany otaczające dwa ciała zderzą się ze sobą. Nie determinuje ona kolizji, ale informuje o możliwości jej zaistnienia, jednocześnie pozwala poprzez zwracaną wartość określić, czy kolizja ma zostać odrzucona. Możliwe jest również przeciążenie metody UserProcess() - wywołania zwrotnego wywoływanego w momencie nastąpienia właściwej kolizji. Nie interesuje nas ona ze względu na to, że odpowiada na kolizje dopiero po ich nastąpieniu, zatem nie daje możliwości ich odrzucenia. Musimy więc w metodzie UserAABBOverlap samodzielnie sprawdzać, czy ciała faktycznie ze sobą kolidują.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      Vector3[] contactPoints;
      Vector3[] contactNormals;
      float[] contactPenetration;
 
      if (MogreNewt.CollisionTool.CollisionCollide(
        Engine.Singleton.NewtonWorld,    
        2,
        body0.Collision,
        body0.Orientation,
        body0.Position,
        body1.Collision,
        body1.Orientation,
        body1.Position,
        out contactPoints,
        out contactNormals,
        out contactPenetration,
        threadIndex) != 0)
      {         
MaterialManager.cs


Statyczna metoda CollisionCollide pozwala sprawdzić, czy dwa obiekty kolizji zderzają się ze sobą. Jako przedostatnie trzy parametry przyjmuje referencje na tablice, w których zapisane będą ewentualne punkty kontaktów. Maksymalny rozmiar tych tablic określany jest przez drugi argument. Zwracana przez nią wartość określa ile kontaktów nastąpiło.

Posiadając informację o kolizji podejmujemy odpowiednie działania:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        if (body0.UserData is TriggerVolume)
        {
          TriggerVolume triggerVolume = body0.UserData as TriggerVolume;
          triggerVolume.HandleGameObject((GameObject)body1.UserData);
        }
        else
        {
          TriggerVolume triggerVolume = body1.UserData as TriggerVolume;
          triggerVolume.HandleGameObject((GameObject)body0.UserData);
        }
      }
      return 0;
    }
  }            
}
MaterialManager.cs


Przy pomocy pola UserData dostajemy się do obiektów związanych z ciałami i sprawdzamy ich typy. Musimy dokonać tego sprawdzenia, ponieważ kolejność w jakiej dwa kolidujące ciała trafią do funkcji nie jest określona. Na końcu zwracamy 0, ponieważ chcemy się pozbyć kolizji. Zauważ, że drugi obiekt rzutujemy na typ GameObject, a nie Character. Dzięki temu, to wywołanie zwrotne będzie mogło być wykorzystane także przez inne pary materiałów odpowiadające za kolizję obiektów innego typu.

Menadżer nie jest jeszcze funkcjonalny, chociażby z uwagi na brak implementacji metody HandleGameObject klasy TriggerVolume, lecz możemy dodać jego instancję do klasy głównej silnika. Dodaj więc do klasy Engine pole:
1
  public MaterialManager MaterialManager;
Engine.cs


Uzupełnij także metodę inicjującą:
1
2
3
4
    ...
    MaterialManager = new MaterialManager();
    MaterialManager.Initialise();
  }
Engine.cs, Initialise()




Kontrola obecności obiektów w obszarze

Najistotniejszym zadaniem wyzwalacza obszarowego jest kontrola zdarzeń takich jak wejście obiektu, czy jego wyjście z obszaru reprezentowanego przez wyzwalacz. Zanim jednak przystąpimy do rozwijania klasy wyzwalacza, musimy uzupełnić klasę postaci o dodatkowe informacje, a mianowicie musimy dodać powiązanie ciała postaci z odpowiednim materiałem i obiektem klasy. Najlepiej dokonać tego w bloku instrukcji związanych z tworzeniem ciała.
1
2
3
4
5
6
7
  ...
  Body.Transformed += BodyTransformCallback;
  Body.ForceCallback += BodyForceCallback;
  
  Body.UserData = this;
  Body.MaterialGroupID = Engine.Singleton.MaterialManager.CharacterMaterialID;
  ...
Character.cs, Character()


Aktualizacji wymaga także klasa Level i jej metoda SetCollisionMesh:
1
2
3
4
5
    ...
    Body.AttachNode(CollisionNode);
    Body.UserData = this;
    Body.MaterialGroupID = Engine.Singleton.MaterialManager.LevelMaterialID;
  }
Level.cs, SetCollisionMesh()


Także ciało klasy TriggerVolume musimy uzupełnić o materiał:
1
2
3
    ...
    Body.UserData = this;
    Body.MaterialGroupID = Engine.Singleton.MaterialManager.TriggerVolumeMaterialID;
TriggerVolume.cs, EndShapeBuild()




Skupmy się teraz na głównym problemie - musimy w jakiś sposób przechowywać informacje o obiektach znajdujących się w obszarze wyzwalacza. Wywołanie zwrotne, które zaprogramowaliśmy w menadżerze materiałów w przypadku, gdy obiekt znajdzie się w przestrzeni wyzwalacza, będzie go zgłaszać w każdej klatce do wyzwalacza za pomocą HandleGameObject. Chcemy jednak wyróżnić momenty, w którym obiekt wchodzi na teren wyzwalacza lub go opuszcza. Z pierwszym przypadkiem nie ma problemu, wystarczy utworzyć kolekcję obiektów znajdujących się w wyzwalaczu i sprawdzać, czy zgłaszający się obiekt nie znajduje się już w niej. Drugi przypadek zmusza nas jednak do rozwinięcia idei. Naturalnym rozwiązaniem będzie podzielenie cyklu życia obiektu na trzy stany. Przejdź więc na początek klasyTriggerVolume i dodaj typ wpisu obecności obiektu:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TriggerVolume : GameObject
{
  enum ObjectState
  {
    New,
    Present,
    Unknown
  }
 
  class ObjectInfo
  {
    public GameObject GameObject;
    public ObjectState State;
 
    public ObjectInfo(GameObject gameObject, ObjectState state)
    {
      GameObject = gameObject;
      State = state;
    }
  }
  
  List<ObjectInfo> ObjectsInside;
TriggerVolume.cs


Obiekt będzie mógł być etykietowany trzema różnymi stanami. Jeżeli zgłaszający się obiekt nie będzie figurował na liście obiektów - zostanie dopisany z etykietą New. W przeciwnym wypadku jego etykieta zostanie ustawiona na Present, zaś pod koniec klatki zostanie oznaczony stanem Unknown. Jeżeli obiekt ze stanem Unknown nie zgłosi się w następnej klatce, będzie to znaczyło, że opuścił wyzwalacz.

Pamiętaj, aby utworzyć obiekt kolekcji w metodzie BeginShapeBuild:
1
2
3
4
5
  public void BeginShapeBuild()
  {
    CompoundParts = new List<Collision>();
    ObjectsInside = new List<ObjectInfo>();
  }
TriggerVolume.cs


Przejdźmy do implementacji metody HandleGameObject
1
2
3
4
5
6
7
8
9
  public void HandleGameObject(GameObject gameObject)
  {
    // Próbujemy znaleźć wpis zgłoszonego obiektu
    ObjectInfo entry = ObjectsInside.Find(
      delegate(ObjectInfo info)
      {
        return info.GameObject == gameObject;
      }
    );
TriggerVolume.cs, HandleGameObject()


Jak widzisz, w C# można korzystać z funkcji anonimowych przekazywanych jako argument metod. W tym przypadku definiujemy prosty predykat. Jeżeli obiekt nie zostanie znaleziony w kolekcji, zmienna entry przyjmie wartość null.
1
2
3
4
5
    if (entry == null)
        ObjectsInside.Add(new ObjectInfo(gameObject, ObjectState.New));               
    else
        entry.State = ObjectState.Present;        
  }
TriggerVolume.cs, HandleGameObject()


Zgodnie z uzgodnieniami, w przypadku nowego obiektu nadajemy mu stan New, w przypadku istniejącego oznaczamy go jako obecny.

Pozostaje uzupełnić metodę Update(), którą pozostawiliśmy pustą:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  public override void Update()
  {
    for (int i = ObjectsInside.Count - 1; i >= 0; i--)
    {
      ObjectInfo info = ObjectsInside[i];
      switch (info.State)
      {
        case ObjectState.Unknown:
          Console.WriteLine("Obiekt opuścił teren wyzwalacza.");
          ObjectsInside.RemoveAt(i);
          break;
 
        case ObjectState.New:
          Console.WriteLine("Obiekt wszedł na teren wyzwalacza.");
          info.State = ObjectState.Unknown;
          break;
 
        case ObjectState.Present:
          info.State = ObjectState.Unknown;
          break;
      }
    }
  }
TriggerVolume.cs, Update()


Iterujemy po liście obiektów znajdujących się wyzwalaczu od tyłu, ponieważ pozwala to na łatwe usuwanie z niej elementów podczas iteracji. Ponieważ nie posiadamy jeszcze systemu zarządzania zdarzeniami, w celu sprawdzenia poprawności działania wyzwalacza posłużymy się bezpośrednio wypisywanymi komunikatami tekstowymi. Kod spełnia wcześniej ustalone założenia, to znaczy, w każdej klatce oznaczamy obiekty stanem Unknown, a jeżeli któryś z obiektów utrzyma ten stan do następnej aktualizacji - zostanie usunięty z listy.

Aktualny kod źródłowy



Od tej chwili wyzwalacz obszarowy zachowuje się zgodnie z naszymi oczekiwaniami. Postać swobodnie przechodzi przez obszar, a komunikaty są generowane w odpowiednich momentach.

5
Twoja ocena: Brak Ocena: 5 (4 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com