Oddzielanie silnika gry od kodu gry w podobnych grach z wersjonowaniem

15

Mam ukończoną grę, którą chcę odrzucić w innych wersjach. Byłyby to podobne gry, z mniej więcej tym samym wyglądem, ale nie zawsze w zasadzie rzeczy mogą się zmieniać, czasem małe, czasem duże.

Chciałbym, aby kod podstawowy był wersjonowany oddzielnie od gry, więc jeśli powiesz, że naprawię błąd znaleziony w grze A, poprawka będzie obecna w grze B.

Próbuję znaleźć najlepszy sposób, aby sobie z tym poradzić. Moje początkowe pomysły są następujące:

  • Utwórz enginemoduł / folder / cokolwiek, który zawiera wszystko, co można uogólnić i jest w 100% niezależny od reszty gry. Obejmuje to część kodu, ale także ogólne zasoby, które są wspólne dla gier.
  • Umieść ten silnik we własnym gitrepozytorium, które zostanie uwzględnione w grach jakogit submodule

Część, z którą mam problem, to sposób zarządzania resztą kodu. Załóżmy, że masz scenę menu, ten kod jest specyficzny dla gry, ale także większość z nich ma charakter ogólny i może być ponownie wykorzystana w innych grach. Nie mogę tego włożyć engine, ale przekodowanie go dla każdej gry byłoby nieefektywne.

Być może użycie jakiegoś wariantu gałęzi git może być skuteczne w zarządzaniu tym, ale nie sądzę, że jest to najlepsza droga.

Czy ktoś ma jakieś pomysły, doświadczenia do podzielenia się lub coś na ten temat?

Malharhak
źródło
W jakim języku jest twój silnik? Niektóre języki mają dedykowane menedżery pakietów, które mogą mieć większy sens niż używanie podmodułów git. Na przykład NodeJS ma npm (który może atakować repozytorium Git jako źródła).
Dan Pantry
Czy masz pytanie o to, jak najlepiej zarządzać konfiguracją kodu generycznego, czy jak zarządzać kodem „pół-generycznym”, czy jak zaprojektować kod, jak zaprojektować kod czy co?
Dunk
Może się to różnić w zależności od środowiska programowania, ale możesz wziąć pod uwagę nie tylko oprogramowanie sterujące wersją, ale także wiedzieć, jak oddzielić silnik gry od kodu gry (np. Pakietów, folderów i interfejsu API) i później , zastosuj wersję kontrolną.
umlcat,
Jak mieć czystą historię jednego folderu w jednym oddziale: Przebuduj silnik tak, aby oddzielne (przyszłe) repozytoria znajdowały się w osobnych folderach, to jest twoje ostatnie zatwierdzenie. Następnie utwórz nową gałąź, usuń wszystko poza tym folderem i zatwierdź. Następnie przejdź do pierwszego zatwierdzenia repozytorium i połącz je z nowym oddziałem. Masz teraz oddział z tylko tym folderem: ściągnij go z innych projektów i / lub połącz z powrotem z istniejącym projektem. Bardzo mi to pomogło w rozdzielaniu silników w gałęziach, jeśli Twój kod jest już rozdzielony. Nie potrzebuję modułów git.
Barry Staes,

Odpowiedzi:

13

Utwórz moduł silnika / folder / cokolwiek, który zawiera wszystko, co można uogólnić i jest w 100% niezależny od reszty gry. Obejmuje to część kodu, ale także ogólne zasoby, które są wspólne dla gier.

Umieść ten silnik we własnym repozytorium git, które będzie zawarte w grach jako podmoduł git

Właśnie to robię i działa bardzo dobrze. Mam strukturę aplikacji i bibliotekę renderującą, a każda z nich jest traktowana jako podmoduły moich projektów. Uważam, że SourceTree jest użyteczny, jeśli chodzi o podmoduły, ponieważ dobrze nimi zarządza i nie pozwoli ci niczego zapomnieć, np. Jeśli zaktualizujesz podmoduł silnika w projekcie A, powiadomi cię on o wycofaniu zmian w projekcie B.

Z doświadczeniem przychodzi wiedza na temat tego, jaki kod powinien być w silniku, a co na projekt. Sugeruję, że jeśli jesteś choć trochę niepewny, na razie zatrzymaj go w każdym projekcie. Z biegiem czasu zobaczysz wśród różnych projektów, co pozostaje niezmienne, a następnie możesz stopniowo uwzględniać to w kodzie silnika. Innymi słowy: zduplikuj kod, dopóki nie osiągniesz 100% pewności, że nie zmienia się on dyskretnie dla każdego projektu, a następnie uogólnij go.

Uwaga na temat kontroli źródła i plików binarnych

Pamiętaj tylko, że jeśli oczekujesz częstych zmian zasobów binarnych, możesz nie chcieć umieszczać ich w kontroli tekstu opartej na źródłach, takiej jak git. Mówiąc tylko ... istnieją lepsze rozwiązania dla plików binarnych. Najprostszą rzeczą, jaką możesz teraz zrobić, aby utrzymać czyste i wydajne repozytorium „silnik-źródło”, jest posiadanie oddzielnego repozytorium „binaria silnika”, które zawiera tylko pliki binarne, które również włączasz jako podmoduł do swojego projektu. W ten sposób zmniejszasz szkody wyrządzone w repozytorium „silnik-źródło”, które cały czas się zmienia i w związku z czym potrzebujesz szybkich iteracji: zatwierdzanie, wypychanie, wyciąganie itp. Systemy zarządzania kontrolą źródła, takie jak git, działają na delcie tekstowej , a po wprowadzeniu plików binarnych wprowadzasz ogromne delty z perspektywy tekstowej - co ostatecznie kosztuje Twój czas.Załącznik do GitLab . Google jest twoim przyjacielem.

Inżynier
źródło
Tak naprawdę nie zmieniają się często, ale interesuje mnie to. Nic nie wiem o wersji binarnej. Jakie są rozwiązania?
Malharhak,
@Malharhak Edytowane, aby odpowiedzieć na twój komentarz.
Inżynier
@Malharak Oto trochę informacji na ten temat.
Inżynier
1
+1 za utrzymanie rzeczy w projekcie tak długo, jak to możliwe. Wspólny kod zapewnia większą złożoność. Należy tego unikać, dopóki nie będzie to absolutnie wymagane.
Gusdor,
1
@Malharhak Nie, szczególnie, że Twoim celem jest przechowywanie „kopii” do czasu, aż zauważysz, że wspomniany kod jest niezmienny i można go traktować jako powszechny. Gusdor powtórzył to - ostrzegamy - można łatwo zmarnować mnóstwo czasu, przedwcześnie dzieląc się rzeczami, a następnie próbując znaleźć sposoby na utrzymanie tego kodu na tyle ogólnego, aby pozostawał wspólny, ale wystarczająco elastycznego, aby pasował do różnych projektów ... dużo parametrów i przełączników i okazuje się, że brzydki bałaganu jeszcze nie to, co trzeba, bo możesz skończyć zmieniając go za nowego projektu w każdym razie . Nie uwzględniaj zbyt wcześnie . Mieć cierpliwość.
Inżynier
6

W pewnym momencie silnik MUSI specjalizować się i wiedzieć coś o grze. Pójdę tutaj na styczną.

Weź zasoby w RTS. Jedna gra może mieć, Creditsa Crystaldruga MetaliPotatoes

Powinieneś właściwie używać koncepcji OO i sięgać po maks. ponowne użycie kodu. Oczywiste jest, że Resourceistnieje tutaj koncepcja istnienia.

Dlatego decydujemy, że zasoby mają następujące elementy:

  1. Zaczep w głównej pętli do samodzielnego zwiększania / zmniejszania
  2. Sposób na uzyskanie bieżącej kwoty (zwraca an int)
  3. Sposób na dowolne odejmowanie / dodawanie (gracze przenoszący zasoby, zakupy ...)

Zauważ, że to pojęcie Resourcemoże oznaczać zabójstwa lub punkty w grze! To nie jest bardzo potężne.

Teraz pomyślmy o grze. Możemy w pewnym sensie mieć walutę, handlując groszami i dodając przecinek dziesiętny do wyniku. Nie możemy zrobić „natychmiastowych” zasobów. Jak powiedz „generowanie sieci energetycznej”

Powiedzmy, że dodajesz InstantResourceklasę za pomocą podobnych metod. Teraz (zaczynasz) zanieczyszczać silnik zasobami.


Problem

Weźmy jeszcze raz przykład RTS. Załóżmy, że gracz cokolwiek przekazuje część Crystalinnemu graczowi. Chcesz zrobić coś takiego:

if(transfer.target == engine.getPlayerId()) {
    engine.hud.addIncoming("You got "+transfer.quantity+" of "+
        engine.resourceDictionary.getNameOf(transfer.resourceId)+
        " from "+engine.getPlayer(transfer.source).name);
}
engine.getPlayer(transfer.target).getResourceById(transfer.resourceId).add(transfer.quantity)
engine.getPlayer(transfer.source).getResourceById(transfer.resourceId).add(-transfer.quantity)

Jest to jednak naprawdę dość niechlujny. To ogólny cel, ale bałagan. Już narzuca to, resourceDictionaryco oznacza, że ​​teraz twoje zasoby muszą mieć nazwy! I to jest na gracza, więc nie możesz już mieć zasobów zespołu.

To jest „za dużo” abstrakcji (nie jest to świetny przykład, przyznaję), zamiast tego powinieneś osiągnąć punkt, w którym akceptujesz, że twoja gra ma graczy i kryształ, wtedy możesz po prostu mieć (na przykład)

engine.getPlayer(transfer.target).crystal().receiveDonation(transfer)
engine.getPlayer(transfer.source).crystal().sendDonation(transfer)

Z klasą Playeri klasą CurrentPlayer, gdzie CurrentPlayer„s crystalobiektu automatycznie pokaże rzeczy na HUD dla przeniesienia / wysyłania datków.

To zanieczyszcza silnik kryształem, przekazuje kryształ, komunikaty na interfejsie dla obecnych graczy i tak dalej. Jest zarówno szybszy, jak i łatwiejszy do odczytu / zapisu / konserwacji (co jest ważniejsze, ponieważ nie jest znacznie szybszy)


Uwagi końcowe

Przypadek zasobów nie jest genialny. Mam jednak nadzieję, że nadal rozumiesz o co chodzi. Jeśli cokolwiek wykazałem, że „zasoby nie należą do silnika” jako to, czego potrzebuje konkretna gra i co dotyczy wszystkich pojęć zasobów, to BARDZO różne rzeczy. Zazwyczaj znajdziesz 3 (lub 4) „warstwy”

  1. „Rdzeń” - jest to podręcznikowa definicja silnika, jest to scena z hakami zdarzeń, dotyczy shaderów i pakietów sieciowych oraz abstrakcyjnego pojęcia graczy
  2. „GameCore” - jest to dość ogólny rodzaj gry, ale nie wszystkie gry - na przykład zasoby w RTS lub amunicja w FPS. Tutaj zaczyna się logika gry. To tutaj byłoby nasze wcześniejsze pojęcie zasobów. Dodaliśmy te rzeczy, które mają sens w przypadku większości zasobów RTS.
  3. „GameLogic” BARDZO specyficzny dla aktualnie tworzonej gry. Znajdziesz zmienne o nazwach takich jak creaturelub shiplub squad. Korzystając z dziedziczenia otrzymasz klasy, które obejmują wszystkie 3 warstwy (na przykład Crystal jest to, Resource co jest GameLoopEventListener powiedzmy)
  4. „Zasoby” są bezużyteczne w żadnej innej grze. Weźmy na przykład skrypty AI łączące okres półtrwania 2, nie będą one używane w RTS z tym samym silnikiem.

Tworzenie nowej gry ze starego silnika

Jest to BARDZO powszechne. Faza 1 polega na rozerwaniu warstw 3 i 4 (i 2, jeśli gra jest CAŁKOWICIE innego typu) Załóżmy, że tworzymy RTS ze starego RTS. Wciąż mamy zasoby, po prostu nie kryształy i inne rzeczy - więc klasy podstawowe w warstwach 2 i 1 nadal mają sens, wszystkie kryształy wymienione w 3 i 4 można odrzucić. Tak robimy. Możemy jednak to sprawdzić jako odniesienie do tego, co chcemy zrobić.


Zanieczyszczenia w warstwie 1

To może się zdarzyć. Abstrakcja i wydajność są wrogami. Na przykład UE4 zapewnia wiele zoptymalizowanych przypadków komponowania (więc jeśli chcesz X i Y, ktoś napisał kod, który X i Y naprawdę razem bardzo szybko - wie, że robi oba) i w rezultacie NAPRAWDĘ jest dość duży. To nie jest złe, ale jest czasochłonne. Warstwa 1 decyduje o tym, jak „przekazać dane do shaderów” i jak animować. Robienie tego w najlepszy sposób dla twojego projektu jest ZAWSZE dobre. Po prostu spróbuj zaplanować przyszłość, ponowne użycie kodu jest twoim przyjacielem, dziedzicz tam, gdzie ma to sens.


Klasy klasyfikacyjne

OSTATNIE (obiecuję) nie bój się zbyt wielu warstw. Silnik jest archaicznym terminem z dawnych czasów rurociągów o stałej funkcji, w których silniki działały prawie tak samo graficznie (i w rezultacie miały wiele wspólnego), programowalny rurociąg odwrócił to do góry nogami i jako taka „warstwa 1” została zanieczyszczona niezależnie od efektów, jakie chcieli osiągnąć programiści. AI była cechą wyróżniającą (ze względu na niezliczoną liczbę podejść) silników, teraz jest to AI i grafika.

Twój kod nie powinien być zapisywany w tych warstwach. Nawet słynny silnik Unreal ma WIELE różnych wersji, z których każda jest specyficzna dla innej gry. Istnieje kilka plików (może innych niż struktury danych), które pozostałyby niezmienione. Jest okej! Jeśli chcesz stworzyć nową grę z innej, zajmie to więcej niż 30 minut. Kluczem jest zaplanowanie, wiedzieć, jakie bity skopiować i wkleić, a co zostawić.

Alec Teal
źródło
1

Moją osobistą sugestią, jak radzić sobie z treścią łączącą ogólne i specyficzne, jest uczynienie jej dynamicznym. Jako przykład wezmę ekran menu. Jeśli źle zrozumiałem, o co prosiłeś, daj mi znać, o co chciałeś wiedzieć, a ja dostosuję moją odpowiedź.

Istnieją 3 rzeczy, które są (prawie) zawsze obecne na scenie menu: tło, logo gry i samo menu. Te rzeczy są zwykle różne w zależności od gry. Możesz zrobić dla tej zawartości MenuScreenGenerator w swoim silniku, który przyjmuje 3 parametry obiektu: BackGround, Logo i Menu. Podstawowa struktura tych 3 części jest również częścią twojego silnika, ale twój silnik nie mówi w jaki sposób te części są generowane, tylko jakie parametry powinieneś im nadać.

Następnie w swoim prawdziwym kodzie gry tworzysz obiekty dla BackGround, logo i menu, i przekazujesz to swojemu MenuScreenGenerator. Ponownie, sama gra nie obsługuje generowania menu, to jest dla silnika. Twoja gra musi tylko powiedzieć silnikowi, jak powinien on wyglądać i gdzie powinien być.

Zasadniczo twój silnik powinien być interfejsem API, który gra mówi, co wyświetlać. Jeśli zostanie to wykonane prawidłowo, silnik powinien wykonać ciężką pracę, a sama gra powinna tylko powiedzieć silnikowi, jakie zasoby należy użyć, jakie działania podjąć i jak wygląda świat.

Nzall
źródło