Jak uniknąć twardego kodowania w silnikach gier

22

Moje pytanie nie jest pytaniem kodującym; dotyczy to ogólnie całego silnika gry.

Jak uniknąć twardego kodowania?

To pytanie jest o wiele głębsze niż się wydaje. Powiedz, jeśli chcesz uruchomić grę, która ładuje pliki niezbędne do działania, w jaki sposób możesz uniknąć mówienia czegoś takiego jak load specificfile.wadw kodzie silnika? Ponadto, kiedy ładujesz plik, jak możesz tego uniknąć load aspecificmap in specificfile.wad?

To pytanie dotyczy prawie całej konstrukcji silnika i jak najmniej silnika powinno być zakodowane na stałe. Jaki jest najlepszy sposób na osiągnięcie tego?

Marcus Cramer
źródło

Odpowiedzi:

42

Kodowanie oparte na danych

Każda rzecz, o której wspominasz, jest czymś, co można określić w danych. Dlaczego ładujesz aspecificmap? Ponieważ konfiguracja gry mówi, że jest to pierwszy poziom, gdy gracz rozpoczyna nową grę, lub ponieważ jest to nazwa bieżącego punktu zapisu w właśnie zapisanym pliku zapisu gracza itp.

Jak znaleźć aspecificmap? Ponieważ znajduje się w pliku danych, który zawiera identyfikatory map i ich zasoby na dysku.

Potrzebny jest tylko szczególnie mały zestaw „podstawowych” zasobów, które są prawnie trudne lub niemożliwe do uniknięcia na stałe. Przy odrobinie pracy można to ograniczyć do pojedynczej zakodowanej domyślnej nazwy zasobu, takiej jak main.wadlub podobnej. Ten plik można potencjalnie zmienić w czasie wykonywania, przekazując do gry argument wiersza polecenia, alias game.exe -wad mymain.wad.

Pisanie kodu opartego na danych opiera się na kilku innych zasadach. Na przykład można uniknąć sytuacji, w której systemy lub moduły proszą o określony zasób, i zamiast tego odwrócić te zależności. Oznacza to, że nie należy DebugDrawerładować debug.fontkodu inicjującego; zamiast tego DebugDrawerweź uchwyt zasobu w jego kodzie inicjującym. Ten uchwyt może zostać załadowany z głównego pliku konfiguracyjnego gry.

Jako konkretne przykłady z naszej bazy kodu mamy obiekt „danych globalnych”, który jest ładowany z bazy danych zasobów (która sama jest domyślnie ./resourcesfolderem, ale może być przeciążona argumentem wiersza poleceń). Identyfikator bazy danych zasobów tych globalnych danych jest jedyną koniecznie zakodowaną nazwą zasobu w bazie kodu (mamy inne, ponieważ czasami programiści stają się leniwi, ale ostatecznie ostatecznie je naprawiamy / usuwamy). Ten globalny obiekt danych jest pełen komponentów, których jedynym celem jest dostarczenie danych konfiguracyjnych. Jednym ze składników jest składnik danych globalnych interfejsu użytkownika, który zawiera uchwyty zasobów dla wszystkich głównych zasobów interfejsu użytkownika (czcionki, pliki Flash, ikony, dane lokalizacji itp.) Wśród wielu innych elementów konfiguracji. Gdy programista interfejsu użytkownika decyduje się zmienić nazwę głównego zasobu interfejsu użytkownika z /ui/mainmenu.swfna/ui/lobby.swfpo prostu aktualizują to globalne odniesienie danych; kod silnika nie musi się w ogóle zmieniać.

Używamy tych globalnych danych do wszystkiego. Wszystkie grywalne postacie, wszystkie poziomy, interfejs użytkownika, audio, podstawowe zasoby, konfiguracja sieci, wszystko. (cóż, nie wszystko , ale te inne rzeczy wymagają naprawy.)

To podejście ma wiele innych zalet. Po pierwsze, sprawia, że ​​pakowanie i wiązanie zasobów jest integralną częścią całego procesu. Ścieżki kodowania w silniku również zwykle oznaczają, że te same ścieżki muszą być zakodowane na sztywno w dowolnych skryptach lub narzędziach pakujących zasoby gry, a ścieżki te mogą się potem zsynchronizować. Bazując na pojedynczym kluczowym zasobie i łańcuchach referencyjnych stamtąd, możemy zbudować pakiet zasobów za pomocą jednego polecenia podobnego bundle.exe -root config.data -out main.wadi wiedzącego, że obejmie on wszystkie potrzebne zasoby. Co więcej, ponieważ producent pakietów po prostu śledzi odniesienia do zasobów, wiemy, że obejmie on tylko potrzebne zasoby i pominie cały pozostały puch, który nieuchronnie gromadzi się przez cały czas trwania projektu (a ponadto możemy automatycznie generować listy tego puch do przycinania).

Trudny przypadek tego wszystkiego jest w skryptach. Stworzenie silnika opartego na danych jest łatwe koncepcyjnie, ale widziałem tak wiele projektów (od hobby do AAA), w których skrypty są uważane za dane i dlatego „wolno” po prostu używać ścieżek zasobów bez rozróżnienia. Nie rób tego Jeśli plik Lua potrzebuje zasobu i po prostu wywołuje funkcję taką jak textures.lua("/path/to/texture.png")wtedy, potok zasobów będzie miał wiele problemów, wiedząc, że skrypt wymaga /path/to/texture.pngpoprawnego działania i może uznać tę teksturę za nieużywaną i niepotrzebną. Skrypty należy traktować jak każdy inny kod: wszelkie potrzebne dane, w tym zasoby lub tabele, powinny być określone we wpisie konfiguracji, który silnik i potok zasobów mogą sprawdzać pod kątem zależności. Dane, które mówią „ładuj skrypt foo.lua” powinny zamiast tego powiedzieć „foo.luai podaj mu te parametry ", gdy parametry obejmują wszelkie niezbędne zasoby. Jeśli skrypt na przykład losowo odradza wrogów, przekaż listę możliwych wrogów do skryptu z tego pliku konfiguracyjnego. Silnik może następnie wstępnie załadować wrogów poziomem ( ponieważ zna pełną listę możliwych odrodzeń), a potok zasobów wie, że łączy wszystkich wrogów z grą (ponieważ są one definitywnie określone w danych konfiguracyjnych). Jeśli skrypty generują ciągi nazw ścieżek i po prostu wywołują loadfunkcję, wówczas żadne silnik ani potok zasobów nie mają żadnego sposobu na określenie, które zasoby skrypt może załadować.

Sean Middleditch
źródło
Dobra odpowiedź, bardzo praktyczna, a także wyjaśnia pułapki i błędy, które ludzie popełniają przy wdrażaniu tego! +1
kiedy
+1. Dodałbym, że podążanie za wzorcem wskazywania na zasoby zawierające dane konfiguracji jest również bardzo pomocne, jeśli chcesz włączyć modowanie. O wiele trudniej i bardziej ryzykownie jest modyfikować gry, które wymagają zmiany oryginalnych plików danych zamiast tworzenia własnych i wskazywania na nie. Jeszcze lepiej, jeśli możesz wskazać wiele plików o określonej kolejności priorytetów.
Jeutnarg,
12

W ten sam sposób unikasz twardego kodowania w funkcjach ogólnych.

Podajesz parametry i przechowujesz informacje w plikach konfiguracyjnych.

W tej sytuacji nie ma absolutnie żadnej różnicy w inżynierii oprogramowania między pisaniem silnika a pisaniem klasy.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

Następnie kod klienta odczytuje „główny” plik konfiguracyjny ( ten jest albo zakodowany na stałe, albo przekazany jako argument wiersza poleceń), który zawiera informacje określające, gdzie znajdują się pliki zasobów i jaką mapę zawierają.

Stamtąd wszystko sterowane jest przez plik konfiguracyjny „master”.

Vaillancourt
źródło
1
Tak, to plus jakiś mechanizm wprowadzający niestandardową logikę. Może to być osadzenie języka takiego jak C #, python itp. W celu rozszerzenia podstawowych funkcji silnika o funkcje zdefiniowane przez użytkownika
qCring
3

Lubię inne odpowiedzi, więc będę trochę przeciwny. ;)

Nie można uniknąć kodowania wiedzy o swoich danych w silniku. Bez względu na to, skąd pochodzą informacje, silnik musi wiedzieć, aby ich szukać. Można jednak uniknąć zakodowania samych informacji w silniku.

Podejście oparte na „czystych” danych wymagałoby uruchomienia pliku wykonywalnego z parametrami wiersza poleceń niezbędnymi do załadowania początkowej konfiguracji, ale silnik musi zostać zakodowany, aby wiedzieć, jak interpretować te informacje. Np jeśli pliki konfiguracyjne są JSON, trzeba ciężko kodem zmienne szukać np silnik będzie musiał wiedzieć, aby szukać "intro_movies"i "level_list"itd.

Jednak „dobrze skonstruowany” silnik może działać w wielu różnych grach, po prostu zamieniając dane konfiguracyjne i dane, do których się odwołuje.

Tak więc mantra nie tyle polega na unikaniu twardego kodowania, ile na zapewnianiu, że możesz wprowadzać zmiany przy jak najmniejszym wysiłku.

W przeciwieństwie do podejścia do plików danych (które z całego serca popieram), być może kompilowanie danych w silniku jest w porządku. Jeśli „koszt” zrobienia tego jest niższy, wówczas nie ma prawdziwej szkody; jeśli tylko nad tym pracujesz, możesz odłożyć obsługę plików na później i niekoniecznie się pieprzyć. W moich pierwszych kilku projektach gier zapisano duże tabele danych w samej grze, np. Listę broni i ich różnorodne dane:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

Umieszczasz więc te dane w łatwym miejscu i możesz je łatwo edytować w razie potrzeby. Idealnym rozwiązaniem byłoby umieszczenie tych rzeczy w jakimś pliku konfiguracyjnym, ale wtedy trzeba wykonać parsowanie i tłumaczenie oraz cały ten jazz, a także podłączanie referencji między strukturami może stać się dodatkowym problemem, którego tak naprawdę nie chcesz radzić sobie z.

dash-tom-bang
źródło
Parsowanie Jsona nie jest strasznie trudne. Jedynym związanym z tym „kosztem” jest nauka. (W szczególności uczenie się korzystania z odpowiedniego modułu lub biblioteki. Go ma na przykład dobre wsparcie Json.)
Wildcard,
To nie jest strasznie trudne, ale wymaga zrobienia czegoś poza nauką. Np. Wiem, jak technicznie parsować JSON, napisałem parsery dla wielu innych formatów plików, ale musiałbym albo znaleźć i zainstalować rozwiązanie innej firmy (i dowiedzieć się o zależnościach i jak je zbudować), albo uruchomić własne. Zajmuje więcej czasu niż nie robienie tego.
dash-tom-bang
4
Wszystko zajmuje więcej czasu niż nie robienie tego. Ale potrzebne narzędzia zostały już napisane. Tak jak nie musisz projektować kompilatora do pisania gry lub majstrować przy kodzie maszynowym, ale musisz nauczyć się języka dla platformy, z którą pracujesz. Naucz się także używać parsera json.
Wildcard,
Nie jestem pewien, jaki jest twój argument. W tej odpowiedzi opowiadam się za YAGNI; jeśli nie musisz tracić czasu na robienie czegoś, co ci nie pomoże, to nie rób tego. Jeśli chcesz spędzić na tym czas, to świetnie. Być może będziesz musiał poświęcić czas później, może nie, ale robienie tego z góry odciąga cię od zadania polegającego na stworzeniu gry. Tworzenie gier jest banalne; każde zadanie związane z tworzeniem gry jest proste. Po prostu większość gier ma milion prostych zadań, a odpowiedzialny twórca wybiera te, które najszybciej osiągną ten cel.
dash-tom-bang
2
Właściwie głosowałem za odpowiedzią; nie ma prawdziwych argumentów jako takich. Chciałem tylko zauważyć, że JSON nie jest trudny do przeanalizowania. Czytając ponownie, chyba reagowałem głównie na fragment „ale potem trzeba parsować i tłumaczyć i cały ten jazz”. Ale zgadzam się, że w przypadku osobistych gier projektowych i podobnych YAGNI. :)
Wildcard,