Budujemy silnik: architektura, renderer

20.12.2008 21:14 in grafika 3D, cg, OpenGL, programowanie, C++, silnik

Mój silnik składa się z kilku modułów. Każdy moduł to osobny plik DLL (tutaj wielkie brawa dla VS, które radzi sobie z solution z kilkoma projektami DLL i dwoma projektami gier tak, jakby wszystko było w tym samym projekcie -- powiem szczerze, że nie spodziewałem się, że w C++ coś takigo się uda). Modułem jest na przykład obsługa danych, wejścia, dźwięku, grafiki, fizyki, etc. Przykładowy kod gry wygląda następująco:

void uGame::Run()
{
  try
  {
    uDataManager dm("data.utd");
    uWorld world;
    uRenderer renderer(dm, world);
    renderer.Init();

    uLight *l = new uLight();
    l->SetPosition(...);
    ...
    world.AddObject(l);

    uModel *m = new uModel();
    m->SetFilename("car.utm");
    world.AddObject(m);

    uCamera *c = new uCamera;
    c->SetPosition(...);
    ...
    world.AddObject(c);
    renderer.SetCamera(c);

    world.OnLogic += UDELEGATE(&MyLogic);

    renderer.StartLoop();	
  }
  catch (uException e)
  {
    ...
  }
}

void uGame::MyLogic(uWorld &world)
{
  ...
}

Jak widać, użycie renderera zostało sprowadzone do trzech bardzo prostych operacji. Ważne rzeczy:

  • renderer nie jest bezpośrednio sterowany przez użytkownika (w tej wersji ma pomocniczą funkcję SetCamera, która zostanie jednak zastąpiona mechanizmem ustawień)
  • wszystkie opcje (rozdzielczość, detale czy w przyszłości aktywna kamera) są przekazywane jako obiekty typu RendererSettings (dziedziczącym po abstrakcyjnym obiekcie, tak samo jak kamera czy model)
  • renderer sam ,,wpina'' się w świat, dodając swojego delegata do obsługi zdarzenia AddObject
  • przy okazji: prosty kod do obsługi delegatów w stylu C#:
#define UDELEGATE(a) fastdelegate::MakeDelegate(this, a);

template <class T>
class uEvent1
{
  typedef fastdelegate::FastDelegate1<T> _delegate;

  std::list<_delegate> mDelegates;
public:
  void operator +=(_delegate &d)
  {
    mDelegates.push_back(d);		
  }
  void operator -=(_delegate &d)
  {
    mDelegates.remove(d);
  }
  void clear()
  {
    mDelegates.clear();
  }
  void operator() (T &value)
  {		
    for (std::list<_delegate>::iterator
      it = mDelegates.begin(); it != mDelegates.end(); it++)
    {
      (*it)(value);
    }
  }
};
  • jak było wspomniane wcześniej, użytkownik nie ,,obsługuje'' samemu renderera. To renderer, korzystając z modułu danych, wczytuje niezbędne modele, tekstury i inne zasoby

Takie podejście to pełne oddzielenie wyświetlania danych od ich logicznego znaczenia. Pozwala na równoległą pracę nad rendererem i innymi modułami w przyjemny i bezpieczny sposób. Umożliwia kompletną wymianę renderera. Na koniec można zauważyć, że w ten sposób w silniku 3D realizujemy wzorzec MVP (model-widok-kontroler). :)

Comments:

  1. Reg

    Reg:

    Dlaczego robisz DLL zamiast LIB?

    21.12.2008 13:02:28

  2. Tomasz Dąbrowski

    Tomasz Dąbrowski:

    LIB-ów używam tylko do importowania funkcji (żeby samemu nie GetProc-ować). Natomiast używam DLL, by w przyszłości móc dołożyć np. nowy renderer do starego projektu.

    21.12.2008 15:29:47

Leave comment: