armedge documentation

15
1 ARMEDGE ENGINE KOMPONENTOWY SILNIK GIER I APLIKACJI MULTIMEDIALNYCH DLA WINDOWS MOBILE 6 Autor: Michał Skowronek [email protected] Dokument ten zawiera opis silnika ARMEDGE w wersji 0.1, wraz z krótkim omówieniem poszczególnych jego elementów oraz ich właściwej realizacji. 1. WPROWADZENIE ARMEDGE to w założeniu lekki silnik przeznaczony głównie do produkcji gier na platformę Windows Mobile 6. Podczas jego tworzenia moim celem było zaimplementowanie własnego rozwiązania opierającego się na komponentowej reprezentacji obiektów biorących udział w wyświetlaniu i logice aplikacji. Mimo wielu publikacji na ten temat, niezwykle trudno znaleźć konkretne przykłady tworzenia tego typu systemów. Większość z nich prezentuje niewielkie fragmenty i samą ideologię działania, abstrahując od ukazania konkretnego sposobu realizacji. W tym dokumencie nie będę powtarzał wyżej wspomnianych schematów i skupię się na przedstawieniu idei i konkretnego sposobu wcielenia tej idei w życie. Zaznaczyć jednak trzeba, że przedstawione przeze mnie rozwiązania w wielu przypadkach nie są doskonałe co spowodowane jest mocno rozwojową wersją aktualnie opisywanego silnika. 2. SYSTEM KOMPONENTOWY Podstawową ideą systemu komponentowego jest reprezentacja obiektu (Entity) za pomocą zbioru komponentów, zawierających w sobie pewne określone zachowania oraz właściwości, które razem tworzą obiekt. W zasadzie obiekt(Entity)staje się w tym momencie jedynie kontenerem na komponenty. Dzięki takiemu rozwiązaniu możemy w miarę prosto tworzyć moduły wielokrotnego użytku, przez co prościej uniknąć dużej nadmiarowości kodu w porównaniu z modelem hierarchicznym. Największą zaletą jest jednak łatwość późniejszego tworzenia obiektów. Składamy je po prostu z klocków zwanych komponentami.

Upload: skowronkow

Post on 26-Jun-2015

47 views

Category:

Technology


0 download

DESCRIPTION

Armedge - 2d component Engine for Widnows Mobile 6.5 (PL only)

TRANSCRIPT

Page 1: Armedge documentation

1

ARMEDGE ENGINE KOMPONENTOWY SILNIK GIER I APLIKACJI MULTIMEDIALNYCH DLA WINDOWS MOBILE 6 Autor: Michał Skowronek [email protected]

Dokument ten zawiera opis silnika ARMEDGE w wersji 0.1, wraz z krótkim omówieniem poszczególnych jego elementów oraz ich właściwej realizacji.

1. WPROWADZENIE

ARMEDGE to w założeniu lekki silnik przeznaczony głównie do produkcji gier na platformę Windows Mobile 6. Podczas jego tworzenia moim celem było zaimplementowanie własnego rozwiązania opierającego się na komponentowej reprezentacji obiektów biorących udział w wyświetlaniu i logice aplikacji. Mimo wielu publikacji na ten temat, niezwykle trudno znaleźć konkretne przykłady tworzenia tego typu systemów. Większość z nich prezentuje niewielkie fragmenty i samą ideologię działania, abstrahując od ukazania konkretnego sposobu realizacji. W tym dokumencie nie będę powtarzał wyżej wspomnianych schematów i skupię się na przedstawieniu idei i konkretnego sposobu wcielenia tej idei w życie. Zaznaczyć jednak trzeba, że przedstawione przeze mnie rozwiązania w wielu przypadkach nie są doskonałe co spowodowane jest mocno rozwojową wersją aktualnie opisywanego silnika.

2. SYSTEM KOMPONENTOWY

Podstawową ideą systemu komponentowego jest reprezentacja obiektu (Entity) za pomocą zbioru komponentów, zawierających w sobie pewne określone zachowania oraz właściwości, które razem tworzą obiekt. W zasadzie obiekt(Entity)staje się w tym momencie jedynie kontenerem na komponenty. Dzięki takiemu rozwiązaniu możemy w miarę prosto tworzyć moduły wielokrotnego użytku, przez co prościej uniknąć dużej nadmiarowości kodu w porównaniu z modelem hierarchicznym. Największą zaletą jest jednak łatwość późniejszego tworzenia obiektów. Składamy je po prostu z klocków zwanych komponentami.

Page 2: Armedge documentation

2

Jakiego rodzaju komponenty może posiadać obiekt(Entity)?

Mogą to być komponenty odpowiedzialne za wyświetlanie grafiki (np. SpriteRendererComponent - przejmujący na siebie zadanie obsługi i wyświetlania spritów), odgrywanie dźwięku (np. SoundSourceComponent odpowiedzialny za odgrywanie skojarzonych z obiektem dźwięków), wykrywanie kolizji (np. CollisionComponent biorący udział w wykrywaniu kolizji skojarzonego obiektu oraz informowania o tym innych komponentów) czy wreszcie za kontrolowanie postaci naszej gry (np. PlayerControllerComponent obsługujący i odpowiednio reagujący na wszelkie zdarzenia naciśnięcia klawisza czy ruchu stylusa bądź myszki).

Należy zwrócić przy tym uwagę, że pewne właściwości opisujące obiekt są wspólne dla różnych typów komponentów. Przykładem może być komponent renderujący, który musi znać pozycję obiektu po to by móc poprawnie wyświetlić reprezentujący go obiekt graficzny oraz komponent sterujący postacią gracza, który również z tej pozycji korzysta modyfikując ją. Właściwość ta nie może więc być własnością któregokolwiek komponentu. Należy ona tak naprawdę do obiektu.

Rozwiązaniem tego problemu było wprowadzenie systemu właściwości współdzielonych, które należą do obiektu(Entity), natomiast komponenty z tych właściwości po prostu korzystają.

Page 3: Armedge documentation

3

Każdy komponent korzystający z właściwości zachowuje do niej referencję by w łatwy sposób móc się do niej odwołać. Właściwości mogą być oczywiście różnego typu. Taką możliwość dało zastosowanie szablonu BOOST::VARIANT, który pozwala na przechowywanie wartości jednego ze zdefiniowanych wcześniej typów. Zdecydowałem się na przechowywanie we właściwościach jedynie typów podstawowych. Takie rozwiązanie jest zarówno dość elastyczne jak i szybkie.

W przypadku dostępu do właściwości z poziomu komponentów docelowo będzie użyta metoda obiektu(Entity) wiążąca referencję właściwości z polem komponentu. Dzięki temu w przypadku braku wymaganej właściwości zostanie ona utworzona. Kolejną korzyścią płynącą z takiego rozwiązania jest całkowite uniezależnienie komponentów oraz kolejności ich dodawania.

Klasa obiektu(Entity) zawiera więc 2 podstawowe kontenery

przechowujący współdzielone właściwości std::map<std::wstring, boost::variant<std::wstring, int, double, bool> > properties

przechowujący komponenty std::map<std::wstring, Component*> components

Sama komunikacja komponentu z obiektem(Entity) (który to zaopatruje go również w szereg niezbędnych interfejsów) odbywa się dzięki przekazywanej do komponentu referencji do obiektu(Entity) zawierającego.

Page 4: Armedge documentation

4

3. KOMPONENT

Czym więc właściwie jest komponent?

Najprościej rzecz ujmując jest to pewien obiekt zawierający szereg określonych zachowań i właściwości pozwalających sprawnie współistnieć w systemie. W ARMEDGE każdy komponent zawiera wspólną dla jego typu nazwę(w celu uniemożliwienia dodania do obiektu(Entity) dwóch komponentów tego samego typu) oraz następujące zachowania:

OnAdd Wywoływana w momencie dodania komponentu do obiektu(Entity). To tutaj powinna nastąpić wszelaka inicjalizacja obiektu.

OnRemove Wywoływana w momencie usunięcia komponentu z obiektu(Entity). Tutaj następuje wszelkie zwalnianie szeroko pojętych zasobów związanych z komponentem.

Obu zachowań dostarcza klasa bazowa komponentu:

Jednak to nie wszystko. Każdy komponent może zostać wzbogacony o dwa kolejne zachowania w zależności od potrzeb. Zachowania te są ściśle związane z dwoma interfejsami:

Jak łatwo się domyśleć implementacja interfejsu IUpdateable daje możliwość uaktualniania się komponentu w każdej klatce. Analogicznie interfejs IRenderable umożliwia odrysowania się komponentu. Różnice między oboma interfejsami sprowadzają się jedynie do tego, że wszelkie komponenty są najpierw aktualizowane a dopiero później rysowane. Poza tym komponent implementujący interfejs IRenderable ma prawo określenia warstwy na której będzie rysowany(analogicznie do warstw programów graficznych, daje to możliwość wyboru przysłaniania jednych obiektów innymi).

Page 5: Armedge documentation

5

4. SCENA

Nieodłączną częścią silnika jest obiekt sceny. To ona zarządza wszelkimi obiektami(Entities) oraz komponentami. Jest odpowiedzialna za tworzenie obiektów(Entities), które od razu przynależą do niej oraz odpowiednie przetwarzanie komponentów.

To właśnie przetwarzanie, a konkretnie aktualizacja i rysowanie wszystkich komponentów implementujących odpowiedni interfejs jest głównym zadaniem sceny.

Dzięki temu wszystkie komponenty wymagające aktualizacji są przetwarzane przed ewentualnym narysowaniem. Dodawanie komponentów do sceny odbywa się podczas dodawania ich do obiektów(Entities), które również są przez nią przechowywane. Pozwala to w odpowiedni sposób reagować na usunięcie obiektu(Entity).

Obowiązek managera scen przejmuje główna klasa silnika. Jest odpowiedzialna za dodawanie oraz usuwanie scen do aplikacji, a także zmianę aktualnie wyświetlanej sceny.

5. KOMUNIKACJA

Komponenty muszą, jak można się domyśleć, komunikować się z innymi tak w obrębie obiektu jak i poza nim. Aby zachować niezależność poszczególnych komponentów od siebie został zastosowany system komunikatów.

Page 6: Armedge documentation

6

Odpowiada za to EventManager wraz z klasami pomocniczymi. Jest to własna realizacja double dispatcher’a dla języka C++.

Do swojego działania wykorzystuje następujące klasy i interfejsy:

Podczas dodania nowego słuchacza zdarzenia metodą AddEventListener,

template<class Args> void AddEventListener( std::wstring eventName, EventListener<Args>* listener )

jeśli wcześniej nie istnieje dodawany jest do EventManager’a obiekt klasy CustomEvent<Args> o typie argumentów Args dziedziczącym po klasię EventArgs i zdefiniowanej nazwie zdarzenia. Przechowuje on z kolei słuchaczy implementujących klasę szablonową EventListener<Args> o tym samym typie argumentów.

Wywołanie metody

void Dispatch( std::wstring eventName, EventArgs* args )

Page 7: Armedge documentation

7

Wyszukuje zdarzenie o danej nazwie(eventName) a ten rozgłasza zdarzenie o typie argumentów implementujących EventArgs do wszstkich swoich słuchaczy. Sama klasa EventArgs zawiera jedynie nazwę ID, do której najczęsciej będzie przypisana nazwa obiektu(Entity) właściciela.

Każda scena jak i każdy obiekt(Entity) posiada swojego EventManager’a aby zapewnić możliwość komunikacji komponentów między obietkami(Entities) jak i wewnątrz obiektu(Entity).

6. USŁUGI

W silniku zrezygnowano całkowicie z użycia wzorca Singleton jako moim zdaniem psującego całą ideologię projektowania obiektowego. Zdecydowano się natomiast na użycie znanego choćby z Microsoftowego XNA, systemu usług systemu.

Jak widać każda usługa implementuje interfejs IService, tak by mogła być przechowywana w systemie w jednym kontenerze. Prośba o funkcjonalność danej klasy usługi odbywa się poprzez pobranie referencji do jej szczegółowego interfejsu za pomocą metody GetService,

template<class T> T* GetService(std::wstring serviceName)

gdzie serviceName to nazwa zwyczajowa serwisu a T jest jego szczegółowym interfesjem.

W ARMEDGE znajdują się 4 podstawowe usługi:

IGraphicsService Usługa udostępniająca metody związane z renderingiem dla komponentów rysujących.

ISoundService Usługa udostępniająca metody związane z odtwarzaniem dźwięków.

Page 8: Armedge documentation

8

IContentSrvice Usługa wspomagająca używanie zewnętrznych zasobów (pliki graiczne, dźwiękowe), zwalniając z obowiązku ręcznego usuwania ich oraz wspomagająca współdzielenie ich między wieloma komponentami.

IInputService Usługa daje możliwość komponentom dostępu do urządzeń wejściowych i odczytu ich stanu.

Metody dostępu do usług jak i do innych potrzebnych elementów systemu są przekazywane do komponentów (jak i do samych usług aby umożliwić komunikację między nimi) w postaci kontekstu aplikacji (IArmedgeContext), implementowanego przez główną klasę silnika.

7. NIE TAKI ARMEDGE STRASZNY

Na zakończenie przedstawiam niewielki przykład użycia silnika ARMEDGE. Nie jest to oczywiście przykład z realnej aplikacji (pewna zamierzona nadmiarowość oraz brak optymalizacji). Pokazuje on jedynie sposób w jaki w tym momencie możemy tworzyć i dodawać sceny, obiekty i komponenty oraz jak wykorzystać możliwości jakie oferują.

Page 9: Armedge documentation

9

RenderComponent class: Klasa komponentu wyświetlania. Korzysta ze współdzielonych właściwości obiektu po to by w odpowiednim miejscu narysować odpowiedniej wielkości kwadrat class RenderComponent: public Component, public IRenderable { public: void OnAdd() { gfx = owner->GetContext()->GetService<IGraphicsService>(L"Graphics"); x = owner->AssignReference<int>(L"X"); y = owner->AssignReference<int>(L"Y"); width = owner->AssignReference<int>(L"WIDTH"); height = owner->AssignReference<int>(L"HEIGHT"); // for testing purpose only, normally exposed outside *width = 30; *height = 30; } void OnRemove() {} void OnRender() { gfx->ClearScreen(RGB_TO_16(100,100,100)); gfx->DrawLine(*x, *y, *x + *width, *y, RGB_TO_16(255, 255, 150)); gfx->DrawLine(*x, *y, *x, *y + *height, RGB_TO_16(255, 255, 150)); gfx->DrawLine(*x + *width, *y, *x + *width, *y + *height, RGB_TO_16(255, 255, 150)); gfx->DrawLine(*x, *y + *height, *x + *width, *y + *height, RGB_TO_16(255, 255, 150)); } std::wstring GetName() const { return name; } private: int *x; int *y; int *width; int *height; IGraphicsService *gfx; static std::wstring name; };

std::wstring RenderComponent::name = L"RenderComponent";

HealthArgs class: Klasa argumentu zdarzenia. W naszym przypadku zdarzenie dotyczyć będzie zderzenia naszego kwadratu z prawą ścianą. class HealthArgs : public EventArgs { public: int Health; };

Page 10: Armedge documentation

10

SquareControllerComponent class: Klasa komponentu odpowiedzialnego za sterowanie prostokątem. Podobne jak klasa renderowania prostokąta korzysta z tych samych współdzielonych właściwości obiektu. Dodatkowo komponent ten wyzwala zdarzenie utraty zdrowia w momencie zetknięcia się naszego kwadratu z prawą scianą. class SquareControllerComponent: public Component, public IUpdateable { public: void OnAdd() { input = owner->GetContext()->GetService<IInputService>(L"Input"); x = owner->AssignReference<int>(L"X"); y = owner->AssignReference<int>(L"Y"); width = owner->AssignReference<int>(L"WIDTH"); height = owner->AssignReference<int>(L"HEIGHT"); } void OnRemove() {} void OnUpdate(Timer &time) { if (input->KeyPressed(KEY_RIGHT) && (*x + *width) < owner->GetContext()->GetWidth()-1) *x+=1; if (input->KeyPressed(KEY_LEFT) && *x > 0) *x-=1; if (input->KeyPressed(KEY_DOWN) && (*y + *height) < owner->GetContext()->GetHeight()-1) *y+=1; if (input->KeyPressed(KEY_UP) && *y > 0) *y-=1; // after touch right wall death if((*x + *width) == owner->GetContext()->GetWidth()-1) { HealthArgs args; args.senderId = owner->GetName(); args.Health = 0; owner->GetSceneEventManager().Dispatch(L"Death", &args); } } std::wstring GetName() const { return name; } private: int *x; int *y; int *width; int *height; IInputService *input; static std::wstring name; }; std::wstring SquareControllerComponent::name = L"SquareControllerComponent";

Page 11: Armedge documentation

11

HealthCheckerComponent class: Klasa komponentu sprawdzająca czy dany obiekt został zabity i jeśli tak zamyka aplikację. Jak widać do obsługi zdarzenia potrzebna była implementacja szablonu EventListener<HealthArgs> obligującego do utworzenia w klasie metody obsługującej zdarzenie (OnEvent(const HealthArgs &args)). Aby jednak otrzymać zdarzenie potrzebne jest jeszcze zarejestrowanie komponentu jako nasłuchującego danego zdarzenia. Dzieje się to w metodzie OnAdd. class HealthCheckerComponent: public Component, public EventListener<HealthArgs> { public: void OnAdd() { owner->GetSceneEventManager().AddEventListener<HealthArgs>(L"Death", this); } void OnRemove() { owner->GetSceneEventManager().RemoveEventListener(L"Death", this); } void OnEvent(const HealthArgs &args) { //Quit after event if(args.senderId == targetId) owner->GetContext()->Quit(); } void SetObjectToCheck(std::wstring id) { targetId = id; } std::wstring GetName() const { return name; } private: std::wstring targetId; static std::wstring name; }; std::wstring HealthCheckerComponent::name = L"HealthCheckerComponent";

Page 12: Armedge documentation

12

Main.cpp: Wreszczie nasza aplikacja może być utworzona. Tak wygląda główny plik tego proektu testowego. Jak widać niemal cała logika została przeniesiona do komponentów a tym samym obiektów.

#include <windows.h> #include "Armedge.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { //Utworzenie głównej klasy aplikacji oraz dodanie podstawowych usług. Armedge arm; //Inicjalizacja silnika (tutaj również inicjalizowane są usługi) arm.Initialize(hInstance); //Utworzenie sceny I dodanie jej o przetwarzania. Scene* scene = new Scene(L"main"); arm.AddScene(scene);

//Utworzenie obiektu (naszego kwadracika) I wypelnienie go odpowiednimi //komponentami.

Entity *entity = scene->AlocateEntity(L"hero"); entity->AddComponent(new RenderComponent); entity->AddComponent(new SquareControllerComponent); //Utworzenie komponentu sprawdzającego śmierć obiektu. HealthCheckerComponent* checkerComponent = new HealthCheckerComponent; //Ustawienie celu obserwacji dla komponentu sprawdzającego. checkerComponent->SetObjectToCheck(L"hero"); //Dodanie obiektu zawierającego nowy komponent do sceny. entity = scene->AlocateEntity(L"checker"); entity->AddComponent(checkerComponent); //Ustawienie aktywnej sceny oraz uruchomienie aplikacji arm.SetCurrentScene(L"main"); arm.Run(); return 0; }

Page 13: Armedge documentation

13

8. ZESTAWIENIE KLAS

Page 14: Armedge documentation

14

Armedge Armedge engine main class

Component Component class. Base class for all custom components

Content Content service

CustomEvent< Args >

Custom event template class. It is used by system for register event based on event arguments type

Entity Entity class. This class represents every single object on scene

EventArgs Default arguments for event. Custom event arguments must specialize it if want to store extra data

EventListener< Args >

Event listener. Implement this one by any listener class

EventManager Event system manager. Main dispatcher

Graphics Graphics service

IArmedgeContext Armedge context

IContentService Content service interface used by custom components to handle using and loading content

IEvent Event interface. It is stored in event manager's container. Used by event system

IGraphicsService Graphics service interface used by custom components to handle graphics rendering

IInput Input service used by engine to handle input

IInputService Input service interface used by custom components to handle input

IListener Listener interface. Used by event system

Input Input service

IRenderable Renderable interface. This interface must be implemented by all renderable components

IRenderer Renderer interface used by engine to handle rendering

IResource Resource. Base interface that must be implemented by any resource type object

IService Service interface. Every custom engine service should implement this to get it work in system

ISoundService Sound service interface used by custom components to handle sound

IUpdateable Updateable inteface. This interface must be implemented by all updateable components

Scene

Scene class. It manages all entities and components. Uses different approach for updateable (implementing IUpdateable interface) and renderable (implementing IRenderable interface) components. Scene is also responsible for creation of every entity (automatically connects entity to it)

Sound Sound service

Timer Timer class used by engine to timing purposes

Page 15: Armedge documentation

15

9. ZAKOŃCZENIE

Jak widać obsługa silnika jest naprawdę prosta a jego sposób działania, mimo jeszcze wielu braków i niedociągnięć wydaje się mieć pewien potencjał czerpany z komponentowego ujęcia kwestii zarządzania sceną. Choć na dzień dzisiejszy jest jeszcze sporo do zrobienia, mam nadzieję wciąż rozwijać i optymalizować to rozwiązanie. Jeśli więc doczytałeś do tego miejsca i uważasz, że masz jakiś ciekawy pomysł na usprawnienie/poprawienie jakichś elementów skontaktuj się ze mną. Chętnie podyskutuje na ten temat.

Życzę przyjemnego i bezstresowego korzystania z ARMEDGE!