Buat Game Multiplayer Bajak Laut Shooter: Di Browser Anda
() translation by (you can also view the original English article)
Membuat game multiplayer merupakan tantangan karena beberapa alasan: mereka dapat mahal untuk dihosting, sulit untuk dirancang, dan sulit untuk diimplementasikan. Dengan tutorial ini, saya berharap untuk mengatasi penghalang terakhir itu.
Ini ditujukan untuk pengembang yang tahu cara membuat game dan mengenal JavaScript, tetapi belum pernah membuat game multiplayer online. Setelah selesai, Anda harus merasa nyaman menerapkan komponen jaringan dasar ke dalam game apa pun dan dapat membangunnya dari sana!
Inilah yang akan kita bangun:



Anda dapat mencoba versi permainan langsung di sini! W atau Up untuk bergerak ke arah mouse dan klik untuk menembak. (Jika tidak ada orang lain yang online, coba buka dua jendela peramban di komputer yang sama, atau yang ada di ponsel Anda, untuk melihat cara kerja multiplayer). Jika Anda tertarik untuk menjalankan ini secara lokal, kode sumber lengkap juga tersedia di GitHub.
Saya menempatkan game ini bersama-sama menggunakan aset seni Kenney's Pirate Pack dan kerangka kerja game Phaser. Anda akan mengambil peran sebagai programmer jaringan untuk tutorial ini. Titik awal Anda akan menjadi versi single-player yang berfungsi penuh dari permainan ini, dan itu akan menjadi tugas Anda untuk menulis server di Node.js, menggunakan Socket.io untuk bagian jaringan. Agar tutorial ini tetap dapat dikelola, saya akan berfokus pada bagian multipemain dan membaca konsep khusus Phaser dan Node.js.
Tidak perlu mengatur apa pun secara lokal karena kami akan membuat game ini sepenuhnya di peramban di Glitch.com! Glitch adalah alat yang luar biasa untuk membuat aplikasi web, termasuk back end, database, dan semuanya. Ini bagus untuk membuat prototipe, mengajar, dan berkolaborasi, dan saya senang memperkenalkannya di seluruh tutorial ini.
Mari kita selami.
1. Pengaturan
Saya sudah memasang starter kit di Glitch.com.
Beberapa kiat antarmuka cepat: kapan saja, Anda dapat melihat pratinjau langsung aplikasi Anda dengan mengklik tombol Show (kiri atas).



Bilah sisi vertikal di sebelah kiri berisi semua file di aplikasi Anda. Untuk mengedit aplikasi ini, Anda harus "mencampurnya". Ini akan membuat salinannya di akun Anda (atau menyimpannya di git lingo). Klik pada tombol Remix this.



Pada titik ini, Anda akan mengedit aplikasi di bawah akun anonim. Anda dapat masuk (kanan atas) untuk menyimpan pekerjaan Anda.
Sekarang, sebelum kita melangkah lebih jauh, penting untuk menjadi akrab dengan kode untuk permainan yang Anda coba tambahkan multiplayer. Lihatlah index.html. Ada tiga fungsi penting yang harus diperhatikan: preload
(baris 99), create
(baris 115), dan GameLoop
(baris 142), selain objek pemain (baris 35).
Jika Anda lebih suka belajar dengan melakukan, cobalah tantangan ini untuk memastikan Anda mendapatkan inti dari cara kerja game:
- Buat dunia lebih besar (baris 29)—perhatikan bahwa ada ukuran dunia yang terpisah, untuk dunia dalam game, dan ukuran jendela, untuk kanvas yang sebenarnya di halaman.
- Buat SPACEBAR juga mendorong ke depan (baris 53).
- Ubah jenis kapal pemain Anda (baris 129).
- Buat peluru bergerak lebih lambat (garis 155).
Memasang Socket.io
Socket.io adalah pustaka untuk mengelola komunikasi waktu nyata di peramban menggunakan WebSockets (sebagai lawan menggunakan protokol seperti UDP jika Anda sedang membangun game desktop multipemain). Ini juga memiliki fallback untuk memastikannya tetap berfungsi meskipun WebSockets tidak didukung. Jadi itu mengurus protokol pesan dan memperlihatkan sistem pesan berbasis kejadian yang bagus untuk Anda gunakan.
Hal pertama yang perlu kita lakukan adalah menginstal modul Socket.io. Pada Glitch, Anda dapat melakukan ini dengan masuk ke file package.json dan mengetik di modul yang Anda inginkan dalam dependensi, atau mengklik Add package dan mengetikkan "socket.io".



Ini akan menjadi saat yang tepat untuk menunjukkan log server. Klik pada tombol Logs di sebelah kiri untuk membuka log server. Anda harus melihatnya menginstal Socket.io bersama dengan semua dependensinya. Di sinilah Anda akan pergi untuk melihat kesalahan atau output dari kode server.



Sekarang untuk pergi server.js. Di sinilah kode server Anda hidup. Saat ini, ia hanya memiliki beberapa boiler dasar untuk melayani HTML kami. Tambahkan baris ini di bagian atas untuk menyertakan Socket.io:
1 |
var io = require('socket.io')(http); // Make sure to put this after http has been defined |
Sekarang kita juga perlu menyertakan Socket.io pada klien, jadi kembali ke index.html dan tambahkan ini di bagian atas di dalam tag <head>
Anda:
1 |
<!-- Load the Socket.io networking library -->
|
2 |
<script src="/socket.io/socket.io.js"></script> |
Catatan: Socket.io secara otomatis menangani melayani pustaka klien di jalur itu, jadi itulah mengapa baris ini berfungsi meskipun Anda tidak melihat direktori / map.io/ di folder Anda.
Sekarang Socket.io sudah termasuk dan siap digunakan!
2. Detecting & Spawning Players
Langkah nyata pertama kami adalah menerima koneksi di server dan menelurkan pemain baru di klien.
Menerima Koneksi di Server
Di bagian bawah server.js, tambahkan kode ini:
1 |
// Tell Socket.io to start accepting connections
|
2 |
io.on('connection', function(socket){ |
3 |
console.log("New client has connected with id:",socket.id); |
4 |
})
|
Ini memberitahu Socket.io untuk mendengarkan setiap acara connection
, yang secara otomatis terpicu ketika klien terhubung. Ini akan membuat objek socket
baru untuk setiap klien, di mana socket.id
adalah pengenal unik untuk klien itu.
Hanya untuk memastikan ini berfungsi, kembalilah ke klien Anda (index.html) dan tambahkan baris ini di suatu tempat dalam fungsi create:
1 |
var socket = io(); // This triggers the 'connection' event on the server |
Jika Anda meluncurkan gim dan kemudian melihat log server Anda (klik pada tombol Logs), Anda akan melihatnya mencatat peristiwa koneksi itu!
Sekarang, ketika seorang pemain baru terhubung, kami mengharapkan mereka untuk mengirim kami informasi tentang negara mereka. Dalam hal ini, kita perlu mengetahui setidaknya x, y, dan angle untuk benar-benar menelurkannya di lokasi yang tepat.
Event connection
adalah acara built-in yang Socket.io nyalakan untuk kita. Kami dapat mendengarkan setiap acara yang ditentukan khusus yang kami inginkan. Saya akan memanggil new-player
milik saya, dan saya mengharapkan klien untuk mengirimnya segera setelah mereka terhubung dengan informasi tentang lokasi mereka. Ini akan terlihat seperti ini:
1 |
// Tell Socket.io to start accepting connections
|
2 |
io.on('connection', function(socket){ |
3 |
console.log("New client has connected with id:",socket.id); |
4 |
socket.on('new-player',function(state_data){ // Listen for new-player event on this client |
5 |
console.log("New player has state:",state_data); |
6 |
})
|
7 |
})
|
Anda tidak akan melihat apa pun di log server jika Anda menjalankan ini. Ini karena kami belum memberi tahu klien untuk mengeluarkan event new-player
ini. Tapi mari kita berpura-pura itu diurus sebentar, dan terus berjalan di server. Apa yang harus terjadi setelah kami menerima lokasi pemain baru yang bergabung?
Kami dapat mengirim pesan ke setiap pemain lain yang terhubung untuk memberi tahu bahwa ada pemain baru yang bergabung. Socket.io menyediakan fungsi praktis untuk melakukan ini:
1 |
socket.broadcast.emit('create-player',state_data); |
Memanggil socket.emit
hanya akan mengirim pesan kembali ke klien yang satu itu. Memanggil socket.broadcast.emit
mengirimkannya ke setiap klien yang terhubung ke server, kecuali satu socket yang dipanggil.
Menggunakan io.emit
akan mengirim pesan ke setiap klien yang terhubung ke server tanpa pengecualian. Kami tidak ingin melakukan itu dengan pengaturan kami saat ini karena jika Anda mendapat pesan kembali dari server meminta Anda untuk membuat kapal Anda sendiri, akan ada sprite duplikat, karena kami sudah membuat kapal pemain sendiri ketika pertandingan dimulai. Berikut ini adalah contekan berguna untuk berbagai jenis fungsi perpesanan yang akan kami gunakan dalam tutorial ini.
Kode server sekarang seharusnya terlihat seperti ini:
1 |
// Tell Socket.io to start accepting connections
|
2 |
io.on('connection', function(socket){ |
3 |
console.log("New client has connected with id:",socket.id); |
4 |
socket.on('new-player',function(state_data){ // Listen for new-player event on this client |
5 |
console.log("New player has state:",state_data); |
6 |
socket.broadcast.emit('create-player',state_data); |
7 |
})
|
8 |
})
|
Jadi setiap kali seorang pemain terhubung, kami berharap mereka mengirimi kami pesan dengan data lokasi mereka, dan kami akan mengirim data itu kembali ke setiap pemain lain sehingga mereka dapat menelurkan sprite itu.
Spawning pada Klien
Sekarang, untuk menyelesaikan siklus ini, kita tahu kita perlu melakukan dua hal pada klien:
- Memancarkan pesan dengan data lokasi kami setelah kami terhubung.
- Listen untuk
create-player
dan menelurkan pemain di lokasi tersebut.
Untuk tugas pertama, setelah kami membuat pemain dalam fungsi create kami (sekitar baris 135), kami dapat memancarkan pesan yang berisi data lokasi yang ingin kami kirim seperti ini:
1 |
socket.emit('new-player',{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation}) |
Anda tidak perlu khawatir tentang serialisasi data yang Anda kirim. Anda dapat melewati objek apa pun dan Socket.io akan menanganinya untuk Anda.
Sebelum bergerak maju, uji apakah ini berfungsi. Anda seharusnya melihat pesan di log server yang mengatakan sesuatu seperti:
1 |
New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 } |
Kami tahu bahwa server kami menerima pengumuman kami bahwa pemain baru telah terhubung, bersama dengan benar mendapatkan data lokasi mereka!
Selanjutnya, kami ingin mendengarkan permintaan untuk membuat pemain baru. Kita dapat menempatkan kode ini tepat setelah emit kami, dan seharusnya terlihat seperti:
1 |
socket.on('create-player',function(state){ |
2 |
// CreateShip is a function I've already defined to create and return a sprite
|
3 |
CreateShip(1,state.x,state.y,state.angle) |
4 |
})
|
Sekarang ujilah. Buka dua jendela permainan Anda dan lihat apakah itu berfungsi.
Apa yang harus Anda lihat adalah bahwa setelah membuka dua klien, klien pertama akan memiliki dua kapal yang bertelur, sedangkan yang kedua hanya akan melihat satu.
Tantangan: Bisakah Anda mencari tahu mengapa ini terjadi? Atau bagaimana Anda memperbaikinya? Langkah melalui logika client/server kami telah menulis dan mencoba untuk debug itu.
Saya harap Anda punya kesempatan untuk memikirkannya sendiri! Apa yang terjadi adalah ketika pemain pertama terhubung, server mengirimkan event create-player
ke setiap pemain lain, tetapi tidak ada pemain lain di sekitarnya untuk menerimanya. Setelah pemain kedua terhubung, server sekali lagi mengirimkan siarannya, dan pemain 1 menerimanya dan dengan benar menghasilkan sprite, sedangkan pemain 2 telah melewatkan siaran koneksi awal pemain 1.
Jadi masalahnya terjadi karena pemain 2 bergabung di akhir permainan dan perlu mengetahui keadaan permainan. Kami perlu memberi tahu pemain baru apa pun yang menghubungkan pemain apa yang sudah ada (atau apa yang telah terjadi di dunia) sehingga mereka dapat mengejar ketinggalan. Sebelum kita masuk untuk memperbaiki ini, saya memiliki peringatan singkat.
Peringatan tentang Menyinkronkan Game State
Ada dua pendekatan untuk menjaga setiap gim pemain disinkronkan. Yang pertama adalah hanya mengirimkan jumlah minimal informasi tentang apa yang telah diubah di seluruh jaringan. Jadi setiap kali pemain baru terhubung, Anda akan mengirim hanya informasi untuk pemain baru itu ke semua pemain lain (dan mengirim pemain baru itu daftar semua pemain lain di dunia), dan ketika mereka memutus sambungan, Anda memberi tahu semua pemain lain bahwa klien individu ini telah terputus.
Pendekatan kedua adalah mengirim seluruh status game. Dalam hal ini, Anda hanya akan mengirim daftar lengkap semua pemain kepada setiap orang setiap kali koneksi atau putuskan terjadi.
Yang pertama lebih baik dalam arti meminimalkan informasi yang dikirim melalui jaringan, tetapi bisa sangat rumit dan memiliki risiko pemain tidak sinkron. Yang kedua menjamin pemain akan selalu sinkron tetapi melibatkan pengiriman lebih banyak data dengan setiap pesan.
Dalam kasus kami, alih-alih mencoba mengirim pesan ketika pemain baru telah terhubung untuk membuatnya ketika mereka memutuskan untuk menghapusnya, dan ketika mereka pindah untuk memperbarui posisi mereka, kami dapat menggabungkan semua itu ke dalam satu event update
. Acara pembaruan ini akan selalu mengirim posisi setiap pemain yang tersedia ke semua klien. Itu saja yang harus dilakukan oleh server. Klien kemudian bertanggung jawab untuk menjaga agar dunia tetap up to date dengan state yang diterimanya.
Untuk menerapkan ini, saya akan:
- Simpan kamus pemain, dengan kunci yang menjadi ID mereka dan nilainya sebagai data lokasi mereka.
- Tambahkan pemain ke kamus ini ketika mereka terhubung dan mengirim acara pembaruan.
- Hapus pemain dari kamus ini ketika mereka memutuskan dan mengirim event update.
Anda dapat mencoba untuk menerapkan ini sendiri karena langkah-langkah ini cukup sederhana (cheatsheet mungkin berguna). Inilah yang mungkin terlihat seperti implementasi penuh:
1 |
// Tell Socket.io to start accepting connections
|
2 |
// 1 - Keep a dictionary of all the players as key/value
|
3 |
var players = {}; |
4 |
io.on('connection', function(socket){ |
5 |
console.log("New client has connected with id:",socket.id); |
6 |
socket.on('new-player',function(state_data){ // Listen for new-player event on this client |
7 |
console.log("New player has state:",state_data); |
8 |
// 2 - Add the new player to the dict
|
9 |
players[socket.id] = state_data; |
10 |
// Send an update event
|
11 |
io.emit('update-players',players); |
12 |
})
|
13 |
socket.on('disconnect',function(){ |
14 |
// 3- Delete from dict on disconnect
|
15 |
delete players[socket.id]; |
16 |
// Send an update event
|
17 |
})
|
18 |
})
|
Sisi klien sedikit lebih rumit. Di satu sisi, kita hanya perlu khawatir tentang event update-players
sekarang, tetapi di sisi lain, kita harus memperhitungkan untuk menciptakan lebih banyak kapal jika server mengirimi kita lebih banyak kapal daripada yang kita ketahui, atau menghancurkan jika kita memiliki terlalu banyak.
Beginilah cara saya menangani event ini di klien:
1 |
// Listen for other players connecting
|
2 |
// NOTE: You must have other_players = {} defined somewhere
|
3 |
socket.on('update-players',function(players_data){ |
4 |
var players_found = {}; |
5 |
// Loop over all the player data received
|
6 |
for(var id in players_data){ |
7 |
// If the player hasn't been created yet
|
8 |
if(other_players[id] == undefined && id != socket.id){ // Make sure you don't create yourself |
9 |
var data = players_data[id]; |
10 |
var p = CreateShip(1,data.x,data.y,data.angle); |
11 |
other_players[id] = p; |
12 |
console.log("Created new player at (" + data.x + ", " + data.y + ")"); |
13 |
}
|
14 |
players_found[id] = true; |
15 |
|
16 |
// Update positions of other players
|
17 |
if(id != socket.id){ |
18 |
other_players[id].x = players_data[id].x; // Update target, not actual position, so we can interpolate |
19 |
other_players[id].y = players_data[id].y; |
20 |
other_players[id].rotation = players_data[id].angle; |
21 |
}
|
22 |
|
23 |
|
24 |
}
|
25 |
// Check if a player is missing and delete them
|
26 |
for(var id in other_players){ |
27 |
if(!players_found[id]){ |
28 |
other_players[id].destroy(); |
29 |
delete other_players[id]; |
30 |
}
|
31 |
}
|
32 |
|
33 |
})
|
Saya melacak kapal-kapal pada klien dalam kamus yang disebut other_players
yang saya jelaskan di bagian atas skrip saya (tidak ditampilkan di sini). Karena server mengirim data pemain ke semua pemain, saya harus menambahkan cek agar klien tidak membuat sprite ekstra untuk mereka sendiri. (Jika Anda mengalami kesulitan menyusun ini, inilah kode lengkap yang seharusnya ada di index.html pada titik ini).
Sekarang ujilah ini. Anda harus dapat membuat dan menutup beberapa klien dan melihat jumlah kapal yang benar-benar bertelur di posisi yang tepat!
3. Menyinkronkan Posisi Kapal
Di sinilah kita mendapatkan bagian yang sangat menyenangkan. Kami ingin benar-benar menyinkronkan posisi kapal di semua klien sekarang. Di sinilah kesederhanaan struktur yang telah kami bangun sejauh ini benar-benar terlihat. Kami sudah memiliki acara pembaruan yang dapat menyinkronkan lokasi semua orang. Yang perlu kita lakukan sekarang adalah:
- Buat klien memancarkan setiap kali mereka pindah dengan lokasi baru mereka.
- Membuat server mendengarkan pesan bergerak dan memperbarui entri itu pemain di kamus
players
.
- Keluarkan event pembaruan untuk semua klien.
Dan seharusnya itu! Sekarang giliran Anda untuk mencoba dan menerapkan ini sendiri.
Jika Anda benar-benar terjebak dan memerlukan petunjuk, Anda dapat melihat proyek akhir yang selesai sebagai referensi.
Catatan tentang Meminimalkan Data Jaringan
Cara paling mudah untuk menerapkan ini adalah memperbarui semua pemain dengan lokasi baru setiap kali Anda menerima pesan bergerak dari pemain mana pun. Ini bagus karena pemain akan selalu menerima informasi terbaru segera setelah tersedia, tetapi jumlah pesan yang dikirim melalui jaringan dapat dengan mudah bertambah hingga ratusan per frame. Bayangkan jika Anda memiliki 10 pemain, masing-masing mengirim pesan bergerak setiap frame, yang kemudian harus dikembalikan ke semua 10 pemain. Itu sudah 100 pesan per frame!
Cara yang lebih baik untuk melakukannya adalah menunggu hingga server menerima semua pesan dari para pemain sebelum mengirimkan pembaruan besar yang berisi semua informasi ke semua pemain. Dengan cara itu Anda menekan jumlah pesan yang Anda kirim ke hanya jumlah pemain yang Anda miliki dalam permainan (sebagai lawan dari kuadrat dari angka itu). Masalahnya, bagaimanapun, adalah bahwa setiap orang akan mengalami lag sebagai pemain dengan koneksi paling lambat dalam game.
Cara lain untuk melakukannya adalah dengan hanya mengirim pembaruan server pada tingkat yang konstan, terlepas dari berapa banyak pesan yang diterima dari pemain sejauh ini. Memiliki pembaruan server sekitar 30 kali per detik sepertinya standar umum.
Namun Anda memutuskan untuk membuat struktur server Anda, berhati-hatilah terhadap berapa banyak pesan yang Anda kirim setiap frame sejak awal ketika Anda mengembangkan game Anda.
4. Menyinkronkan Peluru
Kami hampir sampai! Bagian besar terakhir adalah menyinkronkan peluru di seluruh jaringan. Kami dapat melakukannya dengan cara yang sama seperti kami menyinkronkan para pemain:
- Setiap klien mengirimkan posisi semua pelurunya setiap frame.
- Server menyampaikan hal itu kepada setiap pemain.
Tetapi ada masalah.
Mengamankan Terhadap Cheats
Jika Anda menyampaikan apa pun yang klien kirimkan kepada Anda sebagai posisi sebenarnya dari peluru, maka pemain dapat menipu dengan memodifikasi klien mereka untuk mengirimi Anda data palsu, seperti peluru yang teleport ke mana pun kapal-kapal lain berada. Anda dapat dengan mudah mencoba ini sendiri dengan mengunduh halaman web, memodifikasi JavaScript, dan menjalankannya lagi. Ini bukan hanya masalah untuk game yang dibuat untuk browser. Secara umum, Anda tidak pernah dapat benar-benar mempercayai data yang berasal dari klien.
Untuk mengurangi ini, kami akan mencoba skema yang berbeda:
- Klien memancarkan kapan pun mereka telah menembakkan peluru dengan lokasi dan arah.
- Server mensimulasikan gerakan peluru.
- Server memperbarui setiap klien dengan lokasi semua peluru.
- Klien membuat pelurunya di lokasi yang diterima oleh server.
Dengan cara ini, klien bertanggung jawab di mana peluru menumbuhkan, tetapi tidak seberapa cepat bergerak atau ke mana ia pergi setelah itu. Klien dapat mengubah lokasi peluru dalam pandangan mereka sendiri, tetapi mereka tidak dapat mengubah apa yang dilihat klien lain.
Sekarang, untuk menerapkan ini, saya akan menambahkan emit ketika Anda menembak. Saya tidak akan lagi membuat sprite yang sebenarnya, karena keberadaan dan lokasinya sekarang ditentukan sepenuhnya oleh server. Kode penembakan peluru baru kami di index.html sekarang seharusnya terlihat seperti ini:
1 |
// Shoot bullet
|
2 |
if(game.input.activePointer.leftButton.isDown && !this.shot){ |
3 |
var speed_x = Math.cos(this.sprite.rotation + Math.PI/2) * 20; |
4 |
var speed_y = Math.sin(this.sprite.rotation + Math.PI/2) * 20; |
5 |
/* The server is now simulating the bullets, clients are just rendering bullet locations, so no need to do this anymore
|
6 |
var bullet = {};
|
7 |
bullet.speed_x = speed_x;
|
8 |
bullet.speed_y = speed_y;
|
9 |
bullet.sprite = game.add.sprite(this.sprite.x + bullet.speed_x,this.sprite.y + bullet.speed_y,'bullet');
|
10 |
bullet_array.push(bullet);
|
11 |
*/
|
12 |
this.shot = true; |
13 |
// Tell the server we shot a bullet
|
14 |
socket.emit('shoot-bullet',{x:this.sprite.x,y:this.sprite.y,angle:this.sprite.rotation,speed_x:speed_x,speed_y:speed_y}) |
15 |
}
|
Anda juga dapat mengomentari seluruh bagian ini yang memperbarui peluru pada klien:
1 |
/* We're updating the bullets on the server, so we don't need to do this on the client anymore
|
2 |
// Update bullets
|
3 |
for(var i=0;i<bullet_array.length;i++){
|
4 |
var bullet = bullet_array[i];
|
5 |
bullet.sprite.x += bullet.speed_x;
|
6 |
bullet.sprite.y += bullet.speed_y;
|
7 |
// Remove if it goes too far off screen
|
8 |
if(bullet.sprite.x < -10 || bullet.sprite.x > WORLD_SIZE.w || bullet.sprite.y < -10 || bullet.sprite.y > WORLD_SIZE.h){
|
9 |
bullet.sprite.destroy();
|
10 |
bullet_array.splice(i,1);
|
11 |
i--;
|
12 |
}
|
13 |
}
|
14 |
*/
|
Akhirnya, kita perlu meminta klien untuk mendengarkan pembaruan peluru. Saya telah memilih untuk menangani hal ini dengan cara yang sama seperti yang saya lakukan dengan pemain, di mana server hanya mengirim sebuah array dari semua lokasi peluru dalam suatu acara yang disebut bullet-update
, dan klien akan membuat atau menghancurkan peluru untuk tetap sinkron. Inilah yang terlihat seperti:
1 |
// Listen for bullet update events
|
2 |
socket.on('bullets-update',function(server_bullet_array){ |
3 |
// If there's not enough bullets on the client, create them
|
4 |
for(var i=0;i<server_bullet_array.length;i++){ |
5 |
if(bullet_array[i] == undefined){ |
6 |
bullet_array[i] = game.add.sprite(server_bullet_array[i].x,server_bullet_array[i].y,'bullet'); |
7 |
} else { |
8 |
//Otherwise, just update it!
|
9 |
bullet_array[i].x = server_bullet_array[i].x; |
10 |
bullet_array[i].y = server_bullet_array[i].y; |
11 |
}
|
12 |
}
|
13 |
// Otherwise if there's too many, delete the extra
|
14 |
for(var i=server_bullet_array.length;i<bullet_array.length;i++){ |
15 |
bullet_array[i].destroy(); |
16 |
bullet_array.splice(i,1); |
17 |
i--; |
18 |
}
|
19 |
|
20 |
})
|
Itu harus menjadi segalanya di klien. Saya berasumsi Anda tahu di mana menaruh cuplikan ini dan bagaimana menyusun semuanya bersama-sama pada titik ini, tetapi jika Anda mengalami masalah apa pun, ingat Anda selalu dapat melihat hasil akhir untuk referensi.
Sekarang, di server.js, kita perlu melacak dan mensimulasikan peluru. Pertama, kami membuat larik untuk melacak peluru, dengan cara yang sama kami memiliki satu untuk pemain:
1 |
var bullet_array = []; // Keeps track of all the bullets to update them on the server |
Selanjutnya, kami mendengarkan peristiwa peluru menembak kami:
1 |
// Listen for shoot-bullet events and add it to our bullet array
|
2 |
socket.on('shoot-bullet',function(data){ |
3 |
if(players[socket.id] == undefined) return; |
4 |
var new_bullet = data; |
5 |
data.owner_id = socket.id; // Attach id of the player to the bullet |
6 |
bullet_array.push(new_bullet); |
7 |
});
|
Sekarang kami mensimulasikan peluru 60 kali per detik:
1 |
// Update the bullets 60 times per frame and send updates
|
2 |
function ServerGameLoop(){ |
3 |
for(var i=0;i<bullet_array.length;i++){ |
4 |
var bullet = bullet_array[i]; |
5 |
bullet.x += bullet.speed_x; |
6 |
bullet.y += bullet.speed_y; |
7 |
|
8 |
// Remove if it goes too far off screen
|
9 |
if(bullet.x < -10 || bullet.x > 1000 || bullet.y < -10 || bullet.y > 1000){ |
10 |
bullet_array.splice(i,1); |
11 |
i--; |
12 |
}
|
13 |
|
14 |
}
|
15 |
|
16 |
}
|
17 |
|
18 |
setInterval(ServerGameLoop, 16); |
Dan langkah terakhir adalah mengirim acara pembaruan di suatu tempat di dalam fungsi itu (tapi pasti di luar untuk loop):
1 |
// Tell everyone where all the bullets are by sending the whole array
|
2 |
io.emit("bullets-update",bullet_array); |
Sekarang Anda benar-benar dapat mengujinya! Jika semua berjalan dengan baik, Anda harus melihat peluru menyinkronkan seluruh klien dengan benar. Fakta bahwa kami melakukan ini di server lebih banyak bekerja, tetapi juga memberi kami lebih banyak kontrol. Misalnya, ketika kami menerima peristiwa peluru menembak, kami dapat memeriksa bahwa kecepatan peluru berada dalam rentang tertentu, jika tidak kami tahu pemain ini curang.
5. Tabrakan Peluru
Ini adalah mekanika inti terakhir yang akan kami terapkan. Mudah-mudahan sekarang Anda sudah terbiasa dengan prosedur perencanaan implementasi kami, menyelesaikan implementasi klien sepenuhnya terlebih dahulu sebelum pindah ke server (atau sebaliknya). Ini adalah cara yang jauh lebih rawan kesalahan daripada beralih maju dan mundur saat Anda menerapkannya.
Memeriksa tabrakan adalah mekanika permainan yang krusial, jadi kami ingin ini curang. Kami akan menerapkannya di server yang sama dengan yang kami lakukan untuk peluru. Kita harus:
- Periksa apakah sebuah peluru cukup dekat dengan setiap pemain di server.
- Keluarkan acara untuk semua klien setiap kali ada pemain tertentu yang dipukul.
- Mintalah klien mendengarkan pada acara hit dan membuat kapal mem-flash ketika dipukul.
Anda dapat mencoba melakukan ini sepenuhnya sendiri. Untuk membuat flash player saat memukul, cukup atur alpha mereka ke 0:
1 |
player.sprite.alpha = 0; |
Dan itu akan memudahkan kembali ke alpha penuh lagi (ini dilakukan dalam pembaruan pemain). Untuk pemain lain, Anda akan melakukan hal yang sama, tetapi Anda harus berhati-hati membawa alpha mereka kembali ke satu dengan sesuatu seperti ini di fungsi pembaruan:
1 |
for(var id in other_players){ |
2 |
if(other_players[id].alpha < 1){ |
3 |
other_players[id].alpha += (1 - other_players[id].alpha) * 0.16; |
4 |
} else { |
5 |
other_players[id].alpha = 1; |
6 |
}
|
7 |
}
|
Satu-satunya bagian rumit yang mungkin harus Anda tangani adalah memastikan peluru milik pemain tidak bisa mengenai mereka (jika tidak, Anda mungkin selalu terkena peluru Anda sendiri setiap kali Anda menembak).
Perhatikan bahwa dalam skema ini, bahkan jika klien mencoba untuk menipu dan menolak untuk mengakui pesan klik yang dikirim oleh server, itu hanya akan mengubah apa yang mereka lihat di layar mereka sendiri. Semua pemain lain masih akan melihat bahwa pemain itu dipukul.
6. Gerakan yang Lebih Halus
Jika Anda mengikuti semua langkah hingga titik ini, saya ingin mengucapkan selamat kepada Anda. Anda baru saja membuat game multipemain yang berfungsi! Silakan, kirimkan teman dan saksikan keajaiban pemain penyatuan multiplayer online!
Permainan ini sepenuhnya berfungsi, tetapi pekerjaan kami tidak berhenti di situ. Ada beberapa masalah yang mungkin memengaruhi pengalaman pemain yang harus kami tangani:
- Gerakan pemain lain akan terlihat sangat berombak kecuali semua orang memiliki koneksi yang cepat.
- Peluru bisa terasa tidak responsif, karena peluru tidak langsung menyala. Ini menunggu pesan kembali dari server sebelum muncul di layar klien.
Kami dapat memperbaiki yang pertama dengan menginterpolasi data posisi kami untuk kapal di klien. Jadi bahkan jika kami tidak menerima pembaruan cukup cepat, kami dapat dengan lancar memindahkan kapal ke tempat yang seharusnya berlawanan dengan melakukan teleportasi di sana.
Peluru akan membutuhkan sedikit lebih banyak kecanggihan. Kami ingin server mengelola peluru, karena dengan cara itu curang, tetapi kami juga ingin mendapatkan umpan balik langsung dari menembakkan peluru dan melihatnya menembak. Cara terbaik adalah pendekatan hibrida. Baik server dan klien dapat mensimulasikan peluru, dengan server masih mengirimkan pembaruan posisi peluru. Jika mereka tidak sinkron, asumsikan server benar dan menimpa posisi peluru klien.
Menerapkan sistem peluru yang saya jelaskan di atas berada di luar ruang lingkup untuk tutorial ini, tetapi ada baiknya untuk mengetahui bahwa metode ini ada.
Melakukan interpolasi sederhana untuk posisi kapal sangat mudah. Alih-alih mengatur posisi secara langsung pada acara pembaruan tempat kami pertama kali menerima data posisi baru, kami cukup menyimpan posisi target:
1 |
// Update positions of other players
|
2 |
if(id != socket.id){ |
3 |
other_players[id].target_x = players_data[id].x; // Update target, not actual position, so we can interpolate |
4 |
other_players[id].target_y = players_data[id].y; |
5 |
other_players[id].target_rotation = players_data[id].angle; |
6 |
}
|
Kemudian, di dalam fungsi pembaruan kami (masih dalam klien), kami menggilir semua pemain lain dan mendorongnya menuju target ini:
1 |
// Interpolate all players to where they should be
|
2 |
for(var id in other_players){ |
3 |
var p = other_players[id]; |
4 |
if(p.target_x != undefined){ |
5 |
p.x += (p.target_x - p.x) * 0.16; |
6 |
p.y += (p.target_y - p.y) * 0.16; |
7 |
// Interpolate angle while avoiding the positive/negative issue
|
8 |
var angle = p.target_rotation; |
9 |
var dir = (angle - p.rotation) / (Math.PI * 2); |
10 |
dir -= Math.round(dir); |
11 |
dir = dir * Math.PI * 2; |
12 |
p.rotation += dir * 0.16; |
13 |
}
|
14 |
}
|
Dengan cara ini, server Anda dapat mengirim Anda pembaruan 30 kali per detik, tetapi masih memainkan permainan pada 60 fps dan itu akan terlihat mulus!
Kesimpulan
Fiuh! Kami hanya membahas banyak hal. Sekadar rekap, kami telah melihat cara mengirim pesan antara klien dan server, dan cara menyinkronkan keadaan permainan dengan meminta server menyampaikannya ke semua pemain. Ini adalah cara paling sederhana untuk menciptakan pengalaman multiplayer daring.
Kami juga melihat bagaimana Anda dapat mengamankan permainan Anda dari kecurangan dengan mensimulasikan bagian-bagian penting di server dan memberi tahu klien tentang hasilnya. Semakin sedikit Anda mempercayai klien Anda, semakin aman permainannya.
Akhirnya, kami melihat bagaimana mengatasi lag dengan melakukan interpolasi pada klien. Kompensasi Lag adalah topik yang luas dan sangat penting (beberapa game hanya menjadi tidak dapat dimainkan dengan lag yang cukup tinggi). Interpolasi sambil menunggu pembaruan selanjutnya dari server hanyalah salah satu cara untuk memitigasinya. Cara lain adalah dengan mencoba dan memprediksi beberapa frame berikutnya di muka, dan memperbaiki setelah Anda menerima data sebenarnya dari server, tetapi tentu saja ini bisa sangat rumit.
Cara yang benar-benar berbeda untuk mengurangi dampak lag adalah dengan hanya merancang di sekitarnya. Manfaat memiliki kapal-kapal berubah perlahan-lahan untuk bergerak bertindak sebagai mekanik gerakan yang unik dan juga cara untuk mencegah perubahan tiba-tiba dalam gerakan. Begitu pun dengan koneksi yang lambat, itu tetap tidak akan merusak pengalaman. Akuntansi untuk lag saat merancang elemen inti dari game Anda seperti ini dapat membuat perbedaan besar. Terkadang solusi terbaik tidak teknis sama sekali.
Salah satu fitur terakhir dari Glitch yang mungkin Anda anggap berguna adalah Anda dapat mengunduh atau mengekspor proyek Anda dengan masuk ke pengaturan lanjutan di kiri atas:



Jika Anda membuat sesuatu yang keren, silakan bagikan di komentar di bawah! Atau jika Anda memiliki pertanyaan atau klarifikasi tentang apa pun, saya akan dengan senang hati membantu.