Sıfırdan Gerçek Zamanlı Çok Kullanıcılı Bir Oyun Nasıl Oluşturulur

Yayınlanan: 2022-03-10
Kısa özet ↬ Bu makale, gerçek zamanlı oyun Autowuzzler'ı oluşturmanın ardındaki süreci, teknik kararları ve öğrenilen dersleri vurgulamaktadır. Colyseus ile oyun durumunu gerçek zamanlı olarak birden fazla istemci arasında nasıl paylaşacağınızı, Matter.js ile fizik hesaplamaları yapmayı, Supabase.io'da veri depolamayı ve SvelteKit ile ön uç oluşturmayı öğrenin.

Pandemi sürerken, birlikte çalıştığım aniden uzaktaki ekip giderek daha fazla langırttan yoksun hale geldi. Uzak bir ortamda nasıl langırt oynanacağını düşündüm, ancak langırt kurallarını bir ekranda yeniden oluşturmanın çok eğlenceli olmayacağı açıktı.

Eğlenceli olan , oyuncak arabaları kullanarak topa tekme atmaktır - 2 yaşındaki çocuğumla oynarken bunu fark ettim. Aynı gece, Autowuzzler olacak bir oyun için ilk prototipi oluşturmaya başladım.

Fikir basit : Oyuncular sanal oyuncak arabaları langırt masasına benzeyen yukarıdan aşağıya bir arenada yönlendirir. 10 gol atan ilk takım kazanır.

Tabii ki, futbol oynamak için araba kullanma fikri benzersiz değil, ancak iki ana fikir Autowuzzler'ı diğerlerinden ayırmalıdır: Fiziksel bir langırt masasında oynamanın görünüşünü ve verdiği hissi yeniden oluşturmak istedim ve öyle olduğundan emin olmak istedim. arkadaşlarınızı veya takım arkadaşlarınızı hızlı bir gündelik oyuna davet etmek mümkün olduğunca kolay.

Bu yazıda, hangi araçları ve çerçeveleri seçtiğimi, Autowuzzler'in yaratılmasının arkasındaki süreci anlatacağım ve birkaç uygulama detayını ve öğrendiğim dersleri paylaşacağım.

Bir langırt masası arka planı, iki takım halinde altı araba ve bir top gösteren oyun kullanıcı arayüzü.
İki takımda aynı anda altı oyuncuyla Autowuzzler (beta). (Büyük önizleme)

İlk Çalışan (Korkunç) Prototip

İlk prototip, açık kaynaklı oyun motoru Phaser.js kullanılarak, çoğunlukla dahil edilen fizik motoru için ve zaten onunla biraz deneyimim olduğu için inşa edildi. Oyun aşaması bir Next.js uygulamasına yerleştirildi, çünkü zaten Next.js hakkında sağlam bir anlayışa sahiptim ve esas olarak oyuna odaklanmak istiyordum.

Oyunun gerçek zamanlı olarak birden fazla oyuncuyu desteklemesi gerektiğinden, Express'i WebSockets aracısı olarak kullandım. Yine de işin zorlaştığı yer burası.

Phaser oyununda istemci üzerinde fizik hesaplamaları yapıldığından, basit ama açıkça kusurlu bir mantık seçtim: İlk bağlanan istemci, tüm oyun nesneleri için fizik hesaplamalarını yapma, sonuçları ekspres sunucuya gönderme konusunda şüpheli ayrıcalığa sahipti, bu da güncellenmiş pozisyonları, açıları ve kuvvetleri diğer oyuncunun istemcilerine geri yayınladı. Diğer istemciler daha sonra değişiklikleri oyun nesnelerine uygular.

Bu, ilk oyuncunun fiziği gerçek zamanlı olarak görmesine neden oldu (sonuçta bu, tarayıcılarında yerel olarak oluyor), diğer tüm oyuncular en az 30 milisaniyenin gerisinde kalıyordu (seçtiğim yayın hızı). ) veya - ilk oyuncunun ağ bağlantısı yavaşsa - çok daha kötü.

Bu size zayıf mimari gibi geliyorsa - kesinlikle haklısınız. Ancak, oyunun gerçekten eğlenceli olup olmadığını anlamak için hızlı bir şekilde oynanabilir bir şey elde etmek adına bu gerçeği kabul ettim.

Fikri Doğrulayın, Prototipi Dökün

Uygulama ne kadar kusurlu olsa da, ilk test sürüşüne arkadaşlarınızı davet etmek için yeterince oynanabilirdi. Geri bildirim çok olumluydu ve asıl endişe - şaşırtıcı olmayan bir şekilde - gerçek zamanlı performanstı. Diğer içsel problemler, ilk oyuncunun (unutmayın, her şeyden sorumlu olan) oyundan ayrıldığı durumu içeriyordu - kim devralmalı? Bu noktada sadece bir oyun odası vardı, bu yüzden herkes aynı oyuna katılabilirdi. Phaser.js kitaplığının tanıttığı paket boyutu da beni biraz endişelendirdi.

Prototipi atmanın ve yeni bir kurulum ve net bir hedefle başlamanın zamanı gelmişti.

Proje Kurulumu

Açıkça, "ilk müşteri her şeyi yönetir" yaklaşımının , oyun durumunun sunucuda yaşadığı bir çözümle değiştirilmesi gerekiyordu. Araştırmamda, kulağa iş için mükemmel bir araç gibi gelen Colyseus ile karşılaştım.

Oyunun diğer ana yapı taşları için seçtim:

  • Matter.js, Node'da çalıştığı ve Autowuzzler'ın tam bir oyun çerçevesi gerektirmediği için Phaser.js yerine bir fizik motoru olarak.
  • SvelteKit, Next.js yerine bir uygulama çerçevesi olarak kullanıldı, çünkü o sırada herkese açık betaya girdi. (Ayrıca: Svelte ile çalışmayı seviyorum.)
  • Kullanıcı tarafından oluşturulan oyun PIN'lerini saklamak için Supabase.io.

Bu yapı taşlarına daha ayrıntılı bakalım.

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

Colyseus ile Senkronize, Merkezi Oyun Durumu

Colyseus, Node.js ve Express'e dayalı çok oyunculu bir oyun çerçevesidir. Özünde şunları sağlar:

  • Yetkili bir şekilde istemciler arasında durumu senkronize etme;
  • Yalnızca değiştirilen verileri göndererek WebSockets kullanarak verimli gerçek zamanlı iletişim;
  • Çok odalı kurulumlar;
  • JavaScript, Unity, Defold Engine, Haxe, Cocos Creator, Construct3 için istemci kitaplıkları;
  • Yaşam döngüsü kancaları, örneğin oda oluşturulur, kullanıcı birleşir, kullanıcı ayrılır ve daha fazlası;
  • Odadaki tüm kullanıcılara veya tek bir kullanıcıya yayın mesajları olarak mesaj gönderme;
  • Yerleşik bir izleme paneli ve yük testi aracı.

Not : Colyseus belgeleri, bir npm init ​​betiği ve bir örnek deposu sağlayarak, bir barebone Colyseus sunucusuna başlamayı kolaylaştırır.

Şema Oluşturma

Bir Colyseus uygulamasının ana varlığı, tek bir oda örneği ve tüm oyun nesneleri için durumu tutan oyun odasıdır. Autowuzzler söz konusu olduğunda, bu, aşağıdakileri içeren bir oyun oturumudur:

  • iki takım,
  • sınırlı sayıda oyuncu,
  • bir top.

İstemciler arasında senkronize edilmesi gereken oyun nesnelerinin tüm özellikleri için bir şema tanımlanmalıdır. Örneğin, topun senkronize olmasını istiyoruz ve bu nedenle top için bir şema oluşturmamız gerekiyor:

 class Ball extends Schema { constructor() { super(); this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; } } defineTypes(Ball, { x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number" });

Yukarıdaki örnekte, Colyseus tarafından sağlanan şema sınıfını genişleten yeni bir sınıf yaratılmıştır; yapıcıda, tüm özellikler bir başlangıç ​​değeri alır. Topun konumu ve hareketi beş özellik kullanılarak tanımlanır: x , y , angle , velocityX, velocityY Y . Ek olarak, her bir özelliğin türlerini belirtmemiz gerekiyor. Bu örnek, JavaScript sözdizimini kullanır, ancak biraz daha kompakt TypeScript sözdizimini de kullanabilirsiniz.

Özellik türleri, ilkel türler olabilir:

  • string
  • boolean
  • number (ayrıca daha verimli tamsayı ve kayan nokta türleri)

veya karmaşık türler:

  • ArraySchema (JavaScript'teki Array'e benzer)
  • MapSchema (JavaScript'teki Haritaya benzer)
  • SetSchema (JavaScript'teki Set'e benzer)
  • CollectionSchema (ArraySchema'ya benzer, ancak dizinler üzerinde denetimi yoktur)

Yukarıdaki Ball sınıfı, number türünün beş özelliğine sahiptir: koordinatları ( x , y ), mevcut angle ve hız vektörü ( velocityX X , velocityY Y ).

Oyuncular için şema benzerdir, ancak bir Player örneği oluştururken sağlanması gereken oyuncunun adını ve takım numarasını saklamak için birkaç özellik daha içerir:

 class Player extends Schema { constructor(teamNumber) { super(); this.name = ""; this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; this.teamNumber = teamNumber; } } defineTypes(Player, { name: "string", x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number", angularVelocity: "number", teamNumber: "number", });

Son olarak, Autowuzzler Room şeması önceden tanımlanmış sınıfları birbirine bağlar: Bir oda örneğinin birden fazla ekibi vardır (bir ArraySchema'da depolanır). Ayrıca tek bir top içerir, bu nedenle RoomSchema'nın yapıcısında yeni bir Ball örneği oluştururuz. Oyuncular, kimliklerini kullanarak hızlı erişim için bir MapSchema'da saklanır.

 class RoomSchema extends Schema { constructor() { super(); this.teams = new ArraySchema(); this.ball = new Ball(); this.players = new MapSchema(); } } defineTypes(RoomSchema, { teams: [Team], // an Array of Team ball: Ball, // a single Ball instance players: { map: Player } // a Map of Players });
Not : Team sınıfının tanımı atlanmıştır.

Çoklu Oda Kurulumu (“Eşleştirme”)

Geçerli bir oyun PIN'ine sahip olan herkes Autowuzzler oyununa katılabilir. Colyseus sunucumuz, ilk oyuncu katılır katılmaz her oyun oturumu için yeni bir Oda örneği oluşturur ve son oyuncu odayı terk ettiğinde odayı atar.

Oyuncuları istedikleri oyun odasına atama işlemine “eşleştirme” denir. Colyseus, yeni bir oda tanımlarken filterBy yöntemini kullanarak kurulumu çok kolaylaştırır:

 gameServer.define("autowuzzler", AutowuzzlerRoom).filterBy(['gamePIN']);

Artık oyuna aynı oyun gamePIN ile katılan tüm oyuncular (nasıl “katılacağını” daha sonra göreceğiz) aynı oyun odasına girecekler! Herhangi bir durum güncellemesi ve diğer yayın mesajları, aynı odadaki oyuncularla sınırlıdır.

Bir Colyseus Uygulamasında Fizik

Colyseus, yetkili bir oyun sunucusuyla hızlı bir şekilde çalışmaya başlamak için kullanıma hazır pek çok şey sağlar, ancak fizik dahil olmak üzere gerçek oyun mekaniğini oluşturmayı geliştiriciye bırakır. Prototipte kullandığım Phaser.js, tarayıcı olmayan bir ortamda çalıştırılamaz, ancak Phaser.js'nin entegre fizik motoru Matter.js, Node.js üzerinde çalışabilir.

Matter.js ile boyutu ve yerçekimi gibi belirli fiziksel özelliklere sahip bir fizik dünyası tanımlarsınız. Kütle, çarpışmalar, sürtünmeli hareket vb. dahil olmak üzere (simüle edilmiş) fizik yasalarına bağlı kalarak birbirleriyle etkileşime giren ilkel fizik nesneleri oluşturmak için çeşitli yöntemler sağlar. Gerçek dünyada yaptığınız gibi, güç uygulayarak nesneleri hareket ettirebilirsiniz .

Autowuzzler oyununun kalbinde bir Matter.js “dünyası” yer alır; arabaların ne kadar hızlı hareket ettiğini, topun ne kadar zıplaması gerektiğini, hedeflerin nerede olduğunu ve biri gol atarsa ​​ne olacağını tanımlar.

 let ball = Bodies.circle( ballInitialXPosition, ballInitialYPosition, radius, { render: { sprite: { texture: '/assets/ball.png', } }, friction: 0.002, restitution: 0.8 } ); World.add(this.engine.world, [ball]);

Matter.js'de sahneye bir "top" oyun nesnesi eklemek için basitleştirilmiş kod.

Kurallar bir kez tanımlandıktan sonra, Matter.js bir ekrana bir şey vererek veya göstermeden çalışabilir . Autowuzzler için, fizik dünya kodunu hem sunucu hem de istemci için yeniden kullanmak için bu özelliği kullanıyorum - birkaç önemli farkla:

Sunucudaki fizik dünyası:

  • Colyseus aracılığıyla kullanıcı girdisini (arabayı yönlendirmek için klavye olayları) alır ve oyun nesnesine (kullanıcının arabası) uygun kuvveti uygular;
  • çarpışmaları tespit etmek de dahil olmak üzere tüm nesneler (oyuncular ve top) için tüm fizik hesaplamalarını yapar;
  • her oyun nesnesi için güncellenmiş durumu Colyseus'a iletir, o da bunu istemcilere yayınlar;
  • Colyseus sunucumuz tarafından tetiklenen her 16,6 milisaniyede (= 60 kare/saniye) güncellenir.

İstemcide fizik dünyası:

  • oyun nesnelerini doğrudan manipüle etmez;
  • Colyseus'tan her oyun nesnesi için güncellenmiş durumu alır;
  • güncellenmiş durumu aldıktan sonra konum, hız ve açıdaki değişiklikleri uygular;
  • kullanıcı girdisini (arabayı yönlendirmek için klavye olayları) Colyseus'a gönderir;
  • oyun sprite'larını yükler ve fizik dünyasını bir tuval öğesi üzerine çizmek için bir oluşturucu kullanır;
  • çarpışma algılamayı atlar (nesneler için isSensor seçeneğini kullanarak);
  • ideal olarak 60 fps'de requestAnimationFrame kullanarak güncellemeler.
İki ana bloğu gösteren diyagram: Colyseus Sunucu Uygulaması ve SvelteKit Uygulaması. Colyseus Sunucu Uygulaması, Autowuzzler Odası bloğunu içerir, SvelteKit Uygulaması, Colyseus İstemci bloğunu içerir. Her iki ana blok da Fizik Dünyası (Matter.js) adlı bir bloğu paylaşır.
Autowuzzler mimarisinin ana mantıksal birimleri: Fizik Dünyası, Colyseus sunucusu ve SvelteKit istemci uygulaması arasında paylaşılır. (Büyük önizleme)

Artık sunucuda gerçekleşen tüm sihirle, istemci yalnızca girdiyi ele alıyor ve sunucudan aldığı durumu ekrana çiziyor. Bir istisna dışında:

İstemci Üzerinde Enterpolasyon

İstemcide aynı Matter.js fizik dünyasını yeniden kullandığımızdan, deneyimli performansı basit bir numara ile iyileştirebiliriz. Yalnızca bir oyun nesnesinin konumunu güncellemek yerine , nesnenin hızını da senkronize ediyoruz. Bu şekilde, sunucudan bir sonraki güncelleme normalden daha uzun sürse bile nesne yörüngesinde hareket etmeye devam eder. Böylece nesneleri A konumundan B konumuna ayrı adımlarla hareket ettirmek yerine, konumlarını değiştirip belirli bir yönde hareket etmelerini sağlıyoruz.

Yaşam döngüsü

Autowuzzler Room sınıfı, bir Colyseus odasının farklı evreleriyle ilgili mantığın işlendiği yerdir. Colyseus birkaç yaşam döngüsü yöntemi sağlar:

  • onCreate : yeni bir oda oluşturulduğunda (genellikle ilk istemci bağlandığında);
  • onAuth : odaya girişe izin vermek veya girişi reddetmek için bir yetkilendirme kancası olarak;
  • onJoin : bir istemci odaya bağlandığında;
  • onLeave : bir istemcinin odayla bağlantısı kesildiğinde;
  • onDispose : oda atıldığında.

Autowuzzler odası, oluşturulduğu anda ( onCreate ) fizik dünyasının yeni bir örneğini yaratır ("Bir Colyseus Uygulamasında Fizik" bölümüne bakın) ve bir istemci bağlandığında ( onJoin ) dünyaya bir oyuncu ekler. Ardından, setSimulationInterval yöntemini (ana oyun döngümüz) kullanarak fizik dünyasını saniyede 60 kez (her 16,6 milisaniyede bir) günceller:

 // deltaTime is roughly 16.6 milliseconds this.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));

Fizik nesneleri Colyseus nesnelerinden bağımsızdır, bu da bize aynı oyun nesnesinin (top gibi) iki permütasyonu ile , yani fizik dünyasındaki bir nesne ve senkronize edilebilen bir Colyseus nesnesi ile bırakır.

Fiziksel nesne değişir değişmez, güncellenmiş özelliklerinin Colyseus nesnesine geri uygulanması gerekir. Bunu Matter.js'nin afterUpdate olayını dinleyerek ve buradan değerleri ayarlayarak başarabiliriz:

 Events.on(this.engine, "afterUpdate", () => { // apply the x position of the physics ball object back to the colyseus ball object this.state.ball.x = this.physicsWorld.ball.position.x; // ... all other ball properties // loop over all physics players and apply their properties back to colyseus players objects })

İlgilenmemiz gereken nesnelerin bir kopyası daha var: kullanıcıya yönelik oyundaki oyun nesneleri .

Bir oyun nesnesinin üç sürümünü gösteren diyagram: Colyseus Şema Nesneleri, Matter.js Fizik Nesneleri, İstemci Matter.js Fizik Nesneleri. Matter.js, nesnenin Colyseus sürümünü günceller, Colyseus, İstemci Matter.js Fizik Nesnesi ile senkronize olur.
Autowuzzler, her fizik nesnesinin üç kopyasını, bir yetkili sürümü (Colyseus nesnesi), Matter.js fizik dünyasındaki bir sürümü ve istemcide bir sürümü tutar. (Büyük önizleme)

İstemci Tarafı Uygulaması

Artık sunucuda birden fazla oda için oyun durumunun senkronizasyonunu ve fizik hesaplamalarını yöneten bir uygulamamız olduğuna göre , web sitesini ve gerçek oyun arayüzünü oluşturmaya odaklanalım. Autowuzzler ön yüzü aşağıdaki sorumluluklara sahiptir:

  • kullanıcıların ayrı odalara erişmek için oyun PIN'leri oluşturmasına ve paylaşmasına olanak tanır;
  • kalıcılık için oluşturulan oyun PIN'lerini bir Supabase veritabanına gönderir;
  • oyuncuların oyun PIN'ini girmeleri için isteğe bağlı bir "Oyuna katıl" sayfası sağlar;
  • bir oyuncu bir oyuna katıldığında oyun PIN'lerini doğrular;
  • asıl oyunu paylaşılabilir (yani benzersiz) bir URL'de barındırır ve işler;
  • Colyseus sunucusuna bağlanır ve durum güncellemelerini işler;
  • bir açılış ("pazarlama") sayfası sağlar.

Bu görevlerin uygulanması için aşağıdaki nedenlerle Next.js yerine SvelteKit'i seçtim:

Neden SvelteKit?

Neolightsout'u kurduğumdan beri Svelte kullanarak başka bir uygulama geliştirmek istiyordum. SvelteKit (Svelte'nin resmi uygulama çerçevesi) genel beta sürümüne geçtiğinde, onunla Autowuzzler'ı oluşturmaya ve yeni bir beta kullanmanın getirdiği tüm sorunları kabul etmeye karar verdim - Svelte kullanma sevinci bunu açıkça telafi ediyor.

Bu temel özellikler , oyun ön yüzünün gerçek uygulaması için Next.js yerine SvelteKit'i seçmeme neden oldu:

  • Svelte bir UI çerçevesi ve derleyicidir ve bu nedenle istemci çalışma zamanı olmadan minimum kod gönderir;
  • Svelte, etkileyici bir şablonlama diline ve bileşen sistemine sahiptir (kişisel tercih);
  • Svelte, hazır global mağazalar, geçişler ve animasyonlar içerir, bu da şu anlama gelir: global bir durum yönetimi araç takımı ve bir animasyon kitaplığı seçerken karar vermekten yorulmaz;
  • Svelte, tek dosya bileşenlerinde kapsamlı CSS'yi destekler;
  • SvelteKit, bir API oluşturmak için SSR'yi, basit ama esnek dosya tabanlı yönlendirmeyi ve sunucu tarafı yollarını destekler;
  • SvelteKit, her sayfanın sunucuda kod çalıştırmasına izin verir, örneğin sayfayı oluşturmak için kullanılan verileri getirmek için;
  • Rotalar arasında paylaşılan düzenler;
  • SvelteKit, sunucusuz bir ortamda çalıştırılabilir.

Oyun PIN'leri Oluşturma ve Saklama

Bir kullanıcının oyunu oynamaya başlayabilmesi için öncelikle bir oyun PIN'i oluşturması gerekir. PIN'i başkalarıyla paylaşarak hepsi aynı oyun odasına erişebilirler.

Autowuzzler web sitesinin 751428 oyun PIN'ini ve oyun PIN'ini ve URL'sini kopyalama ve paylaşma seçeneklerini gösteren yeni bir oyun başlat bölümünün ekran görüntüsü.
Oluşturulan oyun PIN kodunu kopyalayarak yeni bir oyun başlatın veya oyun odasının doğrudan bağlantısını paylaşın. (Büyük önizleme)

Bu, Sveltes onMount işleviyle bağlantılı olarak SvelteKits sunucu tarafı uç noktaları için harika bir kullanım örneğidir: /api/createcode bitiş noktası bir oyun PIN'i oluşturur, bunu bir Supabase.io veritabanında saklar ve yanıt olarak oyun PIN'ini verir . Bu yanıt, "oluştur" sayfasının sayfa bileşeni takılır takılmaz alınır:

Üç bölümü gösteren diyagram: Sayfa oluştur, kod bitiş noktası oluştur ve Supabase.io. Sayfa oluştur, onMount işlevinde uç noktayı getirir, uç nokta bir oyun PIN'i oluşturur, bunu Supabase.io'da saklar ve oyun PIN'i ile yanıt verir. Oluştur sayfası daha sonra oyun PIN'ini görüntüler.
Oyun PIN'leri uç noktada oluşturulur, bir Supabase.io veritabanında saklanır ve “Oluştur” sayfasında görüntülenir. (Büyük önizleme)

Supabase.io ile Oyun PIN'lerini Kaydetme

Supabase.io, Firebase'e açık kaynaklı bir alternatiftir. Supabase, bir PostgreSQL veritabanı oluşturmayı ve ona istemci kitaplıklarından biri veya REST aracılığıyla erişmeyi çok kolaylaştırır.

JavaScript istemcisi için createClient işlevini içe aktarıyoruz ve veritabanını oluştururken aldığımız supabase_url ve supabase_key parametrelerini kullanarak onu çalıştırıyoruz. createcode uç noktasına yapılan her çağrıda oluşturulan oyun PIN'ini saklamak için tek yapmamız gereken bu basit insert sorgusunu çalıştırmaktır:

 import { createClient } from '@supabase/supabase-js' const database = createClient( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_KEY ); const { data, error } = await database .from("games") .insert([{ code: 123456 }]);

Not : supabase_url ve supabase_key bir .env dosyasında saklanır. SvelteKit'in kalbindeki oluşturma aracı olan Vite nedeniyle, ortam değişkenlerini SvelteKit'te erişilebilir kılmak için VITE_ ile önek eklemek gerekir.

Oyuna Erişim

Bir Autowuzzler oyununa katılmayı bir bağlantıyı takip etmek kadar kolay hale getirmek istedim. Bu nedenle, her oyun odasının önceden oluşturulmuş oyun PIN'ine dayalı olarak kendi URL'sine sahip olması gerekiyordu, örneğin https://autowuzzler.com/play/12345.

SvelteKit'te, sayfa dosyasını adlandırırken rotanın dinamik kısımlarını köşeli parantez içine alarak dinamik rota parametrelerine sahip sayfalar oluşturulur: client/src/routes/play/[gamePIN].svelte . gamePIN parametresinin değeri daha sonra sayfa bileşeninde kullanılabilir hale gelecektir (ayrıntılar için SvelteKit belgelerine bakın). play rotasında, Colyseus sunucusuna bağlanmamız, ekrana işlemek için fizik dünyasını başlatmamız, oyun nesnelerindeki güncellemeleri işlememiz, klavye girişini dinlememiz ve puan gibi diğer kullanıcı arayüzünü görüntülememiz vb.

Colyseus'a Bağlanma ve Durumu Güncelleme

Colyseus istemci kitaplığı, bir istemciyi bir Colyseus sunucusuna bağlamamızı sağlar. İlk olarak, Colyseus sunucusuna işaret ederek yeni bir Colyseus.Client oluşturalım ( ws://localhost:2567 geliştirme aşamasında). Ardından daha önce seçtiğimiz isimle ( autowuzzler ) ve route parametresinden gamePIN ile odaya katılın. gamePIN parametresi, kullanıcının doğru oda örneğine katıldığından emin olur (yukarıdaki "eşleştirme" konusuna bakın).

 let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN });

SvelteKit sayfaları başlangıçta sunucuda oluşturduğundan, bu kodun yalnızca sayfa yüklendikten sonra istemcide çalıştığından emin olmamız gerekir. Yine, bu kullanım durumu için onMount yaşam döngüsü işlevini kullanıyoruz. (React'e aşina iseniz, onMount , boş bir bağımlılık dizisine sahip useEffect kancasına benzer.)

 onMount(async () => { let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN }); })

Artık Colyseus oyun sunucusuna bağlı olduğumuza göre, oyun nesnelerimizdeki değişiklikleri dinlemeye başlayabiliriz.

İşte odaya katılan ( onAdd ) bir oyuncunun nasıl dinleneceğine ve bu oynatıcıya ardışık durum güncellemeleri almasına ilişkin bir örnek:

 this.room.state.players.onAdd = (player, key) => { console.log(`Player has been added with sessionId: ${key}`); // add player entity to the game world this.world.createPlayer(key, player.teamNumber); // listen for changes to this player player.onChange = (changes) => { changes.forEach(({ field, value }) => { this.world.updatePlayer(key, field, value); // see below }); }; };

Fizik dünyasının updatePlayer yönteminde, Colyseus' onChange değiştirilen tüm özelliklerin bir kümesini sunduğu için özellikleri tek tek güncelliyoruz.

Not : Oyun nesneleri yalnızca Colyseus sunucusu aracılığıyla dolaylı olarak değiştirildiğinden, bu işlev yalnızca fizik dünyasının istemci sürümünde çalışır.

 updatePlayer(sessionId, field, value) { // get the player physics object by its sessionId let player = this.world.players.get(sessionId); // exit if not found if (!player) return; // apply changes to the properties switch (field) { case "angle": Body.setAngle(player, value); break; case "x": Body.setPosition(player, { x: value, y: player.position.y }); break; case "y": Body.setPosition(player, { x: player.position.x, y: value }); break; // set velocityX, velocityY, angularVelocity ... } }

Aynı prosedür diğer oyun nesneleri (top ve takımlar) için de geçerlidir: değişikliklerini dinleyin ve değiştirilen değerleri müşterinin fizik dünyasına uygulayın.

Şimdiye kadar hiçbir nesne hareket etmiyor çünkü hala klavye girişini dinlememiz ve sunucuya göndermemiz gerekiyor . Her keydown olayında olayları doğrudan göndermek yerine, şu anda basılmış tuşların bir haritasını tutuyor ve olayları 50 ms'lik bir döngüde Colyseus sunucusuna gönderiyoruz. Bu şekilde, aynı anda birden fazla tuşa basılmasını destekleyebilir ve tuşa basılı tutulduğunda ilk ve ardışık keydown olaylarından sonra meydana gelen duraklamayı azaltabiliriz:

 let keys = {}; const keyDown = e => { keys[e.key] = true; }; const keyUp = e => { keys[e.key] = false; }; document.addEventListener('keydown', keyDown); document.addEventListener('keyup', keyUp); let loop = () => { if (keys["ArrowLeft"]) { this.room.send("move", { direction: "left" }); } else if (keys["ArrowRight"]) { this.room.send("move", { direction: "right" }); } if (keys["ArrowUp"]) { this.room.send("move", { direction: "up" }); } else if (keys["ArrowDown"]) { this.room.send("move", { direction: "down" }); } // next iteration requestAnimationFrame(() => { setTimeout(loop, 50); }); } // start loop setTimeout(loop, 50);

Şimdi döngü tamamlandı: tuş vuruşlarını dinleyin, sunucudaki fizik dünyasını değiştirmek için ilgili komutları Colyseus sunucusuna gönderin. Colyseus sunucusu daha sonra yeni fiziksel özellikleri tüm oyun nesnelerine uygular ve oyunun kullanıcıya dönük örneğini güncellemek için verileri istemciye geri yayar.

Küçük Sıkıntılar

Geriye dönüp bakıldığında, kimsenin-bana-ama-birinin-söylemediği- kategorisinde akla gelen iki şey:

  • Fizik motorlarının nasıl çalıştığını iyi anlamak faydalıdır. Fizik özelliklerini ve kısıtlamalarını ince ayar yapmak için önemli miktarda zaman harcadım. Daha önce Phaser.js ve Matter.js ile küçük bir oyun yapmış olsam da, nesneleri hayal ettiğim şekilde hareket ettirmek için çok fazla deneme yanılma oldu.
  • Gerçek zamanlı zordur - özellikle fizik tabanlı oyunlarda. Küçük gecikmeler, deneyimi önemli ölçüde kötüleştirir ve Colyseus ile istemciler arasında durumu senkronize etmek harika çalışırken, hesaplama ve iletim gecikmelerini ortadan kaldıramaz.

SvelteKit ile Alıntılar ve Uyarılar

SvelteKit'i beta-fırından yeni çıktığında kullandığım için, dikkat çekmek istediğim birkaç sorun ve uyarı vardı:

  • Ortam değişkenlerinin SvelteKit'te kullanılabilmesi için VITE_ ile önek eklenmesi gerektiğini anlamak biraz zaman aldı. Bu artık SSS'de düzgün bir şekilde belgelenmiştir.
  • Supabase'i kullanmak için, package.json'ın hem dependencies hem de devDependencies listesine Supabase'i eklemem gerekiyordu. Bunun artık böyle olmadığına inanıyorum.
  • SvelteKits load işlevi hem sunucuda hem de istemcide çalışır!
  • Tam etkin modül değiştirmeyi etkinleştirmek için (koruma durumu dahil), sayfa bileşenlerinize manuel olarak <!-- @hmr:keep-all --> bir yorum satırı eklemeniz gerekir. Daha fazla ayrıntı için SSS'ye bakın.

Diğer birçok çerçeve de çok uygun olurdu, ancak bu proje için SvelteKit'i seçtiğim için hiç pişman değilim. İstemci uygulaması üzerinde çok verimli bir şekilde çalışmamı sağladı - çoğunlukla Svelte'nin kendisi çok etkileyici olduğu ve çok sayıda standart kodu atladığı için ve ayrıca Svelte'de animasyonlar, geçişler, kapsamlı CSS ve küresel mağazalar gibi şeyler olduğu için. SvelteKit , ihtiyacım olan tüm yapı taşlarını (SSR, yönlendirme, sunucu yolları) sağladı ve hala beta sürümünde olmasına rağmen çok kararlı ve hızlı hissettirdi.

Dağıtım ve Barındırma

Başlangıçta, Colyseus (Düğüm) sunucusunu bir Heroku örneğinde barındırdım ve WebSockets ve CORS'u çalıştırmak için çok zaman harcadım. Görünen o ki, küçük (ücretsiz) bir Heroku dyno'nun performansı, gerçek zamanlı bir kullanım durumu için yeterli değil. Daha sonra Colyseus uygulamasını Linode'daki küçük bir sunucuya taşıdım. İstemci tarafı uygulama, SvelteKits Adapter-netlify aracılığıyla Netlify tarafından dağıtılır ve burada barındırılır. Burada sürpriz yok: Netlify harika çalıştı!

Çözüm

Fikri doğrulamak için gerçekten basit bir prototiple başlamak, projenin izlenmeye değer olup olmadığını ve oyunun teknik zorluklarının nerede olduğunu anlamamda bana çok yardımcı oldu. Son uygulamada, Colyseus, birden çok odaya dağıtılmış birden çok istemci arasında gerçek zamanlı olarak senkronizasyon durumunun tüm ağır yükünün üstesinden geldi. Şemayı doğru bir şekilde nasıl tanımlayacağınızı çözdükten sonra, Colyseus ile gerçek zamanlı çok kullanıcılı bir uygulamanın ne kadar hızlı oluşturulabileceği etkileyicidir . Colyseus'un yerleşik izleme paneli, herhangi bir senkronizasyon sorununun giderilmesine yardımcı olur.

Bu kurulumu karmaşıklaştıran, oyunun fizik katmanıydı, çünkü bakımı gereken her fizikle ilgili oyun nesnesinin ek bir kopyasını getirdi. Oyun PIN'lerini SvelteKit uygulamasından Supabase.io'da saklamak çok kolaydı. Geriye dönüp baktığımda, oyun PIN'lerini depolamak için bir SQLite veritabanı kullanabilirdim, ancak yeni şeyler denemek yan projeler oluştururken eğlencenin yarısıdır.

Son olarak, oyunun ön yüzünü oluşturmak için SvelteKit'i kullanmak, hızlı hareket etmemi sağladı - ve ara sıra yüzümde sevinç gülümsemesi ile.

Şimdi devam edin ve arkadaşlarınızı bir Autowuzzler turuna davet edin!

Smashing Magazine'de Daha Fazla Okuma

  • Jhey Tompkins, “Bir Köstebek Oyunu Oluşturarak React ile Başlayın”
  • Alvin Wan “Gerçek Zamanlı Çok Oyunculu Sanal Gerçeklik Oyunu Nasıl Oluşturulur”
  • “Node.js'de Çok Oyunculu Metin Macera Motoru Yazma,” Fernando Doglio
  • "Mobil Web Tasarımının Geleceği: Video Oyunu Tasarımı ve Hikaye Anlatımı" Suzanne Scacca
  • Alvin Wan “Sanal Gerçeklikte Sonsuz Bir Koşucu Oyunu Nasıl İnşa Edilir?”