Pisanie wieloosobowego silnika tekstowego przygodowego w Node.js (część 1)
Opublikowany: 2022-03-10Przygody tekstowe były jedną z pierwszych form cyfrowych gier fabularnych, kiedy gry nie miały grafiki, a wszystko, co miałeś, to twoja wyobraźnia i opis, który czytasz na czarnym ekranie monitora CRT.
Jeśli chcemy poczuć nostalgię, być może nazwa Colossal Cave Adventure (lub po prostu Adventure, jak pierwotnie została nazwana) brzmi dzwonkiem. To była pierwsza tekstowa gra przygodowa, jaką kiedykolwiek stworzono.
Powyższy obrazek pokazuje, jak faktycznie widzisz tę grę, co jest dalekie od naszych obecnych najlepszych gier przygodowych AAA. Biorąc to pod uwagę, grało się w nie fajnie i kradły setki godzin twojego czasu, gdy siedziałeś sam przed tym tekstem, próbując wymyślić, jak go pokonać.
Zrozumiałe jest, że tekstowe przygody z biegiem lat zostały zastąpione grami prezentującymi lepszą oprawę graficzną (choć można by argumentować, że wiele z nich poświęciło fabułę na rzecz grafiki) oraz, zwłaszcza w ostatnich latach, coraz większą możliwość współpracy z innymi przyjaciele i bawić się razem. Ta szczególna cecha to ta, której brakowało w oryginalnych przygodach tekstowych i którą chcę przywrócić w tym artykule.
Inne części tej serii
- Część 2: Projekt serwera silnika gry
- Część 3: Tworzenie klienta terminala
- Część 4: Dodawanie czatu do naszej gry
Nasz cel
Cały sens tego przedsięwzięcia, jak zapewne domyślasz się już z tytułu tego artykułu, polega na stworzeniu tekstowego silnika przygodowego, który pozwala dzielić przygodę ze znajomymi, umożliwiając współpracę z nimi podobnie jak podczas gra Dungeons & Dragons (w której, podobnie jak w starych, dobrych tekstowych przygodach, nie ma grafiki do oglądania).
Przy tworzeniu silnika, serwera czatu i klienta jest sporo pracy. W tym artykule pokażę fazę projektowania, wyjaśniając takie rzeczy, jak architektura silnika, sposób interakcji klienta z serwerami i zasady tej gry.
Aby dać ci trochę wizualnej pomocy, jak to będzie wyglądać, oto mój cel:
To jest nasz cel. Gdy już tam dotrzemy, zamiast szybkich i brudnych makiet, będą mieli zrzuty ekranu. Przejdźmy więc do procesu. Pierwszą rzeczą, którą omówimy, jest projekt całości. Następnie omówimy najbardziej odpowiednie narzędzia, których użyję do zakodowania tego. Na koniec pokażę Ci niektóre z najbardziej odpowiednich fragmentów kodu (oczywiście z linkiem do pełnego repozytorium).
Mamy nadzieję, że pod koniec zaczniesz tworzyć nowe przygody tekstowe, aby wypróbować je ze znajomymi!
Faza projektowania
W fazie projektowania omówię nasz ogólny plan. Zrobię co w mojej mocy, aby nie zanudzić cię na śmierć, ale jednocześnie uważam, że ważne jest pokazanie niektórych zakulisowych rzeczy, które muszą się wydarzyć, zanim napiszesz pierwszy wiersz kodu.
Cztery elementy, które chcę tutaj omówić z przyzwoitą ilością szczegółów, to:
- Silnik
To będzie główny serwer gry. Tutaj zostaną zaimplementowane reguły gry, które zapewnią niezależny technologicznie interfejs dla każdego typu klienta. Zaimplementujemy klienta terminala, ale możesz zrobić to samo z klientem przeglądarki internetowej lub dowolnym innym typem. - Serwer czatu
Ponieważ usługa jest wystarczająco złożona, aby mieć własny artykuł, ta usługa będzie również miała swój własny moduł. Serwer czatu zadba o umożliwienie graczom komunikowania się ze sobą podczas gry. - Klient
Jak wspomniano wcześniej, będzie to klient terminala, który w idealnym przypadku będzie wyglądał podobnie do makiety z wcześniejszej wersji. Będzie korzystać z usług dostarczanych zarówno przez silnik, jak i serwer czatu. - Gry (pliki JSON)
Na koniec omówię definicję rzeczywistych gier. Sednem tego jest stworzenie silnika, który może uruchomić dowolną grę, o ile plik gry jest zgodny z wymaganiami silnika. Tak więc, nawet jeśli nie będzie to wymagało kodowania, wyjaśnię, jak ustrukturyzuję pliki przygód, aby w przyszłości pisać własne przygody.
Silnik
Silnik gry lub serwer gry będzie interfejsem API REST i zapewni wszystkie wymagane funkcje.
Postawiłem na REST API po prostu dlatego, że – w tego typu grach – opóźnienie dodawane przez HTTP i jego asynchroniczny charakter nie sprawią żadnych kłopotów. Będziemy jednak musieli pójść inną drogą dla serwera czatu. Ale zanim zaczniemy definiować punkty końcowe dla naszego API, musimy zdefiniować, do czego będzie zdolny silnik. Więc przejdźmy do tego.
Funkcja | Opis |
---|---|
Dołącz do gry | Gracz będzie mógł dołączyć do gry, podając identyfikator gry. |
Utwórz nową grę | Gracz może również stworzyć nową instancję gry. Silnik powinien zwrócić identyfikator, aby inni mogli go użyć do dołączenia. |
Scena powrotu | Ta funkcja powinna zwrócić bieżącą scenę, w której znajduje się impreza. Zasadniczo zwróci opis ze wszystkimi powiązanymi informacjami (możliwe akcje, zawarte w nim obiekty itp.). |
Wejdź w interakcję ze sceną | Będzie to jeden z najbardziej skomplikowanych, ponieważ przyjmie polecenie od klienta i wykona tę akcję — takie rzeczy jak przenoszenie, pchanie, branie, patrzenie, czytanie, żeby wymienić tylko kilka. |
Sprawdź stan magazynowy | Chociaż jest to sposób na interakcję z grą, nie odnosi się to bezpośrednio do sceny. Tak więc sprawdzenie ekwipunku dla każdego gracza będzie traktowane jako inna czynność. |
Słowo o ruchu
Potrzebujemy sposobu na mierzenie odległości w grze, ponieważ przechodzenie przez przygodę jest jedną z podstawowych czynności, które gracz może wykonać. Będziemy używać tej liczby jako miary czasu, aby uprościć rozgrywkę. Mierzenie czasu za pomocą rzeczywistego zegara może nie być najlepsze, biorąc pod uwagę, że tego typu gry mają akcje turowe, takie jak walka. Zamiast tego użyjemy odległości do mierzenia czasu (co oznacza, że pokonanie odległości równej 8 będzie wymagało więcej czasu niż pokonanie odległości równej 2, co pozwoli nam na robienie takich rzeczy, jak dodawanie efektów do graczy, które utrzymują się przez określoną liczbę „punktów odległości” ).
Innym ważnym aspektem, który należy wziąć pod uwagę w odniesieniu do ruchu, jest to, że nie gramy sami. Dla uproszczenia silnik nie pozwoli graczom na rozdzielenie drużyny (chociaż może to być ciekawa poprawa na przyszłość). Początkowa wersja tego modułu pozwoli wszystkim poruszać się tylko tam, gdzie zadecyduje większość drużyny. Tak więc przeprowadzka będzie musiała być dokonana w drodze konsensusu, co oznacza, że każda akcja przeprowadzki będzie czekać, aż większość partii o to poprosi, zanim nastąpi.
Walka
Walka to kolejny bardzo ważny aspekt tego typu gier, który będziemy musieli rozważyć dodanie do naszego silnika; w przeciwnym razie stracimy część zabawy.
To nie jest coś, co trzeba wymyślać na nowo, szczerze mówiąc. Turowa walka drużynowa istnieje od dziesięcioleci, więc po prostu zaimplementujemy wersję tej mechaniki. Będziemy mieszać to z koncepcją „inicjatywy” Dungeons & Dragons, wyrzucając losową liczbę, aby walka była nieco bardziej dynamiczna.
Innymi słowy, kolejność, w jakiej wszyscy zaangażowani w walkę wybierają swoją akcję, będzie losowa, w tym także wrogowie.
Wreszcie (chociaż omówię to bardziej szczegółowo poniżej), będziesz mieć przedmioty, które możesz odebrać z ustaloną liczbą „uszkodzeń”. Są to przedmioty, których będziesz mógł używać podczas walki; wszystko, co nie ma tej właściwości, spowoduje 0 obrażeń twoim wrogom. Prawdopodobnie dodamy wiadomość, gdy spróbujesz użyć tych obiektów do walki, abyś wiedział, że to, co próbujesz zrobić, nie ma sensu.
Interakcja klient-serwer
Zobaczmy teraz, jak dany klient wszedłby w interakcję z naszym serwerem przy użyciu wcześniej zdefiniowanej funkcjonalności (nie myślimy jeszcze o punktach końcowych, ale do tego dojdziemy za chwilę):
Początkowa interakcja między klientem a serwerem (z punktu widzenia serwera) to rozpoczęcie nowej gry, a jej kroki są następujące:
- Utwórz nową grę .
Klient żąda utworzenia nowej gry z serwera. - Utwórz pokój rozmów .
Chociaż nazwa tego nie określa, serwer nie tylko tworzy czat na serwerze czatu, ale także konfiguruje wszystko, czego potrzebuje, aby umożliwić zestawowi graczy rozegranie przygody. - Zwróć metadane gry .
Po utworzeniu gry przez serwer i udostępnieniu pokoju rozmów dla graczy, klient będzie potrzebował tych informacji do kolejnych żądań. Będzie to głównie zestaw identyfikatorów, których klienci mogą używać do identyfikowania siebie i bieżącej gry, do której chcą dołączyć (więcej o tym za chwilę). - Ręcznie udostępnij identyfikator gry .
Ten krok będzie musiał wykonać sami gracze. Moglibyśmy wymyślić jakiś mechanizm dzielenia się, ale zostawię to na liście życzeń dla przyszłych ulepszeń. - Dołącz do gry .
Ten jest całkiem prosty. Ponieważ każdy ma identyfikator gry, dołączy do przygody, korzystając ze swoich aplikacji klienckich. - Dołącz do ich pokoju rozmów .
Wreszcie, aplikacje klienckie graczy będą korzystać z metadanych gry, aby dołączyć do pokoju rozmów w ich przygodzie. To ostatni krok wymagany przed grą. Gdy to wszystko zostanie zrobione, gracze są gotowi do rozpoczęcia przygody!
Po spełnieniu wszystkich warunków wstępnych gracze mogą rozpocząć przygodę, dzielić się przemyśleniami na czacie w grupie i rozwijać historię. Powyższy diagram pokazuje cztery wymagane do tego kroki.
Poniższe kroki będą działać jako część pętli gry, co oznacza, że będą powtarzane aż do zakończenia gry.
- Poproś o scenę .
Aplikacja kliencka zażąda metadanych dla bieżącej sceny. To pierwszy krok w każdej iteracji pętli. - Zwróć metadane .
Serwer z kolei odeśle metadane dla bieżącej sceny. Informacje te będą obejmować takie rzeczy, jak ogólny opis, obiekty znalezione w jego wnętrzu i ich wzajemne relacje. - Wyślij polecenie .
Tu zaczyna się zabawa. To jest główne wejście odtwarzacza. Będzie zawierał czynność, którą chcą wykonać, i opcjonalnie cel tej czynności (na przykład zdmuchnięcie świecy, złapanie kamienia itd.). - Zwróć reakcję na wysłane polecenie .
To może być po prostu krok drugi, ale dla jasności dodałem go jako dodatkowy krok. Główna różnica polega na tym, że krok drugi można uznać za początek tej pętli, podczas gdy ten uwzględnia fakt, że już grasz, a zatem serwer musi zrozumieć, na kogo ta akcja wpłynie (albo na jednego gracza lub wszystkich graczy).
Jako dodatkowy krok, choć nie jest to tak naprawdę częścią przepływu, serwer powiadomi klientów o aktualizacjach statusu, które są dla nich istotne.
Powodem tego dodatkowego, powtarzającego się kroku są aktualizacje, które gracz może otrzymać w wyniku działań innych graczy. Przypomnij sobie wymóg przemieszczania się z jednego miejsca do drugiego; jak powiedziałem wcześniej, gdy większość graczy wybierze kierunek, wszyscy gracze się poruszą (nie jest wymagany wkład wszystkich graczy).
Ciekawostką jest to, że HTTP (wspomnieliśmy już, że serwer będzie REST API) nie pozwala na tego typu zachowanie. Nasze opcje to:
- wykonywać odpytywanie co X sekund od klienta,
- użyj jakiegoś systemu powiadomień, który działa równolegle z połączeniem klient-serwer.
Z mojego doświadczenia wynika, że wolę opcję 2. W rzeczywistości chciałbym (i będę w tym artykule) używać Redis do tego rodzaju zachowań.
Poniższy diagram przedstawia zależności między usługami.
Serwer czatu
Szczegóły projektu tego modułu pozostawiam na fazę rozwoju (która nie jest częścią tego artykułu). Biorąc to pod uwagę, są rzeczy, o których możemy decydować.
Jedyne, co możemy zdefiniować, to zestaw ograniczeń dla serwera, co uprości naszą pracę. A jeśli dobrze rozegramy nasze karty, możemy otrzymać usługę, która zapewni solidny interfejs, co pozwoli nam ostatecznie rozszerzyć lub nawet zmienić implementację, aby zapewnić mniej ograniczeń bez wpływu na grę.
- Będzie tylko jeden pokój na imprezę.
Nie pozwolimy na tworzenie podgrup. Idzie to w parze z niedopuszczeniem do podziału partii. Może po wdrożeniu tego ulepszenia dobrym pomysłem byłoby umożliwienie tworzenia podgrup i niestandardowego czatu. - Nie będzie prywatnych wiadomości.
Służy to wyłącznie uproszczeniu, ale prowadzenie czatu grupowego jest już wystarczająco dobre; nie potrzebujemy teraz prywatnych wiadomości. Pamiętaj, że za każdym razem, gdy pracujesz nad swoim minimalnym opłacalnym produktem, staraj się unikać wchodzenia w króliczą norę niepotrzebnych funkcji; to niebezpieczna ścieżka, z której trudno się wydostać. - Nie będziemy utrwalać wiadomości.
Innymi słowy, jeśli opuścisz imprezę, stracisz wiadomości. To znacznie uprości nasze zadanie, ponieważ nie będziemy musieli zajmować się żadnym rodzajem przechowywania danych, ani nie będziemy musieli tracić czasu na decydowanie o najlepszej strukturze danych do przechowywania i odzyskiwania starych wiadomości. To wszystko pozostanie w pamięci i pozostanie tam tak długo, jak aktywny jest pokój rozmów. Po zamknięciu po prostu się z nimi pożegnamy! - Komunikacja będzie odbywać się przez gniazda .
Niestety nasz klient będzie musiał obsługiwać podwójny kanał komunikacji: RESTful dla silnika gry i socket dla serwera czatu. Może to nieco zwiększyć złożoność klienta, ale jednocześnie wykorzysta najlepsze metody komunikacji dla każdego modułu. (Nie ma sensu wymuszanie REST na naszym serwerze czatu lub wymuszanie gniazd na naszym serwerze gier. Takie podejście zwiększyłoby złożoność kodu po stronie serwera, który również obsługuje logikę biznesową, więc skupmy się na tej stronie Na razie.)
To tyle, jeśli chodzi o serwer czatu. W końcu nie będzie to skomplikowane, przynajmniej nie na początku. Jest więcej do zrobienia, gdy nadejdzie czas, aby zacząć go kodować, ale w tym artykule jest to więcej niż wystarczająco informacji.
Klient
To jest ostatni moduł, który wymaga kodowania i będzie to nasz najgłupszy z wielu. Z reguły wolę, aby moi klienci byli głupi, a moje serwery inteligentne. W ten sposób tworzenie nowych klientów dla serwera staje się znacznie łatwiejsze.
Abyśmy byli na tej samej stronie, oto architektura wysokiego poziomu, z którą powinniśmy skończyć.
Nasz prosty klient CLI nie zaimplementuje niczego bardzo skomplikowanego. W rzeczywistości najbardziej skomplikowanym elementem, z którym będziemy musieli się zmierzyć, jest rzeczywisty interfejs użytkownika, ponieważ jest to interfejs tekstowy.
Biorąc to pod uwagę, funkcjonalność, którą aplikacja kliencka będzie musiała zaimplementować, jest następująca:
- Utwórz nową grę .
Ponieważ chcę, aby wszystko było tak proste, jak to możliwe, zostanie to zrobione tylko przez interfejs CLI. Rzeczywisty interfejs użytkownika będzie używany dopiero po dołączeniu do gry, co prowadzi nas do następnego punktu. - Dołącz do istniejącej gry .
Biorąc pod uwagę kod gry zwrócony z poprzedniego punktu, gracze mogą go użyć, aby dołączyć. Ponownie, jest to coś, co powinieneś być w stanie zrobić bez interfejsu użytkownika, więc ta funkcja będzie częścią procesu wymaganego do rozpoczęcia korzystania z interfejsu tekstowego. - Przeanalizuj pliki definicji gry .
Omówimy je za chwilę, ale klient powinien być w stanie zrozumieć te pliki, aby wiedzieć, co pokazać i wiedzieć, jak korzystać z tych danych. - Wejdź w interakcję z przygodą.
Zasadniczo daje to graczowi możliwość interakcji z opisanym środowiskiem w dowolnym momencie. - Utrzymuj ekwipunek dla każdego gracza .
Każda instancja klienta będzie zawierać listę elementów w pamięci. Ta lista zostanie zarchiwizowana. - Pomoc na czacie .
Aplikacja kliencka musi również połączyć się z serwerem czatu i zalogować użytkownika do pokoju czatu drużyny.
Więcej o wewnętrznej strukturze i projekcie klienta później. W międzyczasie zakończmy etap projektowania ostatnim etapem przygotowań: plikami gry.
Gra: pliki JSON
Tutaj robi się ciekawie, ponieważ do tej pory omówiłem podstawowe definicje mikroserwisów. Niektóre z nich mogą mówić REST, a inne mogą współpracować z gniazdami, ale w gruncie rzeczy wszystkie są takie same: definiujesz je, kodujesz, a one dostarczają usługę.
W przypadku tego konkretnego komponentu nie planuję niczego kodować, ale musimy go zaprojektować. Zasadniczo wdrażamy rodzaj protokołu do definiowania naszej gry, jej scen i wszystkiego, co się w nich znajduje.
Jeśli się nad tym zastanowić, tekstowa przygoda jest w istocie zestawem połączonych ze sobą pokoi, a wewnątrz nich znajdują się „rzeczy”, z którymi można wchodzić w interakcje, a wszystko to powiązane razem, miejmy nadzieję, przyzwoitej historii. Teraz nasz silnik nie zajmie się tą ostatnią częścią; ta część będzie zależeć od ciebie. Ale co do reszty, jest nadzieja.
Teraz, wracając do zestawu połączonych ze sobą pomieszczeń, brzmi to dla mnie jak wykres, a jeśli dodamy również pojęcie odległości lub prędkości ruchu, o którym wspomniałem wcześniej, otrzymamy wykres ważony. A to jest po prostu zestaw węzłów, które mają wagę (lub tylko liczbę — nie martw się, jak to się nazywa), która reprezentuje ścieżkę między nimi. Oto wizualizacja (uwielbiam uczyć się przez widzenie, więc spójrz tylko na obraz, ok?):
To jest wykres ważony — to wszystko. I jestem pewien, że już to rozgryzłeś, ale dla kompletności pozwól, że pokażę ci, jak sobie z tym poradzisz, gdy nasz silnik będzie gotowy.
Gdy zaczniesz konfigurować przygodę, utworzysz swoją mapę (jak widać po lewej stronie poniższego obrazu). A potem przetłumaczysz to na wykres ważony, jak widać po prawej stronie obrazu. Nasz silnik będzie w stanie go odebrać i przejść przez niego w odpowiedniej kolejności.
Dzięki powyższemu wykresowi ważonemu możemy upewnić się, że gracze nie mogą przejść od wejścia do lewego skrzydła. Musiałyby przejść przez węzły pomiędzy tymi dwoma, a zrobienie tego zabierze czas, który możemy zmierzyć za pomocą wagi z połączeń.
Teraz przejdźmy do części „zabawy”. Zobaczmy, jak wyglądałby wykres w formacie JSON. Znoś mnie tutaj; ten JSON będzie zawierał dużo informacji, ale przejdę przez tyle, ile mogę:
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
Wiem, że wygląda to na dużo, ale jeśli sprowadzisz to do prostego opisu gry, otrzymasz loch składający się z sześciu pomieszczeń, z których każdy jest połączony z innymi, jak pokazano na powyższym schemacie.
Twoim zadaniem jest poruszanie się po nim i odkrywanie go. Znajdziesz tam dwa różne miejsca, w których możesz znaleźć broń (w kuchni lub w ciemnym pokoju, rozbijając krzesło). Będziesz także skonfrontowany z zamkniętymi drzwiami; więc gdy znajdziesz klucz (znajdujący się w pokoju przypominającym biuro), będziesz mógł go otworzyć i walczyć z bossem dowolną zebraną bronią.
Albo wygrasz, zabijając go, albo przegrasz, gdy zostaniesz przez niego zabity.
Przejdźmy teraz do bardziej szczegółowego przeglądu całej struktury JSON i jej trzech sekcji.
Wykres
Ten będzie zawierał relację między węzłami. Zasadniczo ta sekcja bezpośrednio przekłada się na wykres, na który patrzyliśmy wcześniej.
Struktura tej sekcji jest dość prosta. Jest to lista węzłów, gdzie każdy węzeł zawiera następujące atrybuty:
- identyfikator, który jednoznacznie identyfikuje węzeł wśród wszystkich innych w grze;
- imię, które jest w zasadzie czytelną dla człowieka wersją identyfikatora;
- zestaw linków do innych węzłów. Świadczy o tym istnienie czterech możliwych kluczy: północy, południa, wschodu i zachodu. Moglibyśmy w końcu dodać dalsze kierunki, dodając kombinacje tych czterech. Każde łącze zawiera identyfikator powiązanego węzła i odległość (lub wagę) tej relacji.
Gra
Ta sekcja zawiera ogólne ustawienia i warunki. W szczególności w powyższym przykładzie ta sekcja zawiera warunki wygranej i przegranej. Innymi słowy, dzięki tym dwóm warunkom, poinformujemy silnik, kiedy gra może się zakończyć.
Aby uprościć sprawę, dodałem tylko dwa warunki:
- albo wygrywasz zabijając szefa,
- lub przegraj, ginąc.
Pokoje
Stąd pochodzi większość ze 163 linii i jest to najbardziej złożona z sekcji. Tutaj opiszemy wszystkie pomieszczenia w naszej przygodzie i wszystko, co się w nich znajduje.
Do każdego pokoju będzie klucz, korzystając ze zdefiniowanego wcześniej identyfikatora. A każdy pokój będzie miał opis, listę przedmiotów, listę wyjść (lub drzwi) oraz listę postaci niegrywalnych (NPC). Spośród tych właściwości jedynym, który powinien być obowiązkowy, jest opis, ponieważ jest on wymagany, aby silnik informował Cię o tym, co widzisz. Reszta będzie tam tylko wtedy, gdy będzie coś do pokazania.
Przyjrzyjmy się, co te właściwości mogą zrobić dla naszej gry.
Opis
Ten element nie jest tak prosty, jak mogłoby się wydawać, ponieważ twój widok pokoju może się zmieniać w zależności od różnych okoliczności. Jeśli na przykład spojrzysz na opis pierwszego pokoju, zauważysz, że domyślnie nic nie widzisz, chyba że masz przy sobie zapaloną pochodnię.
Tak więc podnoszenie przedmiotów i używanie ich może wywołać globalne warunki, które wpłyną na inne części gry.
Przedmioty
Reprezentują one wszystkie rzeczy”, które można znaleźć w pokoju. Każdy element ma ten sam identyfikator i nazwę, które miały węzły w sekcji wykresu.
Będą również miały właściwość „miejsce docelowe”, która wskazuje, gdzie dany przedmiot powinien być przechowywany po odebraniu. Jest to istotne, ponieważ będziesz mógł mieć tylko jeden przedmiot w swoich rękach, podczas gdy będziesz mógł mieć tyle, ile chcesz w ekwipunku.
Wreszcie, niektóre z tych przedmiotów mogą wywołać inne działania lub aktualizacje statusu, w zależności od tego, co gracz zdecyduje się z nimi zrobić. Jednym z przykładów są zapalone pochodnie przy wejściu. Jeśli złapiesz jeden z nich, uruchomisz aktualizację statusu w grze, co z kolei sprawi, że gra wyświetli inny opis następnego pokoju.
Przedmioty mogą również zawierać „podtemy”, które wchodzą do gry, gdy oryginalny przedmiot zostanie zniszczony (na przykład poprzez akcję „przerwania”). Pozycję można podzielić na kilka, co jest zdefiniowane w elemencie „subitems”.
Zasadniczo ten element to tylko szereg nowych elementów, który zawiera również zestaw działań, które mogą wywołać ich tworzenie. Zasadniczo otwiera to możliwość tworzenia różnych podelementów na podstawie działań, które wykonujesz na oryginalnym przedmiocie.
Wreszcie, niektóre przedmioty będą miały właściwość „uszkodzenia”. Tak więc, jeśli użyjesz przedmiotu do trafienia NPC, ta wartość zostanie użyta do odjęcia od niego życia.
Wyjścia
Jest to po prostu zestaw właściwości wskazujących kierunek wyjścia i jego właściwości (opis, jeśli chcesz go sprawdzić, jego nazwę iw niektórych przypadkach jego status).
Wyjścia są oddzielną jednostką od elementów, ponieważ wyszukiwarka będzie musiała zrozumieć, czy rzeczywiście możesz je przemierzać na podstawie ich statusu. Wyjścia, które są zablokowane, nie pozwolą ci przez nie przejść, chyba że wymyślisz, jak zmienić ich status na odblokowane.
NPC
Wreszcie, NPC będą częścią innej listy. Są to w zasadzie elementy ze statystykami, których silnik użyje, aby zrozumieć, jak każdy z nich powinien się zachowywać. Te, które zdefiniowaliśmy w naszym przykładzie, to „hp”, co oznacza punkty zdrowia, oraz „damage”, które, podobnie jak broń, jest liczbą, którą każde trafienie odejmie od zdrowia gracza.
To wszystko dla lochu, który stworzyłem. To dużo, tak, a w przyszłości może rozważę stworzenie pewnego rodzaju edytora poziomów, aby uprościć tworzenie plików JSON. Ale na razie to nie będzie konieczne.
Jeśli jeszcze nie zdajesz sobie z tego sprawy, główną korzyścią z posiadania naszej gry zdefiniowanej w pliku takim jak ten jest to, że będziemy mogli przełączać pliki JSON, tak jak robiłeś to w erze Super Nintendo. Po prostu załaduj nowy plik i rozpocznij nową przygodę. Łatwo!
Myśli zamykające
Dzięki za przeczytanie do tej pory. Mam nadzieję, że podobał Ci się proces projektowania, przez który przechodzę, aby wprowadzić pomysł w życie. Pamiętaj jednak, że wymyślam to na bieżąco, abyśmy mogli później zdać sobie sprawę, że coś, co zdefiniowaliśmy dzisiaj, nie zadziała, w takim przypadku będziemy musieli cofnąć się i to naprawić.
Jestem pewien, że jest mnóstwo sposobów na ulepszenie przedstawionych tu pomysłów i zrobienie piekielnego silnika. Ale to wymagałoby o wiele więcej słów, niż mogę umieścić w artykule, nie czyniąc go nudnym dla wszystkich, więc na razie to zostawmy.
Inne części tej serii
- Część 2: Projekt serwera silnika gry
- Część 3: Tworzenie klienta terminala
- Część 4: Dodawanie czatu do naszej gry