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

10.09.2010 - Mateusz Osowski
TrudnośćTrudność
Dobrze będzie, jeżeli postać również będzie zaznaczalnym obiektem. Zmień więc klasę bazową klasy Character na SelectableObject. Opis i pozostałe właściwości wymagane przez klasę bazową będziemy przechowywać w profilu postaci. Dopisz więc do niego odpowiednie pola:
1
2
3
4
5
  ...
  public String DisplayName;
  public String Description;
  public Vector3 DisplayNameOffset;
  ...
CharacterProfile.cs


W klasie Character przeciąż akcesory, by dawały dostęp do pól znajdujących się w profilu:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  public override string DisplayName
  {
    get { return Profile.DisplayName; }
    set { Profile.DisplayName = value; }
  }
  public override string Description
  {
    get { return Profile.Description; }
    set { Profile.Description = value; }
  }
  public override Vector3 DisplayNameOffset
  {
    get { return Profile.DisplayNameOffset; }
    set { Profile.DisplayNameOffset = value; }
  }
Character.cs


Klasa prostego obiektu z opisem

Oprócz postaci, naszej grze przydadzą się nieinteraktywne obiekty z opisem słownym. Tego typu elementy mogą zostać wykorzystane przy tworzeniu przygodowych elementów gry, przykładowo do stworzenia płyty nagrobkowej z opisem w formie tajemniczej inskrypcji będącej częścią jakiejś zagadki.

Zanim zaimplementujemy samą klasę obiektu, zaczniemy, podobnie jak w przypadku postaci, od profilu. Utwórz więc klasę DescribedProfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DescribedProfile
{
  public String MeshName;
  public String Description;
  public String DisplayName;
  public Vector3 DisplayNameOffset;
  public Vector3 BodyScaleFactor;
 
  public DescribedProfile Clone()
  {
    return (DescribedProfile)MemberwiseClone();
  }
}
DescribedProfile.cs


Pola mają identyczne znaczenie, jak w przypadku postaci.

Utwórz teraz publiczną klasę Described dziedziczącą z SelectableObject:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Described : SelectableObject
{
  DescribedProfile Profile;
 
  Entity Entity;
  SceneNode Node;
  Body Body;
 
  public Described(DescribedProfile profile)
  {
    Profile = profile.Clone();
 
    Entity = Engine.Singleton.SceneManager.CreateEntity(Profile.MeshName);
    Node = Engine.Singleton.SceneManager.RootSceneNode.CreateChildSceneNode();
    Node.AttachObject(Entity);
 
    Vector3 scaledSize = Entity.BoundingBox.Size * Profile.BodyScaleFactor;
 
    ConvexCollision collision = new MogreNewt.CollisionPrimitives.Box(
    Engine.Singleton.NewtonWorld,                                
    scaledSize,
    Quaternion.IDENTITY,
    Engine.Singleton.GetUniqueBodyId()); 
 
 
    Body = new Body(Engine.Singleton.NewtonWorld, collision, true);
    Body.AttachNode(Node);
    Body.SetMassMatrix(0, Vector3.ZERO);            
 
    Body.UserData = this;
    Body.MaterialGroupID = Engine.Singleton.MaterialManager.DescribedMaterialID;
 
    collision.Dispose();
  }
Described.cs


Z podobnym kodem spotkaliśmy się wcześniej. Obiektom tego typu przypisujemy specjalny materiał, który będziemy musieli uwzględnić w klasie MaterialManager.

Tak jak w przypadku klasy postaci, musimy przeciążyć właściwości oraz Update():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  public override void Update()
  {                                    
  }
  public override Vector3 Position
  {
    get { return Body.Position; }
    set { Body.SetPositionOrientation(value, Orientation); }
  }
  public override Quaternion Orientation
  {
    get { return Body.Orientation; }
    set { Body.SetPositionOrientation(Body.Position, value); }
  }
  public override string DisplayName
  {
    get { return Profile.DisplayName; }
    set { Profile.DisplayName = value; }
  }
  public override string Description
  {
    get { return Profile.Description; }
    set { Profile.Description = value; }
  }
  public override Vector3 DisplayNameOffset
  {
    get { return Profile.DisplayNameOffset; }
    set { Profile.DisplayNameOffset = value; }
  }
Described.cs




Wykrywanie obiektów

Pora zaimplementować wykrywanie obiektów przez sensor. W tym celu dodaj do klasy MaterialManager następujące materiały i pary materiałów:
1
2
3
4
5
6
7
  public MaterialID DescribedMaterialID;
  public MaterialID CharacterSensorMaterialID;
  
  MaterialPair CharacterSensorPair;
  MaterialPair SensorLevelPair;  
  MaterialPair SensorDescribedObjectPair;
  MaterialPair SensorTriggerVolumePair;
MaterialManager.cs


Para CharacterSensorPair odpowiadać będzie za ignorowanie kolizji pomiędzy postacią, a sensorem. To rozwiązanie zapobiegnie hamowaniu ruchu postaci przez ciało sensora:
1
2
3
4
  CharacterSensorPair = new MaterialPair(
    Engine.Singleton.NewtonWorld,
    CharacterMaterialID, CharacterSensorMaterialID);
  CharacterSensorPair.SetContactCallback(new IgnoreCollisionCallback());
MaterialManager.cs, Initialise()


Para SensorLevelPair będzie ignorować kolizję sensora z terenem planszy - dzięki temu sensor będzie poruszać się swobodnie przed postacią:
1
2
3
4
  SensorLevelPair = new MaterialPair(
    Engine.Singleton.NewtonWorld,
    LevelMaterialID, CharacterSensorMaterialID);
  SensorLevelPair.SetContactCallback(new IgnoreCollisionCallback());
MaterialManager.cs, Initialise()


Podobnie para SensorTriggerVolumePair zapobiegać będzie kolizjom sensora z wyzwalaczami obszarowymi:
1
2
3
4
  SensorTriggerVolumePair = new MaterialPair(
    Engine.Singleton.NewtonWorld,
    TriggerVolumeMaterialID, CharacterSensorMaterialID);
  SensorTriggerVolumePair.SetContactCallback(new IgnoreCollisionCallback());
MaterialManager.cs, Initialise()


Kluczowa parą jest para SensorDescribedObjectPair. Odpowiadać będzie za przekazywanie wykrytych przez sensor obiektów do listy w klasie postaci:
1
2
3
4
  SensorDescribedObjectPair = new MaterialPair(
    Engine.Singleton.NewtonWorld,
    DescribedMaterialID, CharacterSensorMaterialID);
  SensorDescribedObjectPair.SetContactCallback(new SensorGameObjectCallback());
MaterialManager.cs, Initialise()


Przejdźmy do implemetnacji wywołań zwrotnych:
1
2
3
4
5
6
7
8
9
  class IgnoreCollisionCallback : ContactCallback
  {
    public override int UserAABBOverlap(
      ContactMaterial material, 
      Body body0, Body body1, int threadIndex)
    {
      return 0;
    }
  }
MaterialManager.cs


To proste wywołanie ignoruje wszelkie kolizje.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class SensorGameObjectCallback : ContactCallback
{
  public override int UserAABBOverlap(
    ContactMaterial material, 
    Body body0, Body body1, int threadIndex)
  {   
    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)
    {
      if (body0.UserData is Character && body0.MaterialGroupID ==
        Engine.Singleton.MaterialManager.CharacterSensorMaterialID)
      {
        Character character = body0.UserData as Character;
        character.Contacts.Add(body1.UserData as GameObject);
      }
      else
      {
        Character character = body1.UserData as Character;
        character.Contacts.Add(body0.UserData as GameObject);
      }
    }
    return 0;
  }
}
MaterialManager.cs


W tym wywołaniu zwrotnym wykorzystujemy znaną już metodę statyczną CollisionCollide sprawdzającą, czy kolizja między ciałami nastąpiła. Następnie, w przypadku zaistnienia kolizji, sprawdzamy, który obiekt jest postacią związaną ze sensorem. Oprócz tego sprawdzamy jego materiał, ponieważ w przypadku kontaktu z inną postacią musimy potrafić rozpoznać, do której należy kolidujący sensor.
Zauważ, że powtarzającą się już procedurę sprawdzania kolizji pomiędzy ciałami można przenieść do osobnej metody statycznej. Dobrym pomysłem jest stworzenie modułu pomocniczych funkcji wykorzystywanych przez silnik.

Dodajmy także przykładowe obiekty do gry. Ściągnij archiwum:

Waza



Wypakuj znajdujące się w nim pliki do folderu Media.

W klasie Program dopisz profil wazy i dwa obiekty o tym profilu:
1
2
3
4
5
6
7
8
9
10
11
12
13
  DescribedProfile vaseProfile = new DescribedProfile();            
  vaseProfile.BodyScaleFactor = new Vector3(0.7f, 1, 0.7f);
  vaseProfile.MeshName = "Vase.mesh";
  vaseProfile.DisplayName = "Vase";
  vaseProfile.Description = "Simple described object";
 
  Described vase0 = new Described(vaseProfile);
  vase0.Position = new Vector3(5.5f, 1.55f, 3.5f);
  Engine.Singleton.ObjectManager.Add(vase0);
 
  Described vase1 = new Described(vaseProfile);
  vase1.Position = new Vector3(5.5f, 1.55f, -3.5f); 
  Engine.Singleton.ObjectManager.Add(vase1);
Program.cs


Sensor jest już sprawny. W każdej klatce będzie posiadać listę widzianych przez postać obiektów. Ten prosty element w przyszłości posłuży nam do wielu czynności, między innymi dzięki niemu postać gracza będzie mogła rozpoznawać imiona innych postaci, rozmawiając z nimi będzie mogła się do nich odwrócić, a inne postacie będą mogły prosić o ustąpienie, gdy gracz będzie stał na drodze, którą idą unikając tym samym bezpośredniego kolidowania ze sobą. Możesz pobrać kod źródłowy i przetestować jego działanie:

Aktualny kod źródłowy



Ćwiczenie 1: Zaimplementuj teleport! Posłuż się wyzwalaczem obszarowym.
Ćwiczenie 2: Wyświetl nazwy widzianych przez postać obiektów w konsoli.


Gra jeszcze w żaden sposób nie daje nam znać o widzianych przez postać obiektach. Jak sprawić, by po podejściu do przedmiotu na ekranie pojawiała się jego nazwa dowiemy się w następnej części. Oprócz tego, dodamy do gry możliwość podnoszenia przedmiotów ekwipunku leżących na ziemi.

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com