W grze prawie zawsze występuje klasa gracza. Gracz może na ogół wiele zrobić w grze, co oznacza, że dla mnie ta klasa jest ogromna z mnóstwem zmiennych, które obsługują każdą funkcję, jaką może wykonać gracz. Każdy kawałek jest sam w sobie dość niewielki, ale w połączeniu z nim powstają tysiące linii kodu i trudno jest znaleźć to, czego potrzebujesz, i straszne jest wprowadzanie zmian. Z czymś, co jest w zasadzie ogólną kontrolą dla całej gry, jak uniknąć tego problemu?
architecture
użytkownik441521
źródło
źródło
Odpowiedzi:
Zwykle używasz systemu komponentu encji (system komponentu encji to architektura oparta na komponentach). Ułatwia to także tworzenie innych bytów i może sprawić, że wrogowie / NPC mają te same elementy, co gracz.
To podejście idzie w dokładnie odwrotnym kierunku niż podejście zorientowane obiektowo. Wszystko w grze jest bytem. Istota jest tylko przypadkiem bez wbudowanej mechaniki gry. Ma listę składników i sposób manipulowania nimi.
Na przykład odtwarzacz ma komponent pozycji, komponent animacji i komponent wejściowy, a gdy użytkownik naciska spację, chcesz, aby odtwarzacz skakał.
Możesz to osiągnąć przez nadanie graczowi elementu skoku, który po wywołaniu powoduje zmianę komponentu animatiom na animację skoku i sprawia, że gracz ma dodatnią prędkość y w składniku pozycji. W komponencie wejściowym nasłuchujesz klawisza spacji i wywołujesz komponent skoku. (To tylko przykład, powinieneś mieć komponent kontrolera do ruchu).
Pomaga to podzielić kod na mniejsze moduły wielokrotnego użytku i może doprowadzić do bardziej zorganizowanego projektu.
źródło
Gry nie są w tym wyjątkowe; Boskie klasy są wszędzie anty-wzorcem.
Częstym rozwiązaniem jest rozbicie dużej klasy na drzewo mniejszych klas. Jeśli gracz ma ekwipunek, nie włączaj zarządzania ekwipunkiem
class Player
. Zamiast tego utwórz plikclass Inventory
. Jest to jeden element członkowskiclass Player
, ale wewnętrznieclass Inventory
może owinąć dużo kodu.Kolejny przykład: postać gracza może mieć relacje z postaciami niezależnymi, więc możesz mieć
class Relation
odniesienie zarówno doPlayer
obiektu, jak i doNPC
obiektu, ale nie należy do żadnego.źródło
1) Gracz: architektura stan-maszyna + architektura oparta na komponentach.
Typowe komponenty odtwarzacza: HealthSystem, MovementSystem, InventorySystem, ActionSystem. To są wszystkie klasy
class HealthSystem
.Nie polecam go
Update()
tam używać (zwykle nie ma sensu aktualizować systemu opieki zdrowotnej, chyba że potrzebujesz go do niektórych akcji w każdej klatce, rzadko się zdarzają. Jeden przypadek, o którym możesz pomyśleć - gracz zostaje otruty i potrzebujesz go od czasu do czasu tracić zdrowie - tutaj sugeruję używanie koroutyn. Kolejnym, który stale regeneruje zdrowie lub moc biegania, po prostu bierzesz bieżące zdrowie lub moc i wywołujesz coroutine, aby wypełnić ten poziom, gdy przyjdzie czas. Przełam koroutynę, gdy zdrowie jest pełne lub był uszkodzony lub zaczął biec ponownie itd. OK, to było trochę nie na temat, ale mam nadzieję, że się przydało) .Stany: LootState, RunState, WalkState, AttackState, IDLEState.
Każdy stan dziedziczy po
interface IState
.IState
ma w naszym przypadku 4 metody tylko dla przykładu.Loot() Run() Walk() Attack()
Ponadto mamy miejsce, w
class InputController
którym sprawdzamy każde wejście użytkownika.Teraz prawdziwy przykład:
InputController
sprawdzamy, czy gracz naciska którykolwiek z nich,WASD or arrows
a następnie czy on również naciskaShift
. Jeśli nacisnął tylkoWASD
wtedy, dzwonimy,_currentPlayerState.Walk();
kiedy to się dzieje i musimycurrentPlayerState
być równi,WalkState
więcWalkState.Walk()
mamy wszystkie elementy potrzebne do tego stanu - w tym przypadkuMovementSystem
, więc poruszamy graczapublic void Walk() { _playerMovementSystem.Walk(); }
- widzisz, co tu mamy? Mamy drugą warstwę zachowania, która jest bardzo dobra do utrzymywania kodu i debugowania.Przejdźmy teraz do drugiego przypadku: co jeśli naciśniemy
WASD
+Shift
? Ale nasz poprzedni stan byłWalkState
. W tym przypadkuRun()
zostanie wywołanyInputController
(nie mieszaj tego,Run()
jest wywoływany, ponieważ mamyWASD
+Shift
odprawyInputController
nie z powoduWalkState
). Kiedy wzywamy_currentPlayerState.Run();
wWalkState
- wiemy, że mamy do przełącznika_currentPlayerState
doRunState
i czynimy to wRun()
odWalkState
i nazywają to znowu wewnątrz tej metody, ale teraz z innego stanu, ponieważ nie chcemy stracić działania tej ramki. I teraz oczywiście dzwonimy_playerMovementSystem.Run();
.Ale po co,
LootState
gdy gracz nie może chodzić ani biegać, dopóki nie zwolni przycisku? Cóż, w tym przypadku, kiedy zaczęliśmy grabież, na przykład, kiedyE
naciśniemy przycisk, wywołujemy_currentPlayerState.Loot();
, przełączamy się na,LootState
a teraz wywołujemy jego stamtąd. Tam na przykład wywołujemy metodę kolizji, aby uzyskać, jeśli jest coś do zdobycia w zasięgu. I nazywamy coroutine tam, gdzie mamy animację lub gdzie ją uruchamiamy, a także sprawdzamy, czy gracz nadal trzyma przycisk, jeśli nie coroutine się łamie, jeśli tak, dajemy mu łupy na końcu coroutine. Ale co jeśli gracz naciskaWASD
? -_currentPlayerState.Walk();
nazywa się, ale tutaj jest ładna cecha automatu stanów, wLootState.Walk()
mamy pustą metodę, która nic nie robi lub tak, jak zrobiłbym to jako funkcję - gracze mówią: „Hej, jeszcze tego nie zrabowałem, możesz poczekać?”. Kiedy skończy grabież, zmieniamy naIDLEState
.Można również wykonać inny skrypt, który jest wywoływany,
class BaseState : IState
który ma zaimplementowane wszystkie domyślne zachowanie metod, ale ma jevirtual
tak, aby możnaoverride
je było wclass LootState : BaseState
klasach.System oparty na komponentach jest świetny, jedyne, co mnie martwi, to Instancje, wiele z nich. I zajmuje więcej pamięci i pracy dla śmieciarza. Na przykład, jeśli masz 1000 instancji wroga. Wszystkie mają 4 elementy. 4000 obiektów zamiast 1000. Mb, to nie jest taka wielka sprawa (nie przeprowadzałem testów wydajności), jeśli weźmiemy pod uwagę wszystkie komponenty, które ma jedność gameobject.
2) Architektura oparta na dziedziczeniu. Chociaż zauważysz, że nie możemy całkowicie pozbyć się komponentów - jest to w rzeczywistości niemożliwe, jeśli chcemy mieć czysty i działający kod. Ponadto, jeśli chcemy użyć Wzorów projektowych, które są wysoce zalecane do użycia w odpowiednich przypadkach (nie nadużywaj ich również, nazywa się to nadprodukcją).
Wyobraź sobie, że mamy klasę Gracza, która ma wszystkie właściwości potrzebne do wyjścia z gry. Ma zdrowie, manę lub energię, może poruszać się, biegać i używać umiejętności, posiada ekwipunek, może wytwarzać przedmioty, łupić przedmioty, a nawet budować barykady lub wieżyczki.
Przede wszystkim powiem, że Ekwipunek, Wytwarzanie, Ruch, Budowanie powinny być oparte na komponentach, ponieważ gracz nie jest odpowiedzialny za takie metody
AddItemToInventoryArray()
- chociaż gracz może mieć taką metodę,PutItemToInventory()
która wywoła poprzednio opisaną metodę (2 warstwy - możemy dodaj niektóre warunki w zależności od różnych warstw).Kolejny przykład z budowaniem. Gracz może zadzwonić
OpenBuildingWindow()
, aleBuilding
zająłby się całą resztą, a kiedy użytkownik zdecyduje się zbudować jakiś konkretny budynek, przekazuje mu wszystkie potrzebne informacje,Build(BuildingInfo someBuildingInfo)
a gracz zaczyna budować ze wszystkimi potrzebnymi animacjami.SOLID - zasady OOP. S - jedna odpowiedzialność: to, co widzieliśmy w poprzednich przykładach. No dobrze, ale gdzie jest Dziedzictwo?
Tutaj: czy zdrowie i inne cechy gracza powinny być obsługiwane przez inny byt? Myślę, że nie. Nie może być gracza bez zdrowia, jeśli taki istnieje, po prostu nie dziedziczymy. Na przykład, mamy
IDamagable
,LivingEntity
,IGameActor
,GameActor
.IDamagable
oczywiście, że maTakeDamage()
.Więc tutaj nie mogłem właściwie oddzielić składników od dziedziczenia, ale możemy je mieszać, jak widzisz. Możemy również stworzyć klasy bazowe dla systemu Building, na przykład, jeśli mamy różne typy tego systemu i nie chcemy pisać więcej kodu niż to konieczne. Rzeczywiście możemy również mieć różne typy budynków i nie ma właściwie dobrego sposobu, aby zrobić to w oparciu o komponenty!
OrganicBuilding : Building
,TechBuilding : Building
. Nie musisz tworzyć 2 komponentów i pisać tam kodu dwukrotnie dla typowych operacji lub właściwości budynku. A następnie dodaj je inaczej, możesz użyć mocy dziedziczenia, a później polimorfizmu i inkapsulacji.Sugerowałbym użycie czegoś pomiędzy. I nie nadużywaj komponentów.
Bardzo polecam przeczytanie tej książki o wzorcach programowania gier - jest ona darmowa w WEB.
źródło
Ten problem nie ma srebrnej kuli, ale istnieją różne podejścia, z których prawie wszystkie oparte są na zasadzie „rozdzielenia obaw”. Inne odpowiedzi już omawiały popularne podejście oparte na komponentach, ale istnieją inne podejścia, które można zastosować zamiast lub razem z rozwiązaniem opartym na komponentach. Będę omawiać podejście podmiot-kontroler, ponieważ jest to jedno z moich preferowanych rozwiązań tego problemu.
Po pierwsze, sama idea
Player
klasy wprowadza w błąd. Wiele osób myśli o postaci gracza, postaciach NPC i potworach / wrogach jako o różnych klasach, podczas gdy w rzeczywistości wszystkie one mają ze sobą wiele wspólnego: wszystkie są narysowane na ekranie, wszystkie się poruszają, mogą wszystkie mają zapasy itp.Ten sposób myślenia prowadzi do podejścia, w którym postacie graczy, postacie inne niż potwory oraz potwory / wrogowie są traktowane
Entity
raczej jako „ s” niż traktowane inaczej. Oczywiście muszą jednak zachowywać się inaczej - postać gracza musi być kontrolowana przez wejście, a NPC potrzebują ai.Rozwiązaniem tego jest posiadanie
Controller
klas używanych do kontrolowaniaEntity
s. W ten sposób cała ciężka logika kończy się w kontrolerze, a wszystkie dane i cechy wspólne są przechowywane w jednostce.Ponadto poprzez podklasę
Controller
wInputController
iAIController
pozwala graczowi skutecznie kontrolować dowolneEntity
w pokoju. Takie podejście pomaga również w grze wieloosobowej, ponieważ posiada klasęRemoteController
lub,NetworkController
która działa za pomocą poleceń ze strumienia sieciowego.Może to spowodować, że wiele logiki zostanie przekształconych w jedną,
Controller
jeśli nie będziesz ostrożny. Sposobem na uniknięcie tego jest posiadanieController
s, które składają się z innychController
, lub uzależnienieController
funkcjonalności od różnych właściwościController
. Na przykład,AIController
miałbyDecisionTree
do niego dołączony, iPlayerCharacterController
mógłby składać się z różnych innychController
s, takich jak aMovementController
, aJumpController
(zawierający maszynę stanów ze stanami OnGround, Rosnąco i Malejąco), anInventoryUIController
. Dodatkową korzyścią tego jest to, że noweController
s mogą być dodawane w miarę dodawania nowych funkcji - jeśli gra rozpocznie się bez systemu ekwipunku i zostanie dodany jeden, kontroler może zostać dodany później.źródło