Node.js'de Çok Oyunculu Metin Macera Motoru Yazma: Terminal İstemcisi Oluşturma (Bölüm 3)

Yayınlanan: 2022-03-10
Hızlı özet ↬ Serinin bu üçüncü bölümü, 2. bölümde oluşturulan oyun motoru için metin tabanlı bir istemci eklemeye odaklanacak. Fernando Doglio, size bir metin oluşturmayı göstererek temel mimari tasarımı, araç seçimini ve kod vurgularını açıklar. Node.js yardımıyla kullanıcı arayüzü tabanlı.

Önce size bunun gibi bir projeyi nasıl tanımlayacağınızı gösterdim ve size mimarinin temellerini ve oyun motorunun arkasındaki mekanikleri verdim. Ardından, size motorun temel uygulamasını gösterdim — JSON tanımlı bir dünyada gezinmenizi sağlayan temel bir REST API.

Bugün size API'miz için Node.js'den başka bir şey kullanarak eski tarz bir metin istemcisinin nasıl oluşturulacağını göstereceğim.

Bu Serinin Diğer Parçaları

  • Bölüm 1: Giriş
  • Bölüm 2: Oyun Motoru Sunucu Tasarımı
  • 4. Bölüm: Oyunumuza Sohbet Eklemek

Orijinal Tasarımın İncelenmesi

UI için temel bir tel kafes önerdiğimde, ekranda dört bölüm önerdim:

(Büyük önizleme)

Teoride bu doğru görünse de, oyun komutları ve metin mesajları gönderme arasında geçiş yapmanın acı verici olacağı gerçeğini gözden kaçırdım, bu nedenle oyuncularımızın manuel olarak geçiş yapmasını sağlamak yerine, komut ayrıştırıcımızın biz olup olmadığını ayırt edebildiğinden emin olmamızı sağlayacağız. Oyun ya da arkadaşlarımızla iletişim kurmaya çalışıyoruz.

Yani, ekranımızda dört bölüm yerine şimdi üç tane olacak:

(Büyük önizleme)

Bu, son oyun istemcisinin gerçek bir ekran görüntüsüdür. Altta tek bir ortak giriş kutusu ile sol tarafta oyun ekranını ve sağda sohbeti görebilirsiniz. Kullandığımız modül, renkleri ve bazı temel efektleri özelleştirmemize izin veriyor. Bu kodu Github'dan klonlayabilecek ve görünüm ve his ile istediğinizi yapabileceksiniz.

Yine de bir uyarı: Yukarıdaki ekran görüntüsü sohbetin uygulamanın bir parçası olarak çalıştığını gösterse de, bu makaleyi projeyi kurmaya ve dinamik bir metin-UI tabanlı uygulama oluşturabileceğimiz bir çerçeve tanımlamaya odaklayacağız. Bu serinin sonraki ve son bölümünde sohbet desteği eklemeye odaklanacağız.

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

İhtiyacımız Olan Araçlar

Node.js ile CLI araçları oluşturmamıza izin veren birçok kitaplık olmasına rağmen, metin tabanlı bir kullanıcı arayüzü eklemek, evcilleştirmek için tamamen farklı bir canavardır. Özellikle, tam olarak istediğim şeyi yapmama izin verecek tek bir (çok eksiksiz, aklınızda bulunsun) kitaplık bulabildim: Blessed.

Bu kitaplık çok güçlüdür ve bu proje için kullanmayacağımız birçok özellik sağlar (gölge oluşturma, sürükle ve bırak ve diğerleri gibi). Temel olarak, Node.js bağlamaları olmayan tüm ncurses kitaplığını (geliştiricilerin metin tabanlı UI'ler oluşturmasına izin veren bir C kitaplığı) yeniden uygular ve bunu doğrudan JavaScript'te yapar; bu yüzden, eğer mecbur kalırsak, dahili kodunu çok iyi kontrol edebilirdik (kesinlikle gerekmedikçe tavsiye etmeyeceğim bir şey).

Blessed için belgeler oldukça kapsamlı olmasına rağmen, esas olarak sağlanan her bir yöntem hakkında bireysel ayrıntılardan oluşur (bu yöntemlerin birlikte nasıl kullanılacağını açıklayan öğreticilere sahip olmanın aksine) ve her yerde örnekler yoktur, bu nedenle kazmak zor olabilir. belirli bir yöntemin nasıl çalıştığını anlamanız gerekiyorsa. Bununla birlikte, bir kez anladığınızda, her şey aynı şekilde çalışır, bu büyük bir artı çünkü her kitaplık ve hatta dil (size bakıyorum, PHP) tutarlı bir sözdizimine sahip değildir.

Ancak belgeler bir yana; Bu kitaplığın en büyük artısı, JSON seçeneklerine göre çalışmasıdır. Örneğin ekranın sağ üst köşesine bir kutu çizmek isteseydiniz şöyle bir şey yapardınız:

 var box = blessed.box({ top: '0', right: '0', width: '50%', height: '50%', content: 'Hello {bold}world{/bold}!', tags: true, border: { type: 'line' }, style: { fg: 'white', bg: 'magenta', border: { fg: '#f0f0f0' }, hover: { bg: 'green' } } });

Tahmin edebileceğiniz gibi, kutunun diğer yönleri de burada tanımlanmıştır (boyutu gibi), bu da uçbirimin boyutuna, kenarlık türüne ve renklere göre mükemmel bir şekilde dinamik olabilir - hatta üzerine gelme olayları için bile. Bir noktada ön uç geliştirme yaptıysanız, ikisi arasında çok fazla örtüşme bulacaksınız.

Burada belirtmeye çalıştığım nokta, kutunun temsili ile ilgili her şeyin box yöntemine iletilen JSON nesnesi aracılığıyla yapılandırılmasıdır. Bu benim için mükemmel çünkü bu içeriği kolayca bir yapılandırma dosyasına çıkarabilir ve onu okuyabilen ve ekranda hangi öğelerin çizileceğine karar verebilen bir iş mantığı oluşturabilirim. En önemlisi, çizildikten sonra nasıl görüneceklerine dair bir fikir edinmemize yardımcı olacak.

Bu, bu modülün tüm UI yönünün temeli olacaktır ( bir saniye içinde daha fazlası! ).

Modül Mimarisi

Bu modülün ana mimarisi, tamamen göstereceğimiz UI widget'larına dayanmaktadır. Bu widget'ların bir grubu bir ekran olarak kabul edilir ve tüm bu ekranlar tek bir JSON dosyasında ( /config klasöründe bulabileceğiniz) tanımlanır.

Bu dosyanın 250'den fazla satırı var, bu yüzden burada göstermenin bir anlamı yok. Dosyanın tamamına çevrimiçi olarak bakabilirsiniz, ancak küçük bir pasajı şöyle görünür:

 "screens": { "main-options": { "file": "./main-options.js", "elements": { "username-request": { "type": "input-prompt", "params": { "position": { "top": "0%", "left": "0%", "width": "100%", "height": "25%" }, "content": "Input your username: ", "inputOnFocus": true, "border": { "type": "line" }, "style": { "fg": "white", "bg": "blue", "border": { "fg": "#f0f0f0" }, "hover": { "bg": "green" } } } }, "options": { "type": "window", "params": { "position": { "top": "25%", "left": "0%", "width": "100%", "height": "50%" }, "content": "Please select an option: \n1. Join an existing game.\n2. Create a new game", "border": { "type": "line" }, "style": { //... } } }, "input": { "type": "input", "handlerPath": "../lib/main-options-handler", //... } } }

“Ekranlar” öğesi, uygulama içindeki ekranların listesini içerecektir. Her ekran bir widget listesi içerir (birazdan bahsedeceğim) ve her widget'ın kutsamaya özel tanımı ve ilgili işleyici dosyaları (uygun olduğunda) vardır.

Her "params" öğesinin (belirli bir pencere öğesi içindeki) daha önce gördüğümüz yöntemlerle beklenen gerçek parametre kümesini nasıl temsil ettiğini görebilirsiniz. Orada tanımlanan anahtarların geri kalanı, ne tür widget'ların oluşturulacağı ve davranışları hakkında bağlam sağlamaya yardımcı olur.

Birkaç ilgi çekici nokta:

Ekran İşleyiciler

Her ekran öğesi, o ekranla ilişkili koda başvuran dosya özelliğine sahiptir. Bu kod, bir init yöntemine sahip olması gereken bir nesneden başka bir şey değildir (o belirli ekran için başlatma mantığı bunun içinde gerçekleşir). Özellikle, ana UI motoru, her ekranın bu init yöntemini çağıracak ve bu da, ihtiyaç duyabileceği mantığı başlatmaktan sorumlu olacaktır (yani, giriş kutuları olaylarını ayarlamak).

Uygulamanın, oyuncudan yepyeni bir oyun başlatmak veya mevcut bir oyuna katılmak için bir seçenek seçmesini istediği ana ekranın kodu aşağıdadır:

 const logger = require("../utils/logger") module.exports = { init: function(elements, UI) { this.elements = elements this.UI = UI this. this.setInput() }, moveToIDRequest: function(handler) { return this.UI.loadScreen('id-requests', (err, ) => { }) }, createNewGame: function(handler) { handler.createNewGame(this.UI.gamestate.APIKEY, (err, gameData) => { this.UI.gamestate.gameID = gameData._id handler.joinGame(this.UI.gamestate, (err) => { return this.UI.loadScreen('main-ui', { flashmessage: "You've joined game " + this.UI.gamestate.gameID + " successfully" }, (err, ) => { }) }) }) }, setInput: function() { let handler = require(this.elements["input"].meta.handlerPath) let input = this.elements["input"].obj let usernameRequest = this.elements['username-request'].obj let usernameRequestMeta = this.elements['username-request'].meta let question = usernameRequestMeta.params.content.trim() usernameRequest.setValue(question) this.UI.renderScreen() let validOptions = { 1: this.moveToIDRequest.bind(this), 2: this.createNewGame.bind(this) } usernameRequest.on('submit', (username) => { logger.info("Username:" +username) logger.info("Playername: " + username.replace(question, '')) this.UI.gamestate.playername = username.replace(question, '') input.focus() input.on('submit', (data) => { let command = input.getValue() if(!validOptions[+command]) { this.UI.setUpAlert("Invalid option: " + command) return this.UI.renderScreen() } return validOptions[+command](handler) }) }) return input } }

Gördüğünüz gibi, init yöntemi, kullanıcı girişini işlemek için doğru geri aramayı temel olarak yapılandıran setupInput yöntemini çağırır. Bu geri arama, kullanıcının girişine göre ne yapılacağına karar verme mantığını tutar (1 veya 2).

Widget İşleyicileri

Widget'lardan bazıları (genellikle giriş widget'ları), söz konusu bileşenin arkasındaki mantığı içeren dosyaya başvuruda bulunan bir handlerPath özelliğine sahiptir. Bu, önceki ekran işleyici ile aynı değil. Bunlar, UI bileşenlerini o kadar umursamıyor. Bunun yerine, kullanıcı arayüzü ile harici hizmetlerle (oyun motorunun API'si gibi) etkileşim kurmak için kullandığımız kitaplık arasındaki bağlantı mantığını idare ederler.

Widget Türleri

Widget'ların JSON tanımına yapılan bir başka küçük ekleme de türleridir. Blessed'ın onlar için tanımladığı isimlerle gitmek yerine, davranışları söz konusu olduğunda bana daha fazla yer açmak için yenilerini yaratıyorum. Sonuçta, bir pencere parçacığı her zaman "sadece bilgileri göstermeyebilir" veya bir giriş kutusu her zaman aynı şekilde çalışmayabilir.

Bu çoğunlukla önleyici bir hareketti, sadece gelecekte ihtiyacım olursa bu yeteneğe sahip olmamı sağlamak için, ancak birazdan göreceğiniz gibi, zaten o kadar farklı türde bileşen kullanmıyorum.

Çoklu Ekranlar

Ana ekran yukarıdaki ekran görüntüsünde size gösterdiğim ekran olsa da, oyuncu adınız veya yepyeni bir oyun oturumu oluşturup oluşturmadığınız, hatta mevcut bir oturuma katılıp katılmadığınız gibi şeyleri istemek için oyun birkaç başka ekrana ihtiyaç duyuyor. Bunu ele alma şeklim, yine tüm bu ekranların aynı JSON dosyasındaki tanımıydı. Ve bir ekrandan diğerine geçmek için ekran işleyici dosyalarının içindeki mantığı kullanıyoruz.

Bunu basitçe aşağıdaki kod satırını kullanarak yapabiliriz:

 this.UI.loadScreen('main-ui', (err ) => { if(err) this.UI.setUpAlert(err) })

Size UI özelliği hakkında birazdan daha fazla ayrıntı göstereceğim, ancak ekranı yeniden oluşturmak için bu loadScreen yöntemini kullanıyorum ve parametre olarak iletilen dizeyi kullanarak JSON dosyasından doğru bileşenleri seçiyorum. Çok basit.

Kod Örnekleri

Şimdi bu makalenin et ve patateslerine bakmanın zamanı geldi: kod örnekleri. Sadece içindeki küçük taşlar olduğunu düşündüğüm şeyi vurgulayacağım, ancak kaynak kodunun tamamına istediğiniz zaman doğrudan depodan göz atabilirsiniz.

Kullanıcı Arayüzü Otomatik Oluşturmak için Yapılandırma Dosyalarını Kullanma

Bunun bir kısmını zaten ele aldım, ancak bu jeneratörün arkasındaki ayrıntıları keşfetmeye değer olduğunu düşünüyorum. Arkasındaki ana fikir ( /ui klasörünün içindeki index.js dosyası), Blessed nesnesinin etrafındaki bir sarmalayıcı olmasıdır. Ve içindeki en ilginç yöntem loadScreen yöntemidir.

Bu yöntem, belirli bir ekran için yapılandırmayı (yapılandırma modülü aracılığıyla) alır ve her öğenin türüne göre doğru parçacıkları oluşturmaya çalışarak içeriğinden geçer.

 loadScreen: function(sname, extras, done) { if(typeof extras == "function") { done = extras } let screen = config.get('screens.' + sname) let screenElems = {} if(this.screenElements.length > 0) { //remove previous screen this.screenElements.map( e => e.detach()) this.screen.realloc() } Object.keys(screen.elements).forEach( eName => { let elemObj = null let element = screen.elements[eName] if(element.type == 'window') { elemObj = this.setUpWindow(element) } if(element.type == 'input') { elemObj = this.setUpInputBox(element) } if(element.type == 'input-prompt') { elemObj = this.setUpInputBox(element) } screenElems[eName] = { meta: element, obj: elemObj } }) if(typeof extras === 'object' && extras.flashmessage) { this.setUpAlert(extras.flashmessage) } this.renderScreen() let logicPath = require(screen.file) logicPath.init(screenElems, this) done() },

Gördüğünüz gibi, kod biraz uzun ama arkasındaki mantık basit:

  1. Geçerli belirli ekran için yapılandırmayı yükler;
  2. Önceden var olan tüm widget'ları temizler;
  3. Her parçacığın üzerinden geçer ve onu somutlaştırır;
  4. Bir flaş mesaj olarak fazladan bir uyarı iletildiyse (ki bu temelde Web Dev'den çaldığım ve bir sonraki yenilemeye kadar ekranda gösterilecek bir mesaj ayarladığınız bir kavramdır);
  5. Gerçek ekranı işleyin;
  6. Ve son olarak, ekran işleyicisine ihtiyaç duyun ve onun “init” yöntemini yürütün.

Bu kadar! Yöntemlerin geri kalanına göz atabilirsiniz - bunlar çoğunlukla bireysel widget'larla ve bunların nasıl oluşturulacağıyla ilgilidir.

Kullanıcı Arayüzü ve İş Mantığı Arasındaki İletişim

Her ne kadar büyük ölçekte olsa da, kullanıcı arayüzü, arka uç ve sohbet sunucusunun tümü biraz katmanlı bir iletişime sahiptir; ön ucun kendisi, saf UI öğelerinin bu belirli proje içindeki temel mantığı temsil eden bir dizi işlevle etkileşime girdiği en az iki katmanlı bir iç mimariye ihtiyaç duyar.

Aşağıdaki şema, oluşturmakta olduğumuz metin istemcisinin iç mimarisini göstermektedir:

(Büyük önizleme)

Biraz daha açıklayayım. Yukarıda bahsettiğim gibi loadScreenMethod , widget'ların UI sunumlarını oluşturacaktır (bunlar Blessed nesneleridir). Ancak bunlar, temel olayları ayarladığımız ekran mantığı nesnesinin bir parçası olarak bulunurlar (giriş kutuları için onSubmit gibi).

Size pratik bir örnek vermeme izin verin. UI istemcisini başlatırken gördüğünüz ilk ekran:

(Büyük önizleme)

Bu ekranda üç bölüm vardır:

  1. Kullanıcı adı isteği,
  2. Menü seçenekleri / bilgileri,
  3. Menü seçenekleri için giriş ekranı.

Temel olarak, yapmak istediğimiz kullanıcı adını istemek ve ardından iki seçenekten birini seçmelerini istemek (ya yepyeni bir oyun başlatmak ya da mevcut bir oyuna katılmak).

Bununla ilgilenen kod şudur:

 module.exports = { init: function(elements, UI) { this.elements = elements this.UI = UI this. this.setInput() }, moveToIDRequest: function(handler) { return this.UI.loadScreen('id-requests', (err, ) => { }) }, createNewGame: function(handler) { handler.createNewGame(this.UI.gamestate.APIKEY, (err, gameData) => { this.UI.gamestate.gameID = gameData._id handler.joinGame(this.UI.gamestate, (err) => { return this.UI.loadScreen('main-ui', { flashmessage: "You've joined game " + this.UI.gamestate.gameID + " successfully" }, (err, ) => { }) }) }) }, setInput: function() { let handler = require(this.elements["input"].meta.handlerPath) let input = this.elements["input"].obj let usernameRequest = this.elements['username-request'].obj let usernameRequestMeta = this.elements['username-request'].meta let question = usernameRequestMeta.params.content.trim() usernameRequest.setValue(question) this.UI.renderScreen() let validOptions = { 1: this.moveToIDRequest.bind(this), 2: this.createNewGame.bind(this) } usernameRequest.on('submit', (username) => { logger.info("Username:" +username) logger.info("Playername: " + username.replace(question, '')) this.UI.gamestate.playername = username.replace(question, '') input.focus() input.on('submit', (data) => { let command = input.getValue() if(!validOptions[+command]) { this.UI.setUpAlert("Invalid option: " + command) return this.UI.renderScreen() } return validOptions[+command](handler) }) }) return input } }

Bunun çok fazla kod olduğunu biliyorum, ancak sadece init yöntemine odaklanın. Yaptığı son şey, doğru olayları doğru giriş kutularına eklemeye özen gösteren setInput yöntemini çağırmaktır.

Bu nedenle, bu satırlarla:

 let handler = require(this.elements["input"].meta.handlerPath) let input = this.elements["input"].obj let usernameRequest = this.elements['username-request'].obj let usernameRequestMeta = this.elements['username-request'].meta let question = usernameRequestMeta.params.content.trim()

Daha sonra submit olaylarını ayarlayabilmemiz için Blessed nesnelerine erişiyor ve referanslarını alıyoruz. Bu yüzden kullanıcı adını gönderdikten sonra, odağı ikinci giriş kutusuna değiştiriyoruz (tam anlamıyla input.focus() ile).

Menüden hangi seçeneği seçtiğimize bağlı olarak, yöntemlerden birini çağırıyoruz:

  • createNewGame : ilişkili işleyicisiyle etkileşime girerek yeni bir oyun yaratır;
  • moveToIDRequest : katılmak için oyun kimliğini istemekten sorumlu bir sonraki ekranı oluşturur.

Oyun Motoru ile İletişim

Son olarak (ve yukarıdaki örneği izleyerek), 2'ye basarsanız, createNewGame yönteminin işleyicinin createNewGame ve ardından joinGame (oyunu oluşturduktan hemen sonra katılma) yöntemlerini kullandığını fark edeceksiniz.

Bu yöntemlerin her ikisi de Game Engine'in API'si ile etkileşimi basitleştirmeye yöneliktir. İşte bu ekranın işleyicisinin kodu:

 const request = require("request"), config = require("config"), apiClient = require("./apiClient") let API = config.get("api") module.exports = { joinGame: function(apikey, gameId, cb) { apiClient.joinGame(apikey, gameId, cb) }, createNewGame: function(apikey, cb) { request.post(API.url + API.endpoints.games + "?apikey=" + apikey, { //creating game body: { cartridgeid: config.get("app.game.cartdrigename") }, json: true }, (err, resp, body) => { cb(null, body) }) } }

Orada bu davranışı ele almanın iki farklı yolunu görüyorsunuz. İlk yöntem aslında apiClient sınıfını kullanır ve bu sınıf yine GameEngine ile etkileşimleri başka bir soyutlama katmanına sarar.

İkinci yöntem, eylemi doğrudan doğru yüke sahip doğru URL'ye bir POST isteği göndererek gerçekleştirir. Daha sonra süslü bir şey yapılmaz; sadece yanıtın gövdesini UI mantığına geri gönderiyoruz.

Not : Bu istemcinin kaynak kodunun tam sürümüyle ilgileniyorsanız, buradan kontrol edebilirsiniz.

Son sözler

Metin maceramız için metin tabanlı istemci için bu kadar. kapattım:

  • Bir istemci uygulaması nasıl yapılandırılır;
  • Sunum katmanını oluşturmak için temel teknoloji olarak Blessed'ı nasıl kullandım;
  • Karmaşık bir istemciden gelen arka uç hizmetleriyle etkileşim nasıl yapılandırılır;
  • Ve umarım, mevcut tam depo ile.

Kullanıcı arayüzü tam olarak orijinal sürümdeki gibi görünmese de amacını yerine getiriyor. Umarım, bu makale size böyle bir girişimin nasıl tasarlanacağı konusunda bir fikir vermiştir ve gelecekte kendiniz denemeye meyillisiniz. Blessed kesinlikle çok güçlü bir araçtır, ancak nasıl kullanılacağını ve belgelerinde nasıl gezinileceğini öğrenirken sabırlı olmanız gerekir.

Sonraki ve son bölümde, hem arka uçta hem de bu metin istemcisi için sohbet sunucusunu nasıl eklediğimi ele alacağım.

Bir sonrakinde görüşmek üzere!

Bu Serinin Diğer Parçaları

  • Bölüm 1: Giriş
  • Bölüm 2: Oyun Motoru Sunucu Tasarımı
  • 4. Bölüm: Oyunumuza Sohbet Eklemek