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ż naciska Shift
. Jeśli nacisnął tylko WASD
wtedy, dzwonimy, _currentPlayerState.Walk();
kiedy to się dzieje i musimy currentPlayerState
być równi, WalkState
więc WalkState.Walk()
mamy wszystkie elementy potrzebne do tego stanu - w tym przypadku MovementSystem
, więc poruszamy gracza public 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 przypadku Run()
zostanie wywołany InputController
(nie mieszaj tego, Run()
jest wywoływany, ponieważ mamy WASD
+ Shift
odprawy InputController
nie z powodu WalkState
). Kiedy wzywamy _currentPlayerState.Run();
w WalkState
- wiemy, że mamy do przełącznika _currentPlayerState
do RunState
i czynimy to w Run()
od WalkState
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, kiedy E
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 naciska WASD
? - _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 na IDLEState
.
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 je virtual
tak, aby można override
je było w class 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()
, ale Building
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 ma TakeDamage()
.
class LivinEntity : IDamagable {
private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.
public void TakeDamage() {
....
}
}
class GameActor : LivingEntity, IGameActor {
// Here goes state machine and other attached components needed.
}
class Player : GameActor {
// Inventory, Building, Crafting.... components.
}
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.