RL Engine – Architecture

Pracę przy silniku wypadałoby zacząć od jego architektury.

W tej chwili mamy tylko jeden problem natury architektonicznej.
Mianowicie, jak ukształtować API silnika tak aby było przyjemne dla jego klienta.
Mówimy to o ogólnym szkicu, kierunku czy pomyśle a nie szczegółach. 1

Są dwa “główne” podejścia przy tworzeniu frameworków/silników:

  • Kod klienta działa w naszym systemie – nazwijmy to podejściem “Engine”
  • Nasz system działa w kodzie klienta – nazwijmy to podejściem “Library”
framework-vs-library
Framework/Engine vs Library

 

Podejście Engine:

Bardzo popularne w silnikach do gier, choć nie tylko. W ten sam sposób działają też np.

  • WebAPI (REST framework dla C#)
  • Akka (Java framework do pisania aplikacji w formie aktorów)
  • Django (Python REST framework)
  • Ruby on Rails (Ruby web framework)

Podejście to manifestuje się posiadaniem “magicznych” klas. Aby dodać jakiś fragment logiki klient takiego systemu musi utworzyć klasę która dziedziczy2 po jednej z tych “magicznych” klas wybranego systemu.

A czy to źle? Wydaje mi się, że Twój “magiczny” przymiotnik lekko trąci pesymizmem…

Nie skądże! …nooo może trochę.

Ogólnie nie mam nic przeciwko takim architekturom, nawet są one wskazane dla niektórych zastosowań. Jeżeli dany system zapewnia klientowi całe środowisko (np. Unity3D) to jak najbardziej jest krok w dobrą stronę. Ilość pracy wymaganej do uruchomienia produktu jest wtedy bardzo mała.

Jednakże! Zdarza się tak iż owe “magiczne” klasy są naszpikowane różnymi zależnościami do których nie mamy normalnie dostępu. Co często powoduje utrudnione testowanie a czasami wręcz uniemożliwienia go.

Jak już jesteśmy przy Unity3D to popatrzmy na jego “magiczny” MonoBehaviour.

MonoBehaviour is the base class every script derives from.

Czyli aby cokolwiek zrobić w silniku musimy dziedziczyć po MonoBehaviour.

Ok, to nic nadzwyczajnego. Załóżmy, że mamy już jakiś kod który chcemy przetestować (jednostkowo). Przy pierwszej próbie dostaniemy na twarz coś w stylu:

SecurityException: ECall methods must be packaged into a system module

Cóż, obiektów typu MonoBehaviour nie można tworzyć przy pomocy słowa kluczowego “new” więc cały kod w klasie dziedziczącej po MonoBehaviour jest nietestowalny. Oczywiście można przenieść kod do “czystych” klas i oddelegować wywołania w MonoBehaviour ale nie jest to jakoś specjalnie wygodne.

Nie jestem “anty-unity” – a wręcz przeciwnie, jest to jeden z najczęściej wykorzystywanych przeze mnie silników. Ale jego API nie nazwał bym wygodnym.

Jak widać podejście “Engine” gwarantuje nam łatwiejszą implementację funkcjonalności (w większości) ale utrudnia utrzymanie kodu w dobrym stanie (testy).

Podejście Library:

Jak sama nazwa mówi, wykorzystywane jest przy tworzeniu “mniejszych” bibliotek. W gatunku gier możemy znaleźć np.:

  • SDL
  • PhysX
  • OpenGL
  • DirectX

Od razu możemy zauważyć różnicę w stosunku do produktów klasy “Engine”. Możemy powiedzieć:

Moja gra używa OpenGL

Raczej rzadko spotkamy się, że ktoś powie:

Moja gra jest napisana w OpenGL

Biblioteki zwykle są dedykowane konkretnej sferze funkcjonalności jak na przykład PhysX – fizyka. Mimo to jest również spora grupa bibliotek które udostępniają szerszą gamę udogodnień. Dobrym przykładem jest SDL:

SDL manages video, audio, input devices, CD-ROM, threads, shared object loading, networking and timers.

Całkiem pokaźny zestaw, można spokojnie na nim samym napisać sporą grę (co dużo ludzi robi). Taki prawie silnik, tylko czemu prawie? A no bo podeście do architektury API jest zupełnie inne.
Przykład wzięty z pierwszego lepszego tutoriala dla SDL:

int main( int argc, char* args[] )
{
	//The window we'll be rendering to
	SDL_Window* window = NULL;
	
	//The surface contained by the window
	SDL_Surface* screenSurface = NULL;

	//Initialize SDL
	if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
	{
		printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() );
	}
	else
	{
		//Create window
		window = SDL_CreateWindow( "SDL Tutorial", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
		if( window == NULL )
		{
			printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() );
		}
		else
		{
			//Get window surface
			screenSurface = SDL_GetWindowSurface( window );

			//Fill the surface white
			SDL_FillRect( screenSurface, NULL, SDL_MapRGB( screenSurface->format, 0xFF, 0xFF, 0xFF ) );
			
			//Update the surface
			SDL_UpdateWindowSurface( window );

			//Wait two seconds
			SDL_Delay( 2000 );
		}
	}

	//Destroy window
	SDL_DestroyWindow( window );

	//Quit SDL subsystems
	SDL_Quit();

	return 0;
}

Widać różnicę?
Klient jest tutaj w posiadaniu “władzy” nad końcową aplikacją – nie jest częścią silnika.

A czy to lepiej? Jak zawsze w programowaniu – zależy.

Podeście “biblioteczne” daje nam pełną swobodę w projektowaniu aplikacji.
Umiejętnie zarządzając warstwami w kodzie możemy w dużym stopniu uniezależnić się od wybranej biblioteki. Powoduje to iż testowanie staje się łatwiejsze a kod bardziej elastyczny.

Oczywiście wymaga to większego nakładu pracy przy implementowaniu konkretnych funkcjonalności. Samo odpalenie potencjalnej gry wymagało by sporej ilości kodu.

Quo vadis RL Engine?

Jak wspominałem w poprzednim poście na temat RL Engine, kierunkiem w który będziemy inwestować to styl “biblioteczny”.

Dlaczego?

Wersja biblioteczna jest o wiele bardziej przystosowana do developmentu chałupniczego, że to tak ujmę.

Kto by chciał korzystać z silnika w którym jest tylko rendering albo tylko path finding? – raczej mało osób.
A kto by skorzystał z biblioteki oferującej sam algorytm shadowcastingu? Myślę, że przynajmniej kilka.

Oczywistym jest, że silnik nie da się stworzyć w małym okresie czasu. Aby miało to jakiś sens musi on obsługiwać kilkadziesiąt różnych rzeczy (rendering, input, audio, itp.). A żeby napisać cokolwiek dobrego trzeba mieć dobry feedback. Dlatego siadanie do pisania silnika ot tak, bez innego powodu poza nim samym, jest raczej spisane na porażkę. W większości przypadków wynikiem będzie albo silnik który nie jest odpowiednio przystosowany do wymagań stawianych przed nim albo copy-cat innego silnika.

Z biblioteką jest zupełnie inaczej. Już od pierwszej funkcjonalności możemy znaleźć użytkowników którzy będą w stanie ją sensownie wykorzystać. Jeżeli dany feature nie będzie się nadawał, zawsze możemy go gruntownie przeprojektować. W przypadku silnika nie jest już tak fajnie.

Nie pozostało więc nic innego jak zacząć w końcu pisać ten silnik/bibliotekę.
No, może tylko wybór pierwszej funkcjonalności 🙂

  1. Żadnych frameworków, baz danych czy innych zbędnych pierdół.
  2. Czy w inny sposób podporządkowuje się wymaganiom konkretnego systemu.

Be First to Comment

A penny for your thoughts