Realizacja zachowania w prostej grze przygodowej

11

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 Roomobiekty. Każdy Roomma listę Entityobiektów znajdujących się w tym pokoju. Każdy z nich Entityma 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]. RoomUżywa nazwy jednostki, aby powrócić odpowiedni Entityobiekt, który następnie używa nazwy działania, aby znaleźć właściwą funkcję i wykonuje go.

Aby wygenerować opis pokoju, każdy Roomobiekt wyświetla własny ciąg opisu, a następnie dołącza ciąg opisu każdego Entity. EntityOpis 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 Entitypodklasy 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?

Eric
źródło
1
Myślę, że musisz wyjaśnić, co robią te „funkcje akcji” i być może opublikować jakiś kod, ponieważ nie jestem pewien, o czym tu mówisz.
jhocking
Dodano kod.
Eric

Odpowiedzi:

5

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:

<rooms>
  <room id="room1">
    <description>Outside the dungeon you see some bushes and a heavy door over the entrance.</description>
    <entities>
      <bush>
        <description>The bushes are thick and leafy.</description>
        <contains>
          <key />
        </contains>
      </bush>
      <door connection="room2" isLocked="true">
        <description>It's an oak door with stout iron clasps.</description>
      </door>
    </entities>
  </room>

  <room id="room2">
    etc.

DODATEK: aha, właśnie przeczytałem odpowiedź FxIII i ten kawałek pod koniec wyskoczył na mnie:

(no things like <item triggerFlamesOnPicking="true"> that you will use just once)

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

<entity name="door">
  <description>It's an oak door with stout iron clasps.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room2" />
  </components>
</entity>

ale byłyby jedne drzwi z pułapką na ognistą kulę

<entity name="door">
  <description>There are strange runes etched into the wood.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room7" />
    <fireballTrap />
  </components>
</entity>

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:

Door extends Entity {
  public Door() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
  }
}

TrappedDoor extends Entity {
  public TrappedDoor() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
    addComponent(new FireballTrap());
  }
}
jhocking
źródło
1
Działa to w przypadku typowych, powtarzalnych elementów. Ale co z jednostkami, które reagują wyjątkowo na dane wejściowe użytkownika? Utworzenie podklasy Entitytylko 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?
Eric
1
Zwróciłem się do przykładów, które podałeś. Nie umiem czytać w twoich myślach; jakie obiekty i dane wejściowe chcesz mieć?
jhocking 31.01.12
Zredagowałem mój post, aby lepiej wyjaśnić moje zamiary. Jeśli dobrze rozumiem twój przykład, wygląda na to, że każdy znacznik encji odpowiada pewnej podklasie, Entitya 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?
Eric
tak, to jest pomysł.
jhocking
Powinienem był pomyśleć, że komponenty byłyby częścią rozwiązania. Dzięki za pomoc.
Eric
1

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ć:

  • metoda dodawania przedmiotu przez gawędziarza,
  • metoda pokazania przez silnik elementu, który zawiera,
  • sposób, w jaki gracz wybiera przedmiot.

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 kodu

W kilku słowach: skrypty jako plik konfiguracyjny, który może uruchamiać kod.

FxIII
źródło