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

10.09.2010 - Mateusz Osowski
TrudnośćTrudność

System zdarzeń

Poprawne wyświetlanie komunikatu tekstowego jest dobrym symptomem. To znak, że możemy przejść do uogólnienia naszego rozwiązania do zdarzeń. O ile implementując je w C++ posłużylibyśmy się adresami funkcji umieszczonych w zewnętrznym skrypcie, o tyle korzystając z .NET dostajemy do dyspozycji mechanizm delegatów. Delegaty w połączeniu ze zdarzeniami platformy .NET służą między innymi do tworzenia wywołań zwrotnych. Za pomocą delegata określamy sygnaturę, jaką powinny mieć funkcje wywołania zwrotnego. Przejdź do sekcji pól klasy TriggerVolume i dopisz następujące deklaracje:
1
2
3
4
  public delegate void CharacterEnteredHandler(TriggerVolume sender, Character character);
  public delegate void CharacterLeftHandler(TriggerVolume sender, Character character);
  public event CharacterEnteredHandler OnCharacterEntered;
  public event CharacterLeftHandler OnCharacterLeft;
TriggerVolume.cs


Definiujemy typy delegatów wywołań zwrotnych zdarzeń wejścia i wyjścia postaci na teren wyzwalacza, a następnie definiujemy same zdarzenia. Dopisz do klas Character, TriggerVolume i do klas z nimi związanych modyfikator dostępu public, ponieważ wymaga tego związanie z delegatami. Gdy już to zrobisz, przejdź do funkcji Update() i zaktualizuj kod:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    switch (info.State)
    {
      case ObjectState.Unknown:
        if (OnCharacterLeft != null && info.GameObject is Character)
          OnCharacterLeft(this, info.GameObject as Character);
        ObjectsInside.RemoveAt(i);
      break;
 
      case ObjectState.New:
        if (OnCharacterEntered != null && info.GameObject is Character)
          OnCharacterEntered(this, info.GameObject as Character);
        info.State = ObjectState.Unknown;
      break;
 
      case ObjectState.Present:
        info.State = ObjectState.Unknown;
        break;
    }
TriggerVolume.cs, Update()


Zdarzenie jest wywoływane jak zwyczajna metoda, lecz z uprzednim sprawdzeniem, czy nie jest puste. Ponadto upewniamy się, że zainteresowany obiekt jest postacią. Obsługę innych rodzajów obiektów możesz dopisać w formie ćwiczeń.

Aby przetestować działanie wyzwalacza w całej okazałości musimy jeszcze zaimplementować przynajmniej jedno ze zdarzeń. Przejdź do klasy Program i dopisz implementację zdarzenia. Środowisko Visual Studio jest bardzo pomocne w tej kwestii, praktycznie samo generuje odpowiednio nazwaną metodę:
1
2
3
4
    ...
    triggerVolume.OnCharacterEntered += 
      new TriggerVolume.CharacterEnteredHandler(triggerVolume_OnCharacterEntered);
    ...
Program.cs, Main()


1
2
3
4
static void triggerVolume_OnCharacterEntered(TriggerVolume sender, Character character)
{
    Console.WriteLine("Bohater wszedł na teren wyzwalacza");
}
Program.cs




Możesz teraz przetestować działanie - program powinien sprawnie rejestrować zdarzenie i wywoływać odpowiednią metodę.

Modernizacja obiektu gry

Zanim przystąpimy do dalszego rozwijania funkcjonalności gry, musimy zmodernizować abstrakcyjną klasę GameObject. Wymusimy na każdym obiekcie możliwość ustawienia pozycji i orientacji, a także damy możliwość nazwania obiektu:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class GameObject
{
  public String Name;
 
  public abstract void Update();
  
  public abstract Vector3 Position
  {
    get;
    set;
  }
 
  public abstract Quaternion Orientation
  {
    get;
    set;
  }             
}
GameObject.cs


Oczywiście, należy dokonać modernizacji dziedziczących klas Character i TriggerVolume. W pierwszej z nich uczynimy pole Orientation prywatnym i zmienimy jego nazwę na _Orientation, by nie kolidowało z nową, dziedziczoną właściwością. Zmiana nazwy w środowisku Visual C# jest prosta, wystarczy skorzystać z opcji refaktoryzacji. Kliknij prawym przyciskiem myszy na starą nazwę pola i wybierz opcję zmiany nazwy:
Pozdbądźmy się starej metody związanej z ustawianiem pozycji i wstawmy na jej miejsce implementacje właściwości:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  public override Vector3 Position
  {
    get { return Body.Position; }
    set
    {
      Body.SetPositionOrientation(value, Orientation);
    }
  }
 
  public override Quaternion Orientation
  {
    get { return _Orientation; }
    set { _Orientation = value; }
  }     
Character.cs


Podobnie w klasie TriggerVolume:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  public override Vector3 Position
  {
    get { return Body.Position; }
    set
    {
      Body.SetPositionOrientation(value, Body.Orientation);
    }
  }
 
  public override Quaternion Orientation
  {
    get { return Body.Orientation; }
    set
    {
    Body.SetPositionOrientation(Body.Position, value);
    }
  }
TriggerVolume.cs


Oczywiście należy jeszcze zaktualizować fragmenty pliku Program.cs korzystające z dawnych metod. Posiadając zmodernizowany kod możemy zająć się następnym elementem gry - sensorami, pełniącymi rolę narządów zmysłów bohaterów gry.

Aktualny kod źródłowy



Sensory

Chcemy, aby nasz bohater miał możliwość zobaczenia obiektów znajdujących się w jego pobliżu. W tym celu zaimplementujemy sensor wykrywający kolizje z obiektami w pobliżu postaci. Wykorzystamy do tego celu ciało Newtona. Utworzymy duży cylinder, który postawimy dokładnie przed kapsułą postaci:



Dodaj do klasy Character następujące pola:
1
2
3
4
  public Body ObjectSensor;
  public Node SensorNode;
  
  public List<GameObject> Contacts;
Character.cs


Lista Contacts będzie przechoywać obiekty znajdujące się w zasięgu sensora w danej klatce. Ciało sensora utworzymy na końcu kontruktora, po zniszczeniu obiektu kolizji przeznaczonego do stworzenia ciała postaci:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    ...
    SensorNode = Node.CreateChildSceneNode(
      new Vector3(0, 0, System.Math.Min(scaledSize.x, scaledSize.z) * 2));
 
    collision =  new MogreNewt.CollisionPrimitives.Cylinder(
      Engine.Singleton.NewtonWorld,
      System.Math.Min(scaledSize.x, scaledSize.z) * 2,
      scaledSize.y * 2,
      Vector3.UNIT_X.GetRotationTo(Vector3.UNIT_Y),
      Engine.Singleton.GetUniqueBodyId());
    ObjectSensor = new Body(Engine.Singleton.NewtonWorld, collision, true);
    ObjectSensor.SetMassMatrix(1, new Vector3(1,1,1));
 
    ObjectSensor.UserData = this;
    ObjectSensor.MaterialGroupID = Engine.Singleton.MaterialManager.CharacterSensorMaterialID;
 
    Contacts = new List<GameObject>();
  }
Character.cs, Character()


W linijce 2 tworzymy węzeł będący dzieckiem węzła postaci ustawiony dwie długości promienia przed środkiem postaci. W linijce 5 nadajemy cylindrowi sensora promień dwa razy większy, niż promień postaci.
W linijce 10 ustalamy masę ciała na 1. Pomimo, że sensorem będziemy poruszać samodzielnie, nadając mu pozycję zgodną z pozycją węzła utworzonego w linijce 2, nie możemy ciała uczynić statycznym, ponieważ nie generowałoby ono wtedy kontaktów z innymi ciałami statycznymi.
W linijce 13 nadajemy ciału odpowiedni materiał, który odróżni sensor od samej postaci.

W metodzie Update() dodajemy aktualizację pozycji sensora i czyszczenie listy, odpowiednio na początku i końcu metody:
1
2
3
4
5
6
  public override void Update()
  {
    ObjectSensor.SetPositionOrientation(SensorNode._getDerivedPosition(), Node.Orientation);
    ...
    Contacts.Clear();
  }
Character.cs, Update()


Metoda _getDerivedPosition() Pozwala pobrać bezwzględną pozycję węzła w przestrzeni trójwymiarowej.

Aby sensor mógł zadziałać, musimy wprowadzić do gry klasę obiektu wykrywalnego przez sensory. Do tego typu obiektów będą się klasyfikować wszystkie obiekty, do których można podejść, zobaczyć ich nazwę i opis. Utwórz klasę abstrakcyjną SelectableObject:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class SelectableObject : GameObject
{
  public abstract String DisplayName
  {
    get;
    set;
  }
  public abstract String Description
  {
    get;
    set;
  }
  public abstract Vector3 DisplayNameOffset
  {
    get;
    set;
  }
 
  public bool ShowName;
}
SelectableObject.cs


Jak widzisz, obiekty takie charakteryzować się będą:
  • Wyświetlaną nazwą (DisplayName)
  • Opisem (Description)
  • Przesunięciem etykiety z nazwą względem środka obiektu (DisplayNameOffset)
    Oprócz tego, pole ShowName będzie kontrolować, czy nazwa ma być wyświetlana.

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com