Ostatnio bawię się, programując prostą tekstową grę przygodową i utknąłem w czymś, co wydaje się być bardzo prostym problemem projektowym.
Podsumowując: gra jest podzielona na Room
obiekty. Każdy Room
ma listę Entity
obiektów znajdujących się w tym pokoju. Każdy z nich Entity
ma stan zdarzenia, który jest prostą mapą łańcuchową> logiczną oraz listę akcji, która jest mapą łańcuchową> funkcyjną.
Dane wprowadzane przez użytkownika mają formę [action] [entity]
. Room
Używa nazwy jednostki, aby powrócić odpowiedni Entity
obiekt, który następnie używa nazwy działania, aby znaleźć właściwą funkcję i wykonuje go.
Aby wygenerować opis pokoju, każdy Room
obiekt wyświetla własny ciąg opisu, a następnie dołącza ciąg opisu każdego Entity
. Entity
Opis może się zmieniać w zależności od jego stanu ( „Drzwi są otwarte”, „drzwi są zamknięte”, „Drzwi są zamknięte”, etc).
Oto problem: dzięki tej metodzie liczba funkcji opisu i działania, które muszę zaimplementować, szybko wymyka się spod kontroli. Sam mój pokój startowy ma około 20 funkcji między 5 podmiotami.
Mogę łączyć wszystkie akcje w jedną funkcję i przełączać je między nimi, ale nadal są to dwie funkcje na jednostkę. Mogę również tworzyć określone Entity
podklasy dla wspólnych / ogólnych obiektów, takich jak drzwi i klucze, ale do tej pory mnie to prowadzi.
EDYCJA 1: Zgodnie z żądaniem, pseudokodowe przykłady tych funkcji akcji.
string outsideDungeonBushesSearch(currentRoom, thisEntity, player)
if thisEntity["is_searched"] then
return "There was nothing more in the bushes."
else
thisEntity["is_searched"] := true
currentRoom.setEntity("dungeonDoorKey")
return "You found a key in the bushes."
end if
string dungeonDoorKeyUse(currentRoom, thisEntity, player)
if getEntity("outsideDungeonDoor")["is_locked"] then
getEntity("outsideDungeonDoor")["is_locked"] := false
return "You unlocked the door."
else
return "The door is already unlocked."
end if
Funkcje opisu działają w ten sam sposób, sprawdzając stan i zwracając odpowiedni ciąg.
EDYCJA 2: Zmieniono brzmienie mojego pytania. Załóżmy, że może istnieć znaczna liczba obiektów w grze, które nie mają wspólnego zachowania (reakcje oparte na stanie na określone działania) z innymi obiektami. Czy istnieje sposób, w jaki mogę zdefiniować te unikalne zachowania w czystszy i łatwiejszy do utrzymania sposób niż pisanie niestandardowej funkcji dla każdej akcji specyficznej dla jednostki?
Odpowiedzi:
Zamiast tworzyć osobną funkcję dla każdej kombinacji rzeczowników i czasowników, powinieneś skonfigurować architekturę, w której istnieje jeden wspólny interfejs, który implementują wszystkie obiekty w grze.
Jednym podejściem z góry mojej głowy byłoby zdefiniowanie obiektu Entity, który rozciągają się na wszystkie określone obiekty w grze. Każda jednostka będzie miała tabelę (niezależnie od struktury danych, której używa Twój język dla tablic asocjacyjnych), która wiąże różne działania z różnymi wynikami. Działania w tabeli będą prawdopodobnie ciągami znaków (np. „Otwarte”), podczas gdy powiązany wynik może nawet być funkcją prywatną w obiekcie, jeśli Twój język obsługuje funkcje pierwszej klasy.
Podobnie stan obiektu jest przechowywany w różnych polach obiektu. Na przykład, możesz mieć tablicę rzeczy w Bushu, a wtedy funkcja powiązana z „szukaj” będzie działać na tej tablicy, zwracając znaleziony obiekt lub ciąg „W krzakach nie było nic więcej”.
Tymczasem jedna z metod publicznych jest podobna do Entity.actOn (akcja String) Następnie w tej metodzie porównaj akcję przekazaną z tabelą akcji dla tego obiektu; jeśli ta akcja znajduje się w tabeli, zwróć wynik.
Teraz wszystkie różne funkcje potrzebne dla każdego obiektu będą zawarte w obiekcie, co ułatwi powtórzenie tego obiektu w innych pokojach (np. Utworzenie wystąpienia obiektu Drzwi w każdym pokoju, który ma drzwi)
Na koniec zdefiniuj wszystkie pokoje w XML lub JSON lub cokolwiek innego, abyś mógł mieć wiele unikalnych pokoi bez potrzeby pisania osobnego kodu dla każdego pokoju. Załaduj ten plik danych podczas uruchamiania gry i przeanalizuj dane, aby utworzyć wystąpienia obiektów wypełniających grę. Coś jak:
DODATEK: aha, właśnie przeczytałem odpowiedź FxIII i ten kawałek pod koniec wyskoczył na mnie:
Chociaż nie zgadzam się, że wyzwolenie pułapki płomienia jest czymś, co mogłoby się zdarzyć tylko raz (widziałem, że pułapka ta jest ponownie wykorzystywana do wielu różnych obiektów), myślę, że w końcu rozumiem, co miałeś na myśli, jeśli chodzi o byty, które reagują wyjątkowo na wkład użytkownika. Prawdopodobnie poradziłbym sobie z takimi problemami, jak sprawienie, by jedne drzwi w twoim lochu miały pułapkę z ognistą kulą, budując wszystkie moje istoty za pomocą architektury komponentowej (wyjaśnionej szczegółowo w innym miejscu).
W ten sposób każda jednostka Drzwi jest konstruowana jako pakiet komponentów, a ja mogę elastycznie mieszać i dopasowywać komponenty między różnymi jednostkami. Na przykład większość drzwi miałaby konfiguracje podobne do tych
ale byłyby jedne drzwi z pułapką na ognistą kulę
a następnie jedynym unikalnym kodem, który musiałbym napisać dla tych drzwi, jest składnik FireballTrap. Użyłby tych samych elementów Zamka i Portalu, co wszystkie inne drzwi, a gdybym później zdecydował się użyć FireballTrap na skrzyni skarbów lub czegoś tak prostego, jak dodanie składnika FireballTrap do tej skrzyni.
Niezależnie od tego, czy zdefiniujesz wszystkie elementy w skompilowanym kodzie, czy w osobnym języku skryptowym, nie jest to duże rozróżnienie (tak czy inaczej będziesz gdzieś pisał kod ), ale ważne jest to, że możesz znacznie zmniejszyć ilość unikalnego kodu, który musisz napisać. Heck, jeśli nie martwisz się elastycznością projektantów / modderów poziomów (w końcu piszesz tę grę samodzielnie), możesz nawet sprawić, że wszystkie byty odziedziczą się po Entity i dodać komponenty do konstruktora zamiast pliku konfiguracyjnego lub skryptu lub cokolwiek:
źródło
Entity
tylko dla jednego obiektu grupuje kod razem, ale nie zmniejsza ilości kodu, który muszę napisać. Czy jest to nieunikniona pułapka w tym względzie?Entity
a atrybuty określają jego stan początkowy. Zgaduję, że znaczniki potomne encji działają jako parametry dla każdej akcji, z którą ten tag jest powiązany, prawda?Problem wymiarowy, który rozwiązujesz, jest dość normalny i prawie nieunikniony. Chcesz znaleźć sposób wyrażenia swoich bytów, który jest jednocześnie spójny i elastyczny .
„Pojemnik” (krzak w odpowiedzi na jhocking) jest zbiegiem okoliczności, ale widzisz, że nie jest wystarczająco elastyczny .
Nie proponujemy, aby spróbować znaleźć rodzajowy interfejs, a następnie korzystać z plików konfiguracyjnych do określenia zachowań, dlatego zawsze będziesz mieć nieprzyjemne uczucie bycia między młotem (podmioty standardowe i nudne, łatwe do opisania) a kowadłem ( unikalne fantastyczne byty, ale zbyt długo, aby je wdrożyć).
Moja propozycja jest taka, aby używać jak interpretować język do zachowań kodu.
Pomyśl o przykładzie z krzakiem: jest to pojemnik, ale nasz krzak musi mieć w sobie określone przedmioty; obiekt kontenerowy może mieć:
Jeden z tych przedmiotów ma linę, która uruchamia urządzenie, które z kolei wystrzeliwuje płomień płonący krzak ... (widzisz, mogę czytać w twoich myślach, aby poznać rzeczy, które lubisz).
Możesz użyć skryptu do opisania tego krzaka zamiast pliku konfiguracyjnego umieszczającego odpowiedni dodatkowy kod w zaczepie, który wykonujesz z głównego programu za każdym razem, gdy ktoś wybiera element z kontenera.
Teraz masz wiele możliwości wyboru architektury: możesz zdefiniować narzędzia behawioralne jako klasy podstawowe, używając swojego języka kodowego lub języka skryptowego (takie rzeczy, jak kontenery, drzwi itp.). Cel z theese rzeczy to niech Ci opisać podmioty easely agregacji zachowań prostych i konfigurowanie ich za pomocą wiązań w języku skryptowym .
Wszystkie elementy powinny być dostępne dla skryptu: możesz powiązać identyfikator z każdym elementem i umieścić je w kontenerze, który jest eksportowany jako rozszerzenie skryptu języka skryptowego.
Korzystanie ze strategii skryptowych pozwala zachować prostotę konfiguracji (żadnych rzeczy
<item triggerFlamesOnPicking="true">
, których użyjesz tylko raz), a jednocześnie umożliwia wyrażanie dziwnych zachowań (zabawnych) poprzez dodanie wiersza koduW kilku słowach: skrypty jako plik konfiguracyjny, który może uruchamiać kod.
źródło