Node.js'de Çok Oyunculu Metin Macera Motoru Yazma (1. Kısım)

Yayınlanan: 2022-03-10
Kısa özet ↬ Hiç bir metin macerası duydunuz mu? Bu makale dizisinde, Fernando Doglio, sizin ve arkadaşlarınızın keyif aldığı herhangi bir metin macerasını oynamanıza izin verebilecek tüm bir motorun nasıl oluşturulacağını açıklıyor. Bu doğru, metin macera türüne çok oyunculu modu ekleyerek biraz renklendireceğiz!

Metin maceraları, oyunların grafikleri olmadığı ve sahip olduğunuz tek şeyin kendi hayal gücünüz ve CRT monitörünüzün siyah ekranında okuduğunuz açıklama olduğu zamanlarda, dijital rol yapma oyunlarının ilk biçimlerinden biriydi.

Nostaljik olmak istiyorsak, belki de Colossal Cave Adventure (veya orijinal adıyla sadece Adventure) adı bir zil çalıyor. Bu şimdiye kadar yapılmış ilk metin macera oyunuydu.

Geçmişten gerçek bir metin macerasının resmi
Geçmişten gerçek bir metin macerasının resmi. (Büyük önizleme)

Yukarıdaki resim, oyunu gerçekte nasıl göreceğinizdir, mevcut en iyi AAA macera oyunlarımızdan çok farklıdır. Bununla birlikte, oynaması eğlenceliydi ve bu metnin önünde tek başına oturup onu nasıl yeneceğinizi bulmaya çalışırken yüzlerce saatinizi çalacaktı.

Anlaşılır bir şekilde, metin maceraları yıllar içinde daha iyi görseller sunan oyunlarla değiştirildi (ancak birçoğunun hikayeyi grafikler için feda ettiği iddia edilebilir) ve özellikle son birkaç yılda diğer oyuncularla işbirliği yapma yeteneğinin artması. arkadaşlar ve birlikte oynayın. Bu özel özellik, orijinal metin maceralarında eksik olan ve bu makalede geri getirmek istediğim bir özellik.

Bu Serinin Diğer Parçaları

  • Bölüm 2: Oyun Motoru Sunucu Tasarımı
  • Bölüm 3: Terminal İstemcisi Oluşturma
  • 4. Bölüm: Oyunumuza Sohbet Eklemek

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Hedefimiz

Bu çabanın tüm amacı, muhtemelen bu makalenin başlığından tahmin ettiğiniz gibi, macerayı arkadaşlarınızla paylaşmanıza ve onlarla tıpkı bir macerada yaptığınız gibi işbirliği yapmanıza olanak tanıyan bir metin macera motoru oluşturmaktır. bir Dungeons & Dragons oyunu (tıpkı eski metin maceralarında olduğu gibi, bakılacak hiçbir grafik yoktur).

Motoru oluştururken, sohbet sunucusu ve istemci oldukça fazla iş yapıyor. Bu yazıda size tasarım aşamasını göstereceğim, motorun arkasındaki mimariyi, istemcinin sunucularla nasıl etkileşime gireceğini ve bu oyunun kurallarının ne olacağını açıklayacağım.

Bunun nasıl görüneceğine dair size biraz görsel yardım sağlamak için, işte amacım:

Oyun istemcisinin son kullanıcı arayüzü için genel tel kafes
Oyun istemcisinin son kullanıcı arayüzü için genel tel kafes (Büyük önizleme)

Hedefimiz bu. Oraya vardığımızda, hızlı ve kirli maketler yerine ekran görüntülerine sahip olacaksınız. Öyleyse, süreçle aşağı inelim. Ele alacağımız ilk şey, her şeyin tasarımıdır. Ardından, bunu kodlamak için kullanacağım en alakalı araçları ele alacağız. Son olarak, size en alakalı kod parçalarından bazılarını göstereceğim (tabii ki tam depoya bir bağlantı ile).

Umarım sonunda, arkadaşlarınızla denemek için kendinizi yeni metin maceraları yaratırken bulacaksınız!

Tasarım aşaması

Tasarım aşaması için genel planımızı ele alacağım. Canınızı sıkmamak için elimden gelenin en iyisini yapacağım, ama aynı zamanda, ilk kod satırınızı koymadan önce gerçekleşmesi gereken bazı sahne arkası şeyleri göstermenin önemli olduğunu düşünüyorum.

Burada yeterli miktarda ayrıntıyla ele almak istediğim dört bileşen şunlardır:

  • motor
    Bu ana oyun sunucusu olacak. Oyun kuralları burada uygulanacak ve her tür müşterinin tüketmesi için teknolojik olarak agnostik bir arayüz sağlayacaktır. Bir terminal istemcisi uygulayacağız, ancak aynısını bir web tarayıcı istemcisi veya istediğiniz herhangi bir tür ile yapabilirsiniz.
  • sohbet sunucusu
    Kendi makalesine sahip olacak kadar karmaşık olduğu için, bu hizmetin de kendi modülü olacak. Sohbet sunucusu, oyun sırasında oyuncuların birbirleriyle iletişim kurmasına izin verecek.
  • Müşteri
    Daha önce belirtildiği gibi, bu, ideal olarak önceki modele benzeyen bir terminal istemcisi olacaktır. Hem motor hem de sohbet sunucusu tarafından sağlanan hizmetlerden yararlanacaktır.
  • Oyunlar (JSON dosyaları)
    Son olarak, gerçek oyunların tanımını gözden geçireceğim. Bunun tüm amacı, oyun dosyanız motorun gereksinimlerine uygun olduğu sürece herhangi bir oyunu çalıştırabilecek bir motor oluşturmaktır. Yani bu kodlama gerektirmeyecek olsa da, gelecekte kendi maceralarımızı yazmak için macera dosyalarını nasıl yapılandıracağımı anlatacağım.

motor

Oyun motoru veya oyun sunucusu bir REST API olacak ve gerekli tüm işlevleri sağlayacaktır.

Bir REST API'ye gittim çünkü - bu tür bir oyun için - HTTP tarafından eklenen gecikme ve eşzamansız doğası herhangi bir soruna neden olmayacak. Ancak sohbet sunucusu için farklı bir yoldan gitmemiz gerekecek. Ancak API'miz için uç noktalar tanımlamaya başlamadan önce, motorun neler yapabileceğini tanımlamamız gerekiyor. Öyleyse, ona gidelim.

Özellik Tanım
Bir oyuna katıl Bir oyuncu, oyunun kimliğini belirterek bir oyuna katılabilir.
Yeni bir oyun oluştur Bir oyuncu ayrıca yeni bir oyun örneği oluşturabilir. Başkalarının katılmak için kullanabilmesi için motor bir kimlik döndürmelidir.
dönüş sahnesi Bu özellik, partinin bulunduğu mevcut sahneyi döndürmelidir. Temel olarak, ilgili tüm bilgilerle (olası eylemler, içindeki nesneler vb.) açıklamayı döndürür.
sahne ile etkileşim Bu en karmaşık olanlardan biri olacak, çünkü istemciden bir komut alacak ve bu eylemi gerçekleştirecek - hareket etme, itme, alma, bakma, okuma, bunlardan sadece birkaçı.
Envanteri kontrol et Bu, oyunla etkileşim kurmanın bir yolu olsa da, doğrudan sahne ile ilgili değildir. Bu nedenle, her oyuncu için envanteri kontrol etmek farklı bir işlem olarak kabul edilecektir.

Hareket Hakkında Bir Söz

Oyunda mesafeleri ölçmek için bir yola ihtiyacımız var çünkü macerada ilerlemek bir oyuncunun yapabileceği temel eylemlerden biridir. Bu sayıyı sadece oyunu basitleştirmek için bir zaman ölçüsü olarak kullanacağız. Bu tür oyunların dövüş gibi sıra tabanlı eylemleri olduğu düşünüldüğünde, zamanı gerçek bir saatle ölçmek en iyisi olmayabilir. Bunun yerine, süreyi ölçmek için mesafeyi kullanacağız (8'lik bir mesafe, 2'den birinden daha fazla hareket etmek için daha fazla zaman gerektirecektir, böylece oyunculara belirli bir miktarda "mesafe noktası" boyunca süren efektler eklemek gibi şeyler yapmamıza olanak tanır) ).

Hareketle ilgili düşünülmesi gereken bir diğer önemli husus da yalnız oynamamamızdır. Basitlik adına, motor oyuncuların partiyi bölmesine izin vermeyecek (bu, gelecek için ilginç bir gelişme olabilir). Bu modülün ilk sürümü, herkesin yalnızca partinin çoğunluğunun karar verdiği yere gitmesine izin verecek. Bu nedenle, hareket konsensüs ile yapılmalıdır, yani her hareket eylemi, gerçekleşmeden önce partinin çoğunluğunun talep etmesini bekleyecektir.

savaş

Savaş, bu tür oyunların çok önemli bir başka yönüdür ve motorumuza eklemeyi düşünmemiz gerekecek; Aksi takdirde, eğlencenin bir kısmını kaçıracağız.

Dürüst olmak gerekirse, bu yeniden icat edilmesi gereken bir şey değil. Sıra tabanlı parti savaşı on yıllardır var, bu yüzden bu mekaniğin bir versiyonunu uygulayacağız. Bunu Dungeons & Dragons'ın "girişim" konseptiyle karıştırıp, savaşı biraz daha dinamik tutmak için rastgele bir sayı yuvarlayacağız.

Başka bir deyişle, kavgaya karışan herkesin eylemlerini seçme sırası rastgele olacak ve buna düşmanlar da dahil.

Son olarak (bunu aşağıda daha ayrıntılı olarak ele alacağım), belirli bir "hasar" numarasıyla alabileceğiniz eşyalarınız olacak. Bunlar, savaş sırasında kullanabileceğiniz öğelerdir; bu özelliğe sahip olmayan herhangi bir şey düşmanlarınıza 0 hasar verir. Bu nesneleri savaşmak için kullanmaya çalıştığınızda muhtemelen bir mesaj ekleyeceğiz, böylece yapmaya çalıştığınız şeyin hiçbir anlam ifade etmediğini bileceksiniz.

İstemci-Sunucu Etkileşimi

Şimdi belirli bir istemcinin önceden tanımlanmış işlevselliği kullanarak sunucumuzla nasıl etkileşime gireceğini görelim (henüz uç noktaları düşünmüyoruz, ancak oraya bir saniye içinde geleceğiz):

(Büyük önizleme)

İstemci ve sunucu arasındaki ilk etkileşim (sunucu açısından) yeni bir oyunun başlangıcıdır ve bunun için adımlar aşağıdaki gibidir:

  1. Yeni bir oyun oluşturun .
    İstemci, sunucudan yeni bir oyun oluşturulmasını ister.
  2. Sohbet odası oluşturun .
    Adı belirtmese de, sunucu sadece sohbet sunucusunda bir sohbet odası oluşturmakla kalmıyor, aynı zamanda bir dizi oyuncunun bir macerada oynamasına izin vermek için ihtiyaç duyduğu her şeyi ayarlıyor.
  3. Oyunun meta verilerini döndür .
    Oyun sunucu tarafından oluşturulduğunda ve oyuncular için sohbet odası hazır olduğunda, istemcinin sonraki istekler için bu bilgilere ihtiyacı olacaktır. Bu çoğunlukla, müşterilerin kendilerini ve katılmak istedikleri mevcut oyunu tanımlamak için kullanabilecekleri bir dizi kimlik olacaktır (birazdan daha fazlası).
  4. Oyun kimliğini manuel olarak paylaşın .
    Bu adım oyuncuların kendileri tarafından yapılmalıdır. Bir tür paylaşım mekanizması geliştirebiliriz, ancak bunu gelecekteki iyileştirmeler için istek listesine bırakacağım.
  5. Oyuna katılın .
    Bu oldukça basit. Herkes oyun kimliğine sahip olduğundan, istemci uygulamalarını kullanarak maceraya katılacaklar.
  6. Sohbet odalarına katılın .
    Son olarak, oyuncuların istemci uygulamaları, maceralarının sohbet odasına katılmak için oyunun meta verilerini kullanacak. Bu, oyun öncesi gereken son adımdır. Bütün bunlar bittiğinde, oyuncular maceraya başlamaya hazır!
Mevcut bir oyun için işlem sırası
Mevcut bir oyun için eylem sırası (Büyük önizleme)

Ön koşulların tümü karşılandıktan sonra, oyuncular macerayı oynamaya, parti sohbeti aracılığıyla düşüncelerini paylaşmaya ve hikayeyi ilerletmeye başlayabilirler. Yukarıdaki şema, bunun için gerekli dört adımı göstermektedir.

Aşağıdaki adımlar oyun döngüsünün bir parçası olarak çalışacaktır, yani oyun bitene kadar sürekli olarak tekrarlanacaklardır.

  1. Sahne iste .
    İstemci uygulaması, geçerli sahne için meta verileri isteyecektir. Bu, döngünün her yinelemesindeki ilk adımdır.
  2. Meta verileri döndürün .
    Sunucu sırayla geçerli sahne için meta verileri geri gönderir. Bu bilgi, genel bir açıklama, içinde bulunan nesneler ve bunların birbirleriyle nasıl ilişkili olduğu gibi şeyleri içerecektir.
  3. Komut gönder .
    eğlence burada başlıyor. Bu, oynatıcıdan gelen ana girdidir. Gerçekleştirmek istedikleri eylemi ve isteğe bağlı olarak bu eylemin hedefini (örneğin, mum üfleme, kayayı tutma vb.) içerecektir.
  4. Tepkiyi gönderilen komuta döndürün .
    Bu sadece ikinci adım olabilir, ancak netlik için fazladan bir adım olarak ekledim. Temel fark, ikinci adımın bu döngünün başlangıcı olarak kabul edilebilmesidir, oysa bu adım sizin halihazırda oynadığınızı hesaba katar ve bu nedenle sunucunun bu eylemin kimi etkileyeceğini (ya tek bir oyuncuyu) anlaması gerekir. veya tüm oyuncular).

Ek bir adım olarak, akışın gerçekten bir parçası olmasa da, sunucu istemcileri kendileriyle ilgili durum güncellemeleri hakkında bilgilendirecektir.

Bu ekstra yinelenen adımın nedeni, bir oyuncunun diğer oyuncuların eylemlerinden alabileceği güncellemelerdir. Bir yerden başka bir yere taşınmanın gerekliliğini hatırlayın; Daha önce de söylediğim gibi, oyuncuların çoğunluğu bir yön seçtiğinde, tüm oyuncular hareket edecektir (tüm oyuncuların girdisine gerek yoktur).

Buradaki ilginç nokta, HTTP'nin (sunucunun bir REST API olacağından bahsetmiştik) bu tür davranışlara izin vermemesidir. Yani seçeneklerimiz:

  1. istemciden her X saniyede bir yoklama gerçekleştirin,
  2. istemci-sunucu bağlantısıyla paralel olarak çalışan bir tür bildirim sistemi kullanın.

Deneyimlerime göre 2. seçeneği tercih etme eğilimindeyim. Aslında, bu tür davranışlar için Redis'i kullanırdım (ve bu makale için kullanacağım).

Aşağıdaki diyagram, hizmetler arasındaki bağımlılıkları gösterir.

Bir istemci uygulaması ile oyun motoru arasındaki etkileşimler
Bir istemci uygulaması ile oyun motoru arasındaki etkileşimler (Geniş önizleme)

Sohbet Sunucusu

Bu modülün tasarım detaylarını geliştirme aşamasına bırakacağım (ki bu makalenin bir parçası değildir). Olduğu söyleniyor, karar verebileceğimiz şeyler var.

Tanımlayabileceğimiz bir şey, sunucu için çalışmamızı basitleştirecek olan kısıtlamalar kümesidir. Ve eğer kartlarımızı doğru oynarsak, sağlam bir arayüz sağlayan bir hizmetle sonuçlanabiliriz, böylece sonunda, oyunu hiç etkilemeden daha az kısıtlama sağlamak için uygulamayı genişletmemize ve hatta değiştirmemize izin verebiliriz.

  • Parti başına sadece bir oda olacak.
    Alt grupların oluşturulmasına izin vermeyeceğiz. Bu, partinin bölünmesine izin vermemekle el ele gider. Belki bu geliştirmeyi uyguladığımızda, alt grup ve özel sohbet odası oluşturmaya izin vermek iyi bir fikir olabilir.
  • Özel mesaj olmayacak.
    Bu tamamen basitleştirme amaçlıdır, ancak bir grup sohbeti yapmak zaten yeterince iyidir; Şu anda özel mesajlara ihtiyacımız yok. Minimum geçerli ürününüz üzerinde çalışırken, gereksiz özelliklerin tavşan deliğinden aşağı inmekten kaçınmaya çalışın; tehlikeli ve içinden çıkılması zor bir yoldur.
  • Mesajlarda ısrar etmeyeceğiz.
    Başka bir deyişle, partiden ayrılırsanız mesajları kaybedersiniz. Bu, işimizi büyük ölçüde basitleştirecek, çünkü herhangi bir veri depolama türüyle uğraşmak zorunda kalmayacağız ve eski mesajları depolamak ve kurtarmak için en iyi veri yapısına karar vermek için zaman kaybetmek zorunda kalmayacağız. Hepsi hafızada yaşayacak ve sohbet odası aktif olduğu sürece orada kalacak. Kapandığında, onlara sadece veda edeceğiz!
  • Soketler üzerinden haberleşme sağlanacaktır .
    Ne yazık ki, müşterimiz çift iletişim kanalıyla uğraşmak zorunda kalacak: oyun motoru için RESTful bir kanal ve sohbet sunucusu için bir soket. Bu, istemcinin karmaşıklığını biraz artırabilir, ancak aynı zamanda her modül için en iyi iletişim yöntemlerini kullanacaktır. (Sohbet sunucumuzda REST'i zorlamanın veya oyun sunucumuzdaki soketleri zorlamanın gerçek bir anlamı yoktur. Bu yaklaşım, iş mantığını da ele alan sunucu tarafı kodunun karmaşıklığını artıracaktır, o yüzden o tarafa odaklanalım. şimdilik.)

Sohbet sunucusu için bu kadar. Sonuçta, en azından başlangıçta karmaşık olmayacak. Kodlamaya başlama zamanı geldiğinde yapılacak daha çok şey var, ancak bu makale için fazlasıyla yeterli bilgi.

Müşteri

Bu, kodlama gerektiren son modül ve en aptal modülümüz olacak. Genel bir kural olarak, müşterilerimin aptal ve sunucularımın akıllı olmasını tercih ederim. Bu şekilde, sunucu için yeni istemciler oluşturmak çok daha kolay hale gelir.

Aynı sayfada olmamız için, burada bitirmemiz gereken yüksek seviyeli mimari var.

Tüm geliştirmenin son üst düzey mimarisi
Tüm geliştirmenin son üst düzey mimarisi (Büyük önizleme)

Basit CLI istemcimiz çok karmaşık bir şey uygulamayacaktır. Aslında, üstesinden gelmemiz gereken en karmaşık kısım gerçek kullanıcı arayüzüdür, çünkü bu metin tabanlı bir arayüzdür.

Bununla birlikte, istemci uygulamasının gerçekleştirmesi gereken işlevsellik aşağıdaki gibidir:

  1. Yeni bir oyun oluşturun .
    İşleri olabildiğince basit tutmak istediğim için bu sadece CLI arayüzü üzerinden yapılacak. Gerçek kullanıcı arayüzü yalnızca bir oyuna katıldıktan sonra kullanılacak ve bu da bizi bir sonraki noktaya getiriyor.
  2. Mevcut bir oyuna katılın .
    Bir önceki noktadan döndürülen oyunun kodu göz önüne alındığında, oyuncular bunu katılmak için kullanabilirler. Yine, bu, bir UI olmadan yapabilmeniz gereken bir şeydir, bu nedenle bu işlevsellik, metin UI'sini kullanmaya başlamak için gereken sürecin bir parçası olacaktır.
  3. Oyun tanım dosyalarını ayrıştırın .
    Bunları birazdan tartışacağız, ancak müşterinin neyi göstereceğini ve bu verileri nasıl kullanacağını bilmesi için bu dosyaları anlayabilmesi gerekir.
  4. Macera ile etkileşime geçin.
    Temel olarak, bu, oyuncuya herhangi bir zamanda açıklanan ortamla etkileşime girme yeteneği verir.
  5. Her oyuncu için bir envanter tutun .
    İstemcinin her örneği, bir bellek içi öğe listesi içerecektir. Bu liste yedeklenecek.
  6. Destek sohbeti .
    İstemci uygulamasının ayrıca sohbet sunucusuna bağlanması ve kullanıcıyı grubun sohbet odasında oturum açması gerekir.

Daha sonra müşterinin iç yapısı ve tasarımı hakkında daha fazla bilgi. Bu arada, son hazırlık kısmıyla tasarım aşamasını bitirelim: oyun dosyaları.

Oyun: JSON Dosyaları

Burası ilginçleşiyor çünkü şimdiye kadar temel mikro hizmet tanımlarını ele aldım. Bazıları REST konuşabilir ve diğerleri soketlerle çalışabilir, ancak özünde hepsi aynıdır: Onları siz tanımlarsınız, kodlarsınız ve bir hizmet sağlarlar.

Bu özel bileşen için herhangi bir şey kodlamayı planlamıyorum, ancak onu tasarlamamız gerekiyor. Temel olarak, oyunumuzu, içindeki sahneleri ve içindeki her şeyi tanımlamak için bir tür protokol uyguluyoruz.

Bunu düşünürseniz, bir metin macerası, özünde, temelde birbirine bağlı bir dizi odadır ve içlerinde etkileşime girebileceğiniz, hepsi de umut verici, düzgün bir hikaye ile birbirine bağlı "şeyler" vardır. Şimdi, motorumuz o son kısımla ilgilenmeyecek; o kısım size kalmış. Ama geri kalanı için umut var.

Şimdi, birbirine bağlı odalar kümesine geri dönersek, bu bana bir grafik gibi geliyor ve daha önce bahsettiğim mesafe veya hareket hızı kavramını da eklersek, ağırlıklı bir grafiğimiz var. Ve bu sadece, aralarındaki yolu temsil eden bir ağırlığa (veya sadece bir sayıya - ne dendiği konusunda endişelenmeyin) sahip bir dizi düğümdür. İşte bir görsel (görerek öğrenmeyi seviyorum, bu yüzden sadece resme bakın, tamam mı?):

Ağırlıklı bir grafik örneği
Ağırlıklı bir grafik örneği (Büyük önizleme)

Bu ağırlıklı bir grafik - bu kadar. Ve eminim bunu zaten çözmüşsünüzdür, ancak bütünlük adına, motorumuz hazır olduğunda size nasıl yapacağınızı göstermeme izin verin.

Macerayı kurmaya başladığınızda, haritanızı oluşturacaksınız (aşağıdaki resmin solunda gördüğünüz gibi). Ardından, resmin sağında görebileceğiniz gibi, bunu ağırlıklı bir grafiğe çevireceksiniz. Motorumuz onu alıp doğru sırayla üzerinden geçmenizi sağlayacak.

Belirli bir zindan için örnek grafik
Belirli bir zindan için örnek grafik (Büyük önizleme)

Yukarıdaki ağırlıklı grafikle, oyuncuların girişten sol kanada kadar gidememesini sağlayabiliriz. Bu ikisi arasındaki düğümlerden geçmeleri gerekecek ve bunu yapmak, bağlantılardan gelen ağırlığı kullanarak ölçebileceğimiz zaman harcayacak.

Gelelim “eğlenceli” kısma. JSON formatında grafiğin nasıl görüneceğini görelim. Burada benimle ayı; bu JSON çok fazla bilgi içerecek, ancak elimden geldiğince çoğunu gözden geçireceğim:

 { "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 } } ] } } }

Çok fazla göründüğünü biliyorum, ancak oyunun basit bir açıklamasına indirirseniz, yukarıdaki şemada gösterildiği gibi her biri birbiriyle bağlantılı altı odadan oluşan bir zindanınız var.

Senin görevin, içinden geçmek ve onu keşfetmek. Bir silah bulabileceğiniz iki farklı yer olduğunu göreceksiniz (mutfakta veya karanlık odada, sandalyeyi kırarak). Ayrıca kilitli bir kapı ile karşı karşıya kalacaksınız; yani, anahtarı bulduğunuzda (ofis benzeri odanın içinde bulunur), onu açabilir ve topladığınız silahla patronla savaşabilirsiniz.

Ya onu öldürerek kazanacaksınız ya da onun tarafından öldürülerek kaybedeceksiniz.

Şimdi tüm JSON yapısı ve üç bölümü hakkında daha ayrıntılı bir genel bakışa geçelim.

grafik

Bu, düğümler arasındaki ilişkiyi içerecektir. Temel olarak, bu bölüm doğrudan daha önce baktığımız grafiğe çevrilir.

Bu bölümün yapısı oldukça basittir. Bu, her düğümün aşağıdaki öznitelikleri içerdiği bir düğüm listesidir:

  • oyundaki diğer tüm düğümler arasında benzersiz bir şekilde düğümü tanımlayan bir kimlik;
  • kimliğin temelde insan tarafından okunabilir bir versiyonu olan bir isim;
  • diğer düğümlere bir dizi bağlantı. Bu, dört olası anahtarın varlığıyla kanıtlanır: kuzey”, güney, doğu ve batı. Sonunda bu dördünün kombinasyonlarını ekleyerek başka yönler ekleyebiliriz. Her bağlantı, ilgili düğümün kimliğini ve bu ilişkinin mesafesini (veya ağırlığını) içerir.

oyun

Bu bölüm genel ayarları ve koşulları içerecektir. Özellikle yukarıdaki örnekte bu bölüm kazanma ve kaybetme koşullarını içermektedir. Başka bir deyişle, bu iki koşulla, oyunun ne zaman bitebileceğini motora bildireceğiz.

İşleri basit tutmak için sadece iki koşul ekledim:

  • ya patronu öldürerek kazanırsın,
  • ya da öldürülerek kaybetmek.

Odalar

163 satırın çoğunun geldiği yer burasıdır ve bölümlerin en karmaşıkıdır. Maceramızdaki tüm odaları ve içindeki her şeyi burada anlatacağız.

Daha önce tanımladığımız ID'yi kullanarak her oda için bir anahtar olacak. Ve her odanın bir açıklaması, bir eşya listesi, bir çıkış (veya kapı) listesi ve oynanamaz karakterlerin (NPC'ler) bir listesi olacaktır. Bu özelliklerden zorunlu olması gereken tek şey açıklamadır, çünkü motorun ne gördüğünüzü size bildirmesi için bu gereklidir. Geri kalanlar ancak gösterilecek bir şey varsa orada olacaklar.

Şimdi bu özelliklerin oyunumuz için neler yapabileceğine bakalım.

Tanım

Bu öğe sanıldığı kadar basit değildir, çünkü bir odaya bakışınız farklı koşullara bağlı olarak değişebilir. Örneğin, ilk odanın açıklamasına bakarsanız, varsayılan olarak, elbette yanınızda yanan bir meşale olmadıkça hiçbir şey göremediğinizi fark edeceksiniz.

Bu nedenle, eşyaları toplamak ve kullanmak, oyunun diğer kısımlarını etkileyecek küresel koşulları tetikleyebilir.

Nesneler

Bunlar, bir odanın içinde bulabileceğiniz her şeyi temsil ediyor. Her öğe, grafik bölümündeki düğümlerin sahip olduğu aynı kimliği ve adı paylaşır.

Ayrıca, bu öğenin alındıktan sonra nerede saklanması gerektiğini gösteren bir "varış yeri" özelliğine de sahip olacaklar. Bu önemlidir, çünkü elinizde yalnızca bir öğe bulunabilecekken, envanterinizde istediğiniz kadar öğeye sahip olabileceksiniz.

Son olarak, bu öğelerden bazıları, oynatıcının bunlarla ne yapmaya karar verdiğine bağlı olarak diğer eylemleri veya durum güncellemelerini tetikleyebilir. Bunun bir örneği girişten yanan meşalelerdir. Bunlardan birini alırsanız, oyunda bir durum güncellemesini tetiklersiniz ve bu da oyunun size bir sonraki odanın farklı bir tanımını göstermesini sağlar.

Öğelerin, orijinal öğe yok edildiğinde (örneğin, "kırma" eylemi yoluyla) devreye giren "alt öğeleri" de olabilir. Bir öğe birkaç öğeye bölünebilir ve bu, "alt öğeler" öğesinde tanımlanır.

Esasen, bu öğe yalnızca bir dizi yeni öğedir ve bunların yaratılmasını tetikleyebilecek eylemler kümesini de içerir. Bu, temel olarak, orijinal öğe üzerinde gerçekleştirdiğiniz eylemlere dayalı olarak farklı alt öğeler oluşturma olasılığını açar.

Son olarak, bazı eşyaların bir "hasar" özelliği olacaktır. Bu nedenle, bir NPC'yi vurmak için bir öğe kullanırsanız, bu değer onlardan can çıkarmak için kullanılacaktır.

Çıkışlar

Bu sadece çıkışın yönünü ve özelliklerini gösteren bir dizi özelliktir (incelemek istemeniz durumunda bir açıklama, adı ve bazı durumlarda durumu).

Çıkışlar, öğelerden ayrı bir varlıktır çünkü motorun, durumlarına göre bunları gerçekten geçip geçemeyeceğinizi anlaması gerekir. Kilitli çıkışlar, durumlarını kilitsiz olarak nasıl değiştireceğinizi öğrenmedikçe, onları geçmenize izin vermez.

NPC'ler

Son olarak, NPC'ler başka bir listenin parçası olacak. Temel olarak, motorun her birinin nasıl davranması gerektiğini anlamak için kullanacağı istatistiklere sahip öğelerdir. Örneğimizde tanımladığımız değerler, sağlık puanları anlamına gelen “hp” ve tıpkı silahlar gibi, her vuruşun oyuncunun sağlığından çıkaracağı sayı olan “hasar” dır.

Yarattığım zindan için bu kadar. Çok fazla, evet ve gelecekte JSON dosyalarının oluşturulmasını basitleştirmek için bir tür seviye düzenleyici oluşturmayı düşünebilirim. Ama şimdilik buna gerek kalmayacak.

Henüz fark etmediyseniz, oyunumuzu böyle bir dosyada tanımlamanın asıl yararı, Super Nintendo döneminde kartuşlarda yaptığınız gibi JSON dosyalarını değiştirebilmemizdir. Sadece yeni bir dosya yükleyin ve yeni bir maceraya başlayın. Kolay!

Kapanış Düşünceleri

Buraya kadar okuduğunuz için teşekkürler. Umarım bir fikri hayata geçirmek için geçirdiğim tasarım sürecini beğenmişsinizdir. Yine de, ilerledikçe bunu uydurduğumu unutmayın, böylece daha sonra bugün tanımladığımız bir şeyin işe yaramayacağını anlayabiliriz, bu durumda geriye dönüp düzeltmemiz gerekecek.

Burada sunulan fikirleri geliştirmenin ve harika bir motor yapmanın bir sürü yolu olduğundan eminim. Ancak bu, herkes için sıkıcı hale getirmeden bir makaleye koyabileceğimden çok daha fazla kelime gerektirir, bu yüzden şimdilik burada bırakacağız.

Bu Serinin Diğer Parçaları

  • Bölüm 2: Oyun Motoru Sunucu Tasarımı
  • Bölüm 3: Terminal İstemcisi Oluşturma
  • 4. Bölüm: Oyunumuza Sohbet Eklemek