Cara Membangun Game Multi-Pengguna Real-Time Dari Awal

Diterbitkan: 2022-03-10
Ringkasan cepat Artikel ini menyoroti proses, keputusan teknis, dan pelajaran di balik pembuatan game waktu nyata Autowuzzler. Pelajari cara berbagi status game di beberapa klien secara real-time dengan Colyseus, lakukan perhitungan fisika dengan Matter.js, simpan data di Supabase.io, dan bangun front-end dengan SvelteKit.

Ketika pandemi berlanjut, tim yang tiba-tiba jauh dari tempat saya bekerja menjadi semakin kehilangan bola sepak. Saya berpikir tentang cara bermain sepak bola dalam pengaturan jarak jauh, tetapi jelas bahwa merekonstruksi aturan sepak bola di layar tidak akan menyenangkan.

Yang menyenangkan adalah menendang bola menggunakan mobil mainan — sebuah realisasi yang dibuat saat saya bermain dengan anak saya yang berusia 2 tahun. Pada malam yang sama saya mulai membangun prototipe pertama untuk game yang akan menjadi Autowuzzler .

Idenya sederhana : pemain mengarahkan mobil mainan virtual di arena top-down yang menyerupai meja foosball. Tim pertama yang mencetak 10 gol menang.

Tentu saja, ide menggunakan mobil untuk bermain sepak bola tidaklah unik, tetapi dua ide utama harus membedakan Autowuzzler : Saya ingin merekonstruksi beberapa tampilan dan nuansa bermain di meja foosball fisik, dan saya ingin memastikannya. semudah mungkin untuk mengundang teman atau rekan satu tim ke permainan kasual cepat.

Dalam artikel ini, saya akan menjelaskan proses di balik pembuatan Autowuzzler , alat dan kerangka kerja mana yang saya pilih, dan membagikan beberapa detail implementasi dan pelajaran yang saya pelajari.

Antarmuka pengguna game menampilkan latar belakang meja foosball, enam mobil dalam dua tim dan satu bola.
Autowuzzler (beta) dengan enam pemain bersamaan dalam dua tim. (Pratinjau besar)

Prototipe Kerja Pertama (Mengerikan)

Prototipe pertama dibuat menggunakan mesin permainan sumber terbuka Phaser.js, sebagian besar untuk mesin fisika yang disertakan dan karena saya sudah memiliki pengalaman dengannya. Tahap permainan disematkan dalam aplikasi Next.js, sekali lagi karena saya sudah memiliki pemahaman yang kuat tentang Next.js dan ingin fokus terutama pada permainan.

Karena permainan perlu mendukung banyak pemain secara real-time , saya menggunakan Express sebagai broker WebSockets. Di sinilah hal itu menjadi rumit, meskipun.

Karena perhitungan fisika dilakukan pada klien dalam game Phaser, saya memilih logika sederhana, namun jelas cacat: Klien pertama yang terhubung memiliki hak istimewa yang meragukan untuk melakukan perhitungan fisika untuk semua objek game, mengirimkan hasilnya ke server ekspres, yang pada gilirannya menyiarkan posisi, sudut, dan kekuatan yang diperbarui kembali ke klien pemain lain. Klien lain kemudian akan menerapkan perubahan ke objek game.

Hal ini menyebabkan situasi di mana pemain pertama dapat melihat fisika terjadi secara real-time (ini terjadi secara lokal di browser mereka), sementara semua pemain lain tertinggal setidaknya 30 milidetik (kecepatan siaran yang saya pilih ), atau — jika koneksi jaringan pemain pertama lambat — jauh lebih buruk.

Jika ini terdengar seperti arsitektur yang buruk bagi Anda — Anda benar sekali. Namun, saya menerima fakta ini demi mendapatkan sesuatu yang dapat dimainkan dengan cepat untuk mengetahui apakah game tersebut benar-benar menyenangkan untuk dimainkan.

Validasi Idenya, Buang Prototipenya

Meskipun implementasinya cacat, itu cukup dapat dimainkan untuk mengundang teman-teman untuk test drive pertama. Umpan balik sangat positif , dengan perhatian utama — tidak mengherankan — kinerja waktu nyata. Masalah inheren lainnya termasuk situasi ketika pemain pertama (ingat, yang bertanggung jawab atas segalanya ) meninggalkan permainan — siapa yang harus mengambil alih? Pada titik ini hanya ada satu ruang permainan, jadi siapa pun akan bergabung dalam permainan yang sama. Saya juga agak khawatir dengan ukuran bundel yang diperkenalkan perpustakaan Phaser.js.

Sudah waktunya untuk membuang prototipe dan mulai dengan pengaturan baru dan tujuan yang jelas.

Pengaturan Proyek

Jelas, pendekatan "klien pertama mengatur semua" perlu diganti dengan solusi di mana status permainan hidup di server . Dalam penelitian saya, saya menemukan Colyseus, yang terdengar seperti alat yang sempurna untuk pekerjaan itu.

Untuk blok bangunan utama lainnya dari gim ini, saya memilih:

  • Matter.js sebagai mesin fisika bukan Phaser.js karena berjalan di Node dan Autowuzzler tidak memerlukan kerangka permainan penuh.
  • SvelteKit sebagai kerangka kerja aplikasi, bukan Next.js, karena baru saja masuk ke beta publik saat itu. (Selain: Saya suka bekerja dengan Svelte.)
  • Supabase.io untuk menyimpan PIN game yang dibuat pengguna.

Mari kita lihat blok bangunan tersebut secara lebih rinci.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Status Game Tersinkronisasi dan Terpusat Dengan Colyseus

Colyseus adalah kerangka permainan multipemain berdasarkan Node.js dan Express. Pada intinya, ini menyediakan:

  • Menyinkronkan status di seluruh klien dengan cara yang otoritatif;
  • Komunikasi real-time yang efisien menggunakan WebSockets dengan mengirimkan data yang diubah saja;
  • Pengaturan multi-ruangan;
  • Pustaka klien untuk JavaScript, Unity, Defold Engine, Haxe, Cocos Creator, Construct3;
  • Kait siklus hidup, misalnya ruang dibuat, pengguna bergabung, pengguna keluar, dan banyak lagi;
  • Mengirim pesan, baik sebagai pesan siaran ke semua pengguna di ruang, atau ke satu pengguna;
  • Panel pemantauan dan alat uji beban bawaan.

Catatan : Dokumen Colyseus memudahkan untuk memulai server Colyseus barebone dengan menyediakan skrip npm init dan repositori contoh.

Membuat Skema

Entitas utama aplikasi Colyseus adalah ruang permainan, yang menyimpan status untuk satu contoh ruang dan semua objek permainannya. Dalam kasus Autowuzzler , ini adalah sesi permainan dengan:

  • dua tim,
  • jumlah pemain yang terbatas,
  • satu bola.

Skema perlu ditentukan untuk semua properti objek game yang harus disinkronkan di seluruh klien . Misalnya, kami ingin bola disinkronkan, jadi kami perlu membuat skema untuk bola:

 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" });

Dalam contoh di atas, kelas baru yang memperluas kelas skema yang disediakan oleh Colyseus dibuat; di konstruktor, semua properti menerima nilai awal. Posisi dan pergerakan bola dijelaskan menggunakan lima sifat: x , y , angle , velocityX, velocityY . Selain itu, kita perlu menentukan jenis setiap properti . Contoh ini menggunakan sintaks JavaScript, tetapi Anda juga dapat menggunakan sintaks TypeScript yang sedikit lebih ringkas.

Tipe properti dapat berupa tipe primitif:

  • string
  • boolean
  • number (serta tipe integer dan float yang lebih efisien)

atau tipe kompleks:

  • ArraySchema (mirip dengan Array di JavaScript)
  • MapSchema (mirip dengan Peta di JavaScript)
  • SetSchema (mirip dengan Set di JavaScript)
  • CollectionSchema (mirip dengan ArraySchema, tetapi tanpa kontrol atas indeks)

Kelas Ball di atas memiliki lima sifat number tipe : koordinatnya ( x , y ), angle arusnya dan vektor kecepatan ( velocityX , velocityY ).

Skema untuk pemain serupa, tetapi mencakup beberapa properti lagi untuk menyimpan nama pemain dan nomor tim, yang perlu disediakan saat membuat instance Pemain:

 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", });

Terakhir, skema untuk Room Autowuzzler menghubungkan kelas yang telah ditentukan sebelumnya: Satu instance ruang memiliki banyak tim (disimpan dalam ArraySchema). Itu juga berisi satu bola, oleh karena itu kami membuat instance Ball baru di konstruktor RoomSchema. Pemain disimpan dalam MapSchema untuk pengambilan cepat menggunakan ID mereka.

 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 });
Catatan : Definisi kelas Team dihilangkan.

Pengaturan Multi-Kamar ("Mencocokkan")

Siapa pun dapat bergabung dengan game Autowuzzler jika mereka memiliki PIN game yang valid. Server Colyseus kami membuat instance Kamar baru untuk setiap sesi permainan segera setelah pemain pertama bergabung dan membuang ruangan saat pemain terakhir meninggalkannya.

Proses menugaskan pemain ke ruang permainan yang mereka inginkan disebut “match-making”. Colyseus membuatnya sangat mudah diatur dengan menggunakan metode filterBy saat menentukan ruangan baru:

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

Sekarang, setiap pemain yang bergabung dengan game dengan gamePIN game yang sama (kita akan melihat bagaimana "bergabung" nanti) akan berakhir di ruang permainan yang sama! Setiap pembaruan status dan pesan siaran lainnya dibatasi untuk pemain di ruangan yang sama.

Fisika Dalam Aplikasi Colyseus

Colyseus menyediakan banyak hal untuk bangun dan berjalan dengan cepat dengan server game yang otoritatif, tetapi menyerahkannya kepada pengembang untuk membuat mekanika game yang sebenarnya — termasuk fisika. Phaser.js, yang saya gunakan dalam prototipe, tidak dapat dijalankan di lingkungan non-browser, tetapi mesin fisika terintegrasi Phaser.js, Matter.js dapat berjalan di Node.js.

Dengan Matter.js, Anda mendefinisikan dunia fisika dengan sifat fisik tertentu seperti ukuran dan gravitasinya. Ini menyediakan beberapa metode untuk membuat objek fisika primitif yang berinteraksi satu sama lain dengan mengikuti (simulasi) hukum fisika, termasuk massa, tumbukan, gerakan dengan gesekan, dan sebagainya. Anda dapat memindahkan objek dengan menerapkan kekuatan — seperti yang Anda lakukan di dunia nyata.

"Dunia" Matter.js berada di jantung game Autowuzzler ; itu menentukan seberapa cepat mobil bergerak, seberapa melenting bola seharusnya, di mana tujuan berada, dan apa yang terjadi jika seseorang menembakkan gol.

 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]);

Kode sederhana untuk menambahkan objek permainan "bola" ke panggung di Matter.js.

Setelah aturan ditentukan, Matter.js dapat berjalan dengan atau tanpa benar-benar merender sesuatu ke layar. Untuk Autowuzzler , saya menggunakan fitur ini untuk menggunakan kembali kode dunia fisika untuk server dan klien — dengan beberapa perbedaan utama:

Dunia fisika di server :

  • menerima input pengguna (acara keyboard untuk menyetir mobil) melalui Colyseus dan menerapkan gaya yang sesuai pada objek game (mobil pengguna);
  • melakukan semua perhitungan fisika untuk semua objek (pemain dan bola), termasuk mendeteksi tabrakan;
  • mengomunikasikan status terbaru untuk setiap objek game kembali ke Colyseus, yang pada gilirannya menyiarkannya ke klien;
  • diperbarui setiap 16,6 milidetik (= 60 bingkai per detik), dipicu oleh server Colyseus kami.

Dunia fisika pada klien :

  • tidak memanipulasi objek game secara langsung;
  • menerima status terbaru untuk setiap objek game dari Colyseus;
  • menerapkan perubahan posisi, kecepatan, dan sudut setelah menerima status yang diperbarui;
  • mengirimkan input pengguna (acara keyboard untuk menyetir mobil) ke Colyseus;
  • memuat sprite game dan menggunakan penyaji untuk menggambar dunia fisika ke elemen kanvas;
  • melewatkan deteksi tabrakan (menggunakan opsi isSensor untuk objek);
  • pembaruan menggunakan requestAnimationFrame, idealnya pada 60 fps.
Diagram yang menunjukkan dua blok utama: Aplikasi Server Colyseus dan Aplikasi SvelteKit. Aplikasi Server Colyseus berisi blok Ruang Autowuzzler, Aplikasi SvelteKit berisi blok Klien Colyseus. Kedua blok utama berbagi blok bernama Physics World (Matter.js)
Unit logis utama dari arsitektur Autowuzzler: Dunia Fisika dibagi antara server Colyseus dan aplikasi klien SvelteKit. (Pratinjau besar)

Sekarang, dengan semua keajaiban yang terjadi di server, klien hanya menangani input dan menggambar status yang diterimanya dari server ke layar. Dengan satu pengecualian:

Interpolasi Pada Klien

Karena kami menggunakan kembali dunia fisika Matter.js yang sama pada klien, kami dapat meningkatkan kinerja berpengalaman dengan trik sederhana. Daripada hanya memperbarui posisi objek game, kami juga menyinkronkan kecepatan objek . Dengan cara ini, objek terus bergerak pada lintasannya meskipun pembaruan berikutnya dari server membutuhkan waktu lebih lama dari biasanya. Jadi, daripada memindahkan objek dalam langkah-langkah diskrit dari posisi A ke posisi B, kita mengubah posisinya dan membuatnya bergerak ke arah tertentu.

Lingkaran kehidupan

Kelas Room Autowuzzler adalah tempat logika yang berkaitan dengan fase berbeda dari ruang Colyseus ditangani. Colyseus menyediakan beberapa metode siklus hidup:

  • onCreate : saat ruangan baru dibuat (biasanya saat klien pertama terhubung);
  • onAuth : sebagai pengait otorisasi untuk mengizinkan atau menolak masuk ke ruangan;
  • onJoin : ketika klien terhubung ke ruangan;
  • onLeave : ketika klien terputus dari ruangan;
  • onDispose : saat ruangan dibuang.

Ruang Autowuzzler membuat instance baru dari dunia fisika (lihat bagian "Fisika Dalam Aplikasi Colyseus") segera setelah dibuat ( onCreate ) dan menambahkan pemain ke dunia saat klien terhubung ( onJoin ). Ini kemudian memperbarui dunia fisika 60 kali per detik (setiap 16,6 milidetik) menggunakan metode setSimulationInterval (loop permainan utama kami):

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

Objek fisika tidak tergantung pada objek Colyseus, yang memberi kita dua permutasi dari objek game yang sama (seperti bola), yaitu objek di dunia fisika dan objek Colyseus yang dapat disinkronkan.

Segera setelah objek fisik berubah, propertinya yang diperbarui perlu diterapkan kembali ke objek Colyseus. Kita dapat mencapainya dengan mendengarkan event afterUpdate dan menyetel nilainya dari sana:

 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 })

Ada satu salinan lagi dari objek yang perlu kita tangani: objek game di game yang menghadap pengguna .

Diagram yang menunjukkan tiga versi objek game: Objek Skema Colyseus, Objek Fisika Matter.js, Objek Fisika Matter.js Klien. Matter.js memperbarui versi objek Colyseus, Colyseus menyinkronkan ke Objek Fisika Matter.js Klien.
Autowuzzler memelihara tiga salinan dari setiap objek fisika, satu versi otoritatif (objek Colyseus), versi di dunia fisika Matter.js dan versi di klien. (Pratinjau besar)

Aplikasi Sisi Klien

Sekarang kita memiliki aplikasi di server yang menangani sinkronisasi status game untuk beberapa ruangan serta perhitungan fisika, mari fokus membangun situs web dan antarmuka game yang sebenarnya . Frontend Autowuzzler memiliki tanggung jawab berikut:

  • memungkinkan pengguna untuk membuat dan berbagi PIN game untuk mengakses kamar individu;
  • mengirimkan PIN game yang dibuat ke database Supabase untuk persistensi;
  • menyediakan halaman "Bergabung dengan game" opsional bagi pemain untuk memasukkan PIN game;
  • memvalidasi PIN game saat pemain bergabung dengan game;
  • menghosting dan membuat game yang sebenarnya pada URL yang dapat dibagikan (yaitu unik);
  • terhubung ke server Colyseus dan menangani pembaruan status;
  • menyediakan halaman arahan (“pemasaran”).

Untuk implementasi tugas tersebut, saya memilih SvelteKit daripada Next.js karena alasan berikut:

Mengapa SvelteKit?

Saya sudah lama ingin mengembangkan aplikasi lain menggunakan Svelte sejak saya membangun neolightsout. Ketika SvelteKit (kerangka aplikasi resmi untuk Svelte) masuk ke beta publik, saya memutuskan untuk membangun Autowuzzler dengannya dan menerima sakit kepala apa pun yang datang dengan menggunakan beta baru — kegembiraan menggunakan Svelte jelas menebusnya.

Fitur-fitur utama ini membuat saya memilih SvelteKit daripada Next.js untuk implementasi aktual dari frontend game:

  • Svelte adalah kerangka kerja UI dan kompiler dan karenanya mengirimkan kode minimal tanpa runtime klien;
  • Svelte memiliki bahasa templating ekspresif dan sistem komponen (preferensi pribadi);
  • Svelte menyertakan penyimpanan global, transisi, dan animasi di luar kotak, yang berarti: tidak ada keputusan yang melelahkan memilih perangkat manajemen keadaan global dan perpustakaan animasi;
  • Svelte mendukung CSS tercakup dalam komponen file tunggal;
  • SvelteKit mendukung SSR, perutean berbasis file yang sederhana namun fleksibel dan rute sisi server untuk membangun API;
  • SvelteKit memungkinkan setiap halaman menjalankan kode di server, misalnya mengambil data yang digunakan untuk merender halaman;
  • Tata letak yang dibagikan di seluruh rute;
  • SvelteKit dapat dijalankan di lingkungan tanpa server.

Membuat Dan Menyimpan PIN Game

Sebelum pengguna dapat mulai memainkan game, mereka harus membuat PIN game terlebih dahulu. Dengan berbagi PIN dengan orang lain, mereka semua dapat mengakses ruang permainan yang sama.

Cuplikan layar bagian awal permainan baru dari situs web Autowuzzler yang menunjukkan PIN permainan 751428 dan opsi untuk menyalin dan membagikan PIN dan URL permainan.
Mulai permainan baru dengan menyalin PIN permainan yang dihasilkan atau bagikan tautan langsung ke ruang permainan. (Pratinjau besar)

Ini adalah kasus penggunaan yang bagus untuk titik akhir sisi server SvelteKits dalam hubungannya dengan fungsi Sveltes onMount: Titik akhir /api/createcode menghasilkan PIN game, menyimpannya di database Supabase.io dan mengeluarkan PIN game sebagai respons . Ini adalah respons yang diambil segera setelah komponen halaman dari halaman "buat" dipasang:

Diagram yang menunjukkan tiga bagian: Buat halaman, buat titik akhir kode, dan Supabase.io. Buat halaman mengambil titik akhir dalam fungsi onMount, titik akhir menghasilkan PIN game, menyimpannya di Supabase.io dan merespons dengan PIN game. Halaman Create kemudian menampilkan PIN game.
PIN game dibuat di titik akhir, disimpan di database Supabase.io dan ditampilkan di halaman “Buat”. (Pratinjau besar)

Menyimpan PIN Game Dengan Supabase.io

Supabase.io adalah alternatif sumber terbuka untuk Firebase. Supabase membuatnya sangat mudah untuk membuat database PostgreSQL dan mengaksesnya baik melalui salah satu pustaka kliennya atau melalui REST.

Untuk klien JavaScript, kami mengimpor fungsi createClient dan menjalankannya menggunakan parameter supabase_url dan supabase_key yang kami terima saat membuat database. Untuk menyimpan PIN game yang dibuat pada setiap panggilan ke titik akhir createcode , yang perlu kita lakukan adalah menjalankan kueri insert sederhana ini:

 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 }]);

Catatan : supabase_url dan supabase_key disimpan dalam file .env. Karena Vite — alat build di jantung SvelteKit — diperlukan awalan variabel lingkungan dengan VITE_ agar dapat diakses di SvelteKit.

Mengakses Game

Saya ingin bergabung dengan game Autowuzzler semudah mengikuti tautan. Oleh karena itu, setiap ruang permainan harus memiliki URL sendiri berdasarkan PIN permainan yang dibuat sebelumnya , misalnya https://autowuzzler.com/play/12345.

Di SvelteKit, halaman dengan parameter rute dinamis dibuat dengan meletakkan bagian dinamis rute dalam tanda kurung siku saat memberi nama file halaman: client/src/routes/play/[gamePIN].svelte . Nilai parameter gamePIN kemudian akan tersedia di komponen halaman (lihat dokumen SvelteKit untuk detailnya). Dalam rute play , kita perlu terhubung ke server Colyseus, membuat instance dunia fisika untuk dirender ke layar, menangani pembaruan ke objek game, mendengarkan input keyboard dan menampilkan UI lain seperti skor, dan seterusnya.

Menghubungkan Ke Colyseus Dan Memperbarui Status

Pustaka klien Colyseus memungkinkan kita untuk menghubungkan klien ke server Colyseus. Pertama, mari buat Colyseus.Client baru dengan mengarahkannya ke server Colyseus ( ws://localhost:2567 dalam pengembangan). Kemudian gabungkan ruangan dengan nama yang kami pilih sebelumnya ( autowuzzler ) dan gamePIN dari parameter rute. Parameter gamePIN memastikan pengguna bergabung dengan instance ruang yang benar (lihat “mencari jodoh” di atas).

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

Karena SvelteKit merender halaman di server pada awalnya, kita perlu memastikan bahwa kode ini hanya berjalan di klien setelah halaman selesai dimuat. Sekali lagi, kami menggunakan fungsi siklus hidup onMount untuk kasus penggunaan itu. (Jika Anda terbiasa dengan React, onMount mirip dengan kait useEffect dengan larik dependensi kosong.)

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

Sekarang setelah kita terhubung ke server game Colyseus, kita dapat mulai mendengarkan setiap perubahan pada objek game kita.

Berikut adalah contoh cara mendengarkan pemain yang bergabung ke ruang ( onAdd ) dan menerima pembaruan status berturut-turut untuk pemain ini:

 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 }); }; };

Dalam metode updatePlayer dunia fisika, kami memperbarui properti satu per satu karena onChange onChange memberikan satu set semua properti yang diubah.

Catatan : Fungsi ini hanya berjalan pada versi klien dari dunia fisika, karena objek game hanya dimanipulasi secara tidak langsung melalui server Colyseus.

 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 ... } }

Prosedur yang sama berlaku untuk objek permainan lainnya (bola dan tim): dengarkan perubahannya dan terapkan nilai yang diubah ke dunia fisika klien.

Sejauh ini, tidak ada objek yang bergerak karena kita masih perlu mendengarkan input keyboard dan mengirimkannya ke server . Alih-alih mengirim acara secara langsung pada setiap acara keydown , kami memelihara peta tombol yang saat ini ditekan dan mengirim acara ke server Colyseus dalam loop 50ms. Dengan cara ini, kami dapat mendukung penekanan beberapa tombol secara bersamaan dan mengurangi jeda yang terjadi setelah peristiwa keydown pertama dan berturut-turut saat tombol tetap ditekan:

 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);

Sekarang siklusnya selesai: dengarkan penekanan tombol, kirim perintah yang sesuai ke server Colyseus untuk memanipulasi dunia fisika di server. Server Colyseus kemudian menerapkan properti fisik baru ke semua objek game dan menyebarkan data kembali ke klien untuk memperbarui instance game yang dihadapi pengguna.

Gangguan Kecil

Dalam retrospeksi, dua hal dari kategori yang tidak ada yang memberi tahu saya tetapi seseorang-seharusnya muncul dalam pikiran:

  • Pemahaman yang baik tentang cara kerja mesin fisika bermanfaat. Saya menghabiskan banyak waktu untuk menyempurnakan sifat dan batasan fisika. Meskipun saya membuat game kecil dengan Phaser.js dan Matter.js sebelumnya, ada banyak percobaan dan kesalahan untuk membuat objek bergerak seperti yang saya bayangkan.
  • Real-time itu sulit — terutama dalam game berbasis fisika. Penundaan kecil sangat memperburuk pengalaman, dan sementara menyinkronkan status di seluruh klien dengan Colyseus berfungsi dengan baik, itu tidak dapat menghapus penundaan komputasi dan transmisi.

Gotchas Dan Peringatan Dengan SvelteKit

Karena saya menggunakan SvelteKit ketika baru keluar dari beta-oven, ada beberapa gotcha dan peringatan yang ingin saya tunjukkan:

  • Butuh beberapa saat untuk mengetahui bahwa variabel lingkungan perlu diawali dengan VITE_ untuk menggunakannya di SvelteKit. Ini sekarang didokumentasikan dengan baik di FAQ.
  • Untuk menggunakan Supabase, saya harus menambahkan Supabase ke daftar dependencies dan devDependencies dari package.json. Saya percaya ini tidak lagi terjadi.
  • Fungsi load SvelteKits berjalan baik di server maupun di klien!
  • Untuk mengaktifkan penggantian modul panas penuh (termasuk mempertahankan status), Anda harus menambahkan baris komentar <!-- @hmr:keep-all --> secara manual di komponen halaman Anda. Lihat FAQ untuk lebih jelasnya.

Banyak kerangka kerja lain juga sangat cocok, tetapi saya tidak menyesal memilih SvelteKit untuk proyek ini. Ini memungkinkan saya untuk bekerja pada aplikasi klien dengan cara yang sangat efisien — sebagian besar karena Svelte sendiri sangat ekspresif dan melewatkan banyak kode boilerplate, tetapi juga karena Svelte memiliki hal-hal seperti animasi, transisi, cakupan CSS, dan toko global yang dipanggang. SvelteKit menyediakan semua blok bangunan yang saya butuhkan (SSR, perutean, rute server) dan meskipun masih dalam versi beta, rasanya sangat stabil dan cepat.

Penerapan dan Hosting

Awalnya, saya meng-host server Colyseus (Node) pada instance Heroku dan membuang banyak waktu untuk membuat WebSockets dan CORS berfungsi. Ternyata, kinerja dyno Heroku kecil (gratis) tidak cukup untuk kasus penggunaan waktu nyata. Saya kemudian memigrasikan aplikasi Colyseus ke server kecil di Linode. Aplikasi sisi klien disebarkan oleh dan dihosting di Netlify melalui adaptor-netlify SvelteKits. Tidak ada kejutan di sini: Netlify baru saja bekerja dengan baik!

Kesimpulan

Memulai dengan prototipe yang sangat sederhana untuk memvalidasi ide tersebut sangat membantu saya dalam mencari tahu apakah proyek tersebut layak untuk diikuti dan di mana tantangan teknis dari permainan tersebut. Dalam implementasi akhir, Colyseus menangani semua beban berat status sinkronisasi secara real-time di beberapa klien, yang didistribusikan di beberapa ruangan. Sungguh mengesankan betapa cepatnya aplikasi multi-pengguna waktu nyata dapat dibangun dengan Colyseus — setelah Anda mengetahui cara menggambarkan skema dengan benar. Panel pemantauan bawaan Colyseus membantu memecahkan masalah sinkronisasi apa pun.

Yang memperumit pengaturan ini adalah lapisan fisika permainan karena memperkenalkan salinan tambahan dari setiap objek permainan terkait fisika yang perlu dipertahankan. Menyimpan PIN game di Supabase.io dari aplikasi SvelteKit sangat mudah. Kalau dipikir-pikir, saya bisa saja menggunakan database SQLite untuk menyimpan PIN game, tetapi mencoba hal-hal baru adalah setengah kesenangan ketika membangun proyek sampingan.

Akhirnya, menggunakan SvelteKit untuk membangun bagian depan game memungkinkan saya untuk bergerak cepat — dan sesekali dengan seringai kegembiraan di wajah saya.

Sekarang, silakan dan undang teman Anda ke putaran Autowuzzler!

Bacaan Lebih Lanjut di Majalah Smashing

  • “Mulai Bereaksi Dengan Membuat Game Whac-A-Mole,” Jhey Tompkins
  • “Cara Membuat Game Real-Time Multiplayer Virtual Reality,” Alvin Wan
  • “Menulis Mesin Petualangan Teks Multiplayer Di Node.js,” Fernando Doglio
  • “Masa Depan Desain Web Seluler: Desain Video Game Dan Mendongeng,” Suzanne Scacca
  • “Cara Membangun Game Pelari Tanpa Akhir Dalam Realitas Virtual,” Alvin Wan