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

09.08.2010 - Mateusz Osowski
TrudnośćTrudność

Tworzymy kod!

Zacząć musimy od klasy Engine. Pierwszą metodą, którą napiszemy będzie najmniej przyjemna inicjacja silników Ogre i Newton. Dalej będzie już tylko z górki. Na początek, utwórz nową klasę. W tym celu dodaj nowy obiekt do projektu:



Typem nowego elementu będzie klasa, a nazwa pliku to Engine.cs.

Silnik gry jako taki powinien istnieć tylko jeden na grę. Do ograniczania ilości instancji danej klasy służy wzorzec projektowy singleton. Zaimplementujmy go więc, zanim przejdziemy do pisania właściwych metod. Co powinien singleton?

  • Zapobiegać ręcznemu tworzeniu instancji swojej klasy
  • Zapobiegać dziedziczeniu po sobie (przeciążaniu)
  • Istnieć jeden na aplikację, bez względu na ilość wątków

Zacznijmy tworzyć zawartość pliku Engine.cs. Aby zapobiec możliwości dziedziczenia po klasie Engine, wystarczy dopisać do niej modyfikator sealed. Klasa jest już "zapieczętowana".

1
2
sealed class Engine
{
Engine.cs


Aby zapobiec ręcznemu tworzeniu więcej niż jednego obiektu, wystarczy konstruktor uczynić prywatnym - wtedy można go wywołać jedynie wewnątrz samej klasy.

1
2
3
    Engine()
    {
    }
Engine.cs


Istnieć powinna jedna instancja, tworzona tylko raz w trakcie działania aplikacji. Pomocny jest statyczny konstruktor - wywołać się może on tylko raz w trakcie działania programu (w trakcie istnienia AppDomain z nim związanym). Wywoływany jest w momencie pierwszego wywołania statycznej metody klasy. W tym właśnie statycznym konstruktorze utworzymy sobie jedyną instancję singletonu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    // nasza instancja - prywatna, dostępna jedynie poprzez akcesor (metodę zwracającą ją)
    static Engine instance; 
 
    static Engine()
    {
        instance = new Engine();
    }
 
    // Taki akcesor jest statyczną metodą, która wywołana po raz pierwszy
    // zainicjuje statyczny konstruktor
    public static Engine Singleton
    {
        get
        {
            return instance;
        }
    }
}
Engine.cs


Przejdźmy do inicjalizacji silników. Wróć na początek pliku. Najpierw w sekcji using dopisz przestrzenie nazw bibliotek Mogre i MogreNewt:

1
2
using Mogre;
using MogreNewt;

Następnie przejdź do klasy Engine i dodaj do niej pola:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    // Główny obiekt silnika Ogre
    public Root Root;
    // Obiekt okna, do którego Ogre będzie renderować grafikę
    public RenderWindow RenderWindow;
    // Menadżer sceny Ogre - przechowuje informacje o obiektach w postaci drzewa
    public SceneManager SceneManager;
    // Kamera Ogra - nie myl jej z klasą GameCamera
    public Camera Camera;
    // Prostokąt w którym będzie wyświetlany obraz z kamery
    public Viewport Viewport;
    // Obiekt klawiatury
    public MOIS.Keyboard Keyboard;
    // Obiekt myszy
    public MOIS.Mouse Mouse;
    // Menadżer urządzeń wejścia
    public MOIS.InputManager InputManager;
    // Główny obiekt silnika Newton
    public World NewtonWorld;
        // Debugger Newtona - rysuje siatki kolizji
        public Debugger NewtonDebugger;
Engine.cs


Jak widzisz, w C# nazwy instancji mogą być takie same jak nazwy klas. Kiedy nazwa jest naturalna - nie ma sensu na siłę wymyślać innej.

Przejdźmy do metody inicjującej silniki. W C# przyjęło się tworzyć metody pod wszystkimi polami. Pamiętaj o tym dopisując nowe pola do klasy. Najpierw, stwórzmy główny obiekt Ogre - Root. Obiekt klasy ConfigFile wczyta zawartość pliku Resources.cfg. Drugim parametrem jest lista znaków speratorów, które mogą oddzielać nazwę typu zasobu od ścieżki do niego.

1
2
3
4
5
    public void Initialise()
    {
        Root = new Root();
        ConfigFile cf = new ConfigFile();
        cf.Load("Resources.cfg", "\t:=", true);
Engine.cs


Na stępnie musimy przekazać informacje wczytane z pliku do silnika. Robimy to iterując po sekcjach pliku, a następnie po parach typ-ścieżka w tych sekcjach. Silnik informowany jest o katalogach poprzez ResourceGroupManager. Typem wpisu może być Filesystem - zwyczajny folder dyskowy, lub Archive - spakowany plik .zip:

1
2
3
4
5
6
7
8
9
10
11
12
        ConfigFile.SectionIterator seci = cf.GetSectionIterator();
 
                // Iterujemy po sekcjach
        while (seci.MoveNext())
        {
            ConfigFile.SettingsMultiMap settings = seci.Current;
                        // Iterujemy po parach typ - ścieżka
            foreach (KeyValuePair<string, string> pair in settings)
                                // Dodajemy lokacje zasobów do silnika
                ResourceGroupManager.Singleton.AddResourceLocation(
                    pair.Value, pair.Key, seci.CurrentKey);
        }
Engine.cs


Teraz możemy przejść do wczytania konfiguracji wyświetlania silnika. Domyślnie wyświetlane będzie okienko konfiguracyjne, w którym użytkownik może wybrać renderer i jego ustawienia. Ogre zapamiętuje te ustawienia w pliku ogre.cfg, dzięki czemu przy następnym uruchomieniu użytkownik nie będzie o nie pytany.

1
2
3
4
5
                // Próbujemy przywrócić zasoby z pliku ogre.cfg
        if (!Root.RestoreConfig())                      
                        // Jeżeli się to nie powiodło, prosimy użytkownika o wybranie ustawień
            if (!Root.ShowConfigDialog())
                return;
Engine.cs


Po skonfigurowaniu silnika tworzymy obiekt okna i inicjujemy zasoby. Parametr funkcji Initialise mówi, czy silnik ma automatycznie utworzyć okno, czy też chcemy zając się tym sami. Druga linijka nakazuje menadżerowi grup zasobów analizę zawartości podanych wcześniej ścieżek. Silnik zapamięta informacje o teksturach, modelach (lecz je same załaduje dopiero gdy będą potrzebne), sparsuje skrypty i skompiluje shadery, jeżeli na takowe natrafi w podanych katalogach.

1
2
        RenderWindow = Root.Initialise(true);           
        ResourceGroupManager.Singleton.InitialiseAllResourceGroups();
Engine.cs


Kolejno, potrzebujemy otworzyć menadżera sceny o przeznaczeniu ogólnym oraz kamerę. Przydzielamy kamerę do prostokąta pokrywającego domyślnie całe okno. Możesz utworzyć większą ilość Viewport - tak uzyskuje się efekt split screen. Dodatkowo określamy przedział, w którym znajdujące się modele kamera będzie widzieć.

1
2
3
4
5
6
7
                        
        SceneManager = Root.CreateSceneManager(SceneType.ST_GENERIC);
        Camera = SceneManager.CreateCamera("MainCamera");
                // Wydzielamy na oknie obszar, na który trafi obraz z kamery
        Viewport = RenderWindow.AddViewport(Camera);
        Camera.NearClipDistance = 0.1f;
        Camera.FarClipDistance = 1000.0f;
Engine.cs


Kolejne cztery linijki mają charakter czysto techniczny. Musimy bowiem przygotować listę parametrów potrzebnych do stworzenia menadżera urządzeń wejścia, która de facto składa się tylko z uchwytu do okna utworzonego przed chwilą.

1
2
3
4
        MOIS.ParamList pl = new MOIS.ParamList();
        IntPtr windowHandle;
        RenderWindow.GetCustomAttribute("WINDOW", out windowHandle);
                pl.Insert("WINDOW", windowHandle.ToString());
Engine.cs


Tworzymy wspomniany menadżer, po czym przystępujemy do zainicjowania obiektów klawiatury i myszki. Parametry false oznaczają, że wejście nie będzie buforowane. Nie będziemy bowiem używać wywołań zwrotnych dla zdarzeń wejścia.

1
2
3
4
5
6
        InputManager = MOIS.InputManager.CreateInputSystem(pl);
 
        Keyboard = (MOIS.Keyboard)InputManager.CreateInputObject(
            MOIS.Type.OISKeyboard, false);
        Mouse = (MOIS.Mouse)InputManager.CreateInputObject(
            MOIS.Type.OISMouse, false);                 
Engine.cs


Ostatnią rzeczą, która pozostała nam do stworzenia jest świat Newtonowski. Tworzy się go bardzo prosto. Tworzymy także NewtonDebugger.

1
2
3
4
        NewtonWorld = new World();
        NewtonDebugger = new Debugger(NewtonWorld);
        NewtonDebugger.Init(SceneManager);
    }
Engine.cs


Napiszmy metodę Update, która będzie wywoływana co klatkę. Ponieważ zgodnie z projektem klasa Engine zarządza obiektami silników i menadżerów - warto skupić ich aktualizację w jednym miejscu. Musimy zaktualizować urządzenia wejścia, świat Newtona i renderer Ogre. Aby uniknąć zbędnych komplikacji zakładamy, że ilość renderowanych na sekundę klatek wynosi 60. Później dowiesz się o mechanizmie akumulowania czasu i kontroli renderowania klatek.

1
2
3
4
5
6
7
8
9
    public void Update()
    {
        Keyboard.Capture();
        Mouse.Capture();
        NewtonWorld.Update(1.0f / 60.0f);
        Root.RenderOneFrame();            
                // Przekazujemy kontrolę do systemu, aby program nie przestał mu odpowiadać
        WindowEventUtilities.MessagePump();
    }
Engine.cs


Przejdźmy teraz do pliku Program.cs i utwórzmy prostą pętlę gry: W każdym przebiegu pętli aktualizujemy stan gry, a w przypadku naciśnięcia klawisza Esc wychodzimy z pętli.

1
2
3
4
5
6
7
8
9
10
11
12
static void Main(string[] args)
{
    Engine.Singleton.Initialise();
 
    while (true)
    {
        Engine.Singleton.Update();
 
        if (Engine.Singleton.Keyboard.IsKeyDown(MOIS.KeyCode.KC_ESCAPE))
            break;              
    }           
}
Program.cs


Możesz uruchomić grę. Twoim oczom powinno ukazać się okienko konfiguracyjne Ogre. Ustaw synchronizację pionową (Vsync) na Yes, by ograniczyć ilość wyświetlanych klatek do częstotliwości odświeżania ekranu (zazwyczaj 60Hz). Warto też testować grę w trybie okienkowym (Fullscreen - No).

Po zatwierdzeniu opcji silnik powinien renderować czarny ekran i zakończyć pracę po wciśnięciu Escape. Możesz pobrać projekt w aktualnym stanie:

Projekt Visual C#

Bardzo ważne:

Jeżeli podczas próby uruchomienia aplikacji środowisko informuje o niezłapanym wyjątku FileLoadException lub występuje inny błąd upewnij się, że kompilujesz projekt dla architektury x86, ponadto katalog wskazany we właściwościach projektu jako "Working Directory" powinien zawierać:

  • Pliki Mogre.dll, MOIS.dll, MogreNewt.dll
  • Pliki OgreMain.dll, RenderSystem_Direct3D9.dll
  • Pliki Resources.cfg, Plugins.cfg
  • Katalog Media

W swoim systemie powinny być zainstalowane:

  • DirectX9 Redistributable
  • Visual C++ 2008 SP1 Redistributable

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com