Gerçek Zamanlı Çok Oyunculu Sanal Gerçeklik Oyunu Nasıl Oluşturulur (Bölüm 2)
Yayınlanan: 2022-03-10Bu eğitim serisinde, oyuncuların bir bulmacayı çözmek için işbirliği yapması gereken web tabanlı çok oyunculu bir sanal gerçeklik oyunu oluşturacağız. Bu serinin ilk bölümünde oyunda yer alan küreleri tasarladık. Serinin bu bölümünde, oyun mekaniklerini ekleyeceğiz ve oyuncu çiftleri arasında iletişim protokolleri ayarlayacağız.
Buradaki oyun açıklaması serinin ilk bölümünden alınmıştır: Her oyuncu çiftine bir küre halkası verilir. Amaç, yükseltilmiş ve parlaksa bir kürenin “açık” olduğu tüm küreleri “açmaktır”. Bir küre daha alçak ve loşsa "kapalı"dır. Bununla birlikte, belirli "baskın" küreler komşularını etkiler: durum değiştirirse, komşuları da durum değiştirir. Oyuncu 2 çift sayılı küreleri kontrol edebilir ve oyuncu 1 tek sayılı küreleri kontrol edebilir. Bu, her iki oyuncuyu da bulmacayı çözmek için işbirliği yapmaya zorlar.
Bu eğitimdeki 8 adım 3 bölüme ayrılmıştır:
- Kullanıcı Arayüzünü Doldurma (1. ve 2. Adımlar)
- Oyun Mekaniği Ekleyin (3 - 5 arasındaki Adımlar)
- İletişim Kurulumu (Adım 6 - 8)
Bu bölüm, herkesin oynayabileceği, tamamen işlevsel bir çevrimiçi demo ile sona erecek. A-Frame VR ve birkaç A-Frame uzantısı kullanacaksınız.
Bitmiş kaynak kodunu burada bulabilirsiniz.
1. Görsel Göstergeler Ekleyin
Başlamak için, bir kürenin kimliğinin görsel göstergelerini ekleyeceğiz. #container-orb0
öğesinin ilk alt öğesi olarak yeni a-text
VR öğesi ekleyin.
<a-entity ...> <a-text class="orb-id" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="3 3 3" position="0 -2 -0.25" material="side:double"></a-text> ... <a-entity position...> ... </a-entity> </a-entity>
Bir kürenin “bağımlılıkları”, değiştirildiğinde değiştireceği kürelerdir: örneğin, küre 1'in bağımlılık olarak küreler 2 ve 3'e sahip olduğunu söyleyin. Bu, küre 1 değiştirilirse, küreler 2 ve 3'ün de değiştirileceği anlamına gelir. .animation-position
'dan hemen sonra, bağımlılıkların görsel göstergelerini aşağıdaki gibi ekleyeceğiz.
<a-animation class="animation-position" ... /> <a-text class="dep-right" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="10 10 10" position="0 0 1" material="side:double" ></a-text> <a-text class="dep-left" opacity="0.25"rotation="0 -90 0" value="1" color="#FFF" scale="10 10 10" position="0 0 -3" material="side:double" ></a-text>
Kodunuzun 1. Adım için kaynak kodumuzla eşleştiğini doğrulayın. Küreniz şimdi aşağıdakilerle eşleşmelidir:
Bu, ihtiyaç duyacağımız ek görsel göstergeleri sonuçlandırıyor. Ardından, bu şablon küresini kullanarak VR sahnesine dinamik olarak küreler ekleyeceğiz.
2. Dinamik Olarak Küre Ekle
Bu adımda, bir seviyenin JSON-esque belirtimine göre küreler ekleyeceğiz. Bu, yeni seviyeleri kolayca belirlememizi ve oluşturmamızı sağlar. Bölüm 1'deki son adımdaki küreyi şablon olarak kullanacağız.
Başlamak için, jQuery'yi içe aktarın, çünkü bu DOM değişikliklerini ve dolayısıyla VR sahnesindeki değişiklikleri daha kolay hale getirecektir. A-Frame içe aktarma işleminden hemen sonra, aşağıdakileri L8'e ekleyin:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Bir dizi kullanarak bir seviye belirtin. Dizi, her bir kürenin "bağımlılıklarını" kodlayan nesne değişmezlerini içerecektir. <head>
etiketinin içine aşağıdaki düzey yapılandırmasını ekleyin:
<script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>
Şimdilik, her kürenin “sağına” ve “soluna” yalnızca bir bağımlılığı olabilir. Yukarıda orbs
bildirdikten hemen sonra, sayfa yüklendiğinde çalışacak bir işleyici ekleyin. Bu işleyici (1) şablon küresini çoğaltacak ve (2) sağlanan seviye konfigürasyonunu kullanarak şablon küresini kaldıracaktır:
$(document).ready(function() { function populateTemplate(orb, template, i, total) {} function remove(selector) {} for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; var template = $('#template').clone(); template = populateTemplate(orb, template, i, orbs.length); $('#carousel').append(template); } remove('#template'); } function clickOrb(i) {}
Ardından, bir seçici verildiğinde VR sahnesinden bir öğeyi kaldıran remove
işlevini doldurun. Neyse ki, A-Frame, DOM'deki değişiklikleri gözlemler ve bu nedenle, öğeyi DOM'den çıkarmak, onu VR sahnesinden çıkarmak için yeterlidir. remove
işlevini aşağıdaki gibi doldurun.
function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }
Bir küre üzerindeki tıklama eylemini basitçe tetikleyen clickOrb
işlevini doldurun.
function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }
Ardından, populateTemplate
işlevini yazmaya başlayın. Bu işlevde, .container
alarak başlayın. Küre için bu kap, ayrıca önceki adımda eklediğimiz görsel göstergeleri içerir. Ayrıca, bağımlılıklarına bağlı olarak kürenin onclick
davranışını değiştirmemiz gerekecek. Bir sola bağımlılık varsa, bunu yansıtmak için hem görsel göstergeyi hem de onclick
davranışını değiştirin; aynısı bir sağa bağımlılık için de geçerlidir:
function populateTemplate(orb, template, i, total) { var container = template.find('.container'); var onclick = 'document.querySelector("#light-orb' + i + '").emit("switch");'; if (orb.left || orb.right) { if (orb.left) { onclick += 'clickOrb(' + orb.left + ');'; container.find('.dep-left').attr('value', orb.left); } if (orb.right) { onclick += 'clickOrb(' + orb.right + ');'; container.find('.dep-right').attr('value', orb.right); } } else { container.find('.dep-left').remove(); container.find('.dep-right').remove(); } }
Yine de populateTemplate
işlevinde, küre kimliğinin tümünde ve kapsayıcısının öğelerinde küre kimliğini doğru şekilde ayarlayın.
container.find('.orb-id').attr('value', i); container.attr('id', 'container-orb' + i); template.find('.orb').attr('id', 'orb' + i); template.find('.light-orb').attr('id', 'light-orb' + i); template.find('.clickable').attr('data-id', i);
Yine de populateTemplate
işlevinde, onclick
davranışını ayarlayın, rastgele çekirdeği ayarlayın, böylece her bir küre görsel olarak farklı olur ve son olarak, kürenin dönüş konumunu kimliğine göre ayarlayın.
container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');
İşlevin sonunda, template
yukarıdaki tüm konfigürasyonlarla birlikte döndürün.
return template;
Belge yükleme işleyicisinin içinde ve remove('#template')
ile şablonu kaldırdıktan sonra, başlangıçta açık olacak şekilde yapılandırılan küreleri açın.
$(document).ready(function() { ... setTimeout(function() { for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; if (orb.on) { document.querySelector("#container-orb" + i).click(); } } }, 1000); });
Bu, Javascript değişikliklerini tamamlar. Ardından, şablonun varsayılan ayarlarını 'kapalı' bir küreninkiyle değiştireceğiz. #container-orb0
için konumu ve ölçeği aşağıdaki şekilde değiştirin:
position="8 0.5 0" scale="0.5 0.5 0.5"
Ardından, #light-orb0
yoğunluğunu 0 olarak değiştirin.
intensity="0"
Kaynak kodunuzun 2. Adım için kaynak kodumuzla eşleştiğini doğrulayın.
VR sahneniz artık dinamik olarak doldurulmuş 5 küre içermelidir. Kürelerden biri ayrıca aşağıdaki gibi görsel bağımlılık göstergelerine sahip olmalıdır:
Bu, dinamik olarak küre eklemedeki ilk bölümü tamamlar. Bir sonraki bölümde oyun mekaniklerini ekleyerek üç adım atacağız. Spesifik olarak, oyuncu, oyuncu kimliğine bağlı olarak yalnızca belirli küreler arasında geçiş yapabilecektir.
3. Terminal Durumu Ekle
Bu adımda, bir terminal durumu ekleyeceğiz. Tüm küreler başarıyla açılırsa, oyuncu bir "zafer" sayfası görür. Bunu yapmak için, tüm kürelerin durumunu izlemeniz gerekecek. Bir küre her açılıp kapatıldığında, dahili durumumuzu güncellememiz gerekecek. toggleOrb
bir yardımcı işlevin bizim için güncelleme durumu olduğunu söyleyin. Bir kürenin durumu her değiştiğinde toggleOrb
işlevini çağırın: (1) aşırı yük işleyicisine bir tıklama dinleyicisi ekleyin ve (2) bir toggleOrb(i);
clickOrb
için çağrı. Son olarak, (3) boş bir toggleOrb
tanımlayın.
$(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }
Basit olması için, oyun durumunu belirtmek için seviye konfigürasyonumuzu kullanacağız. ith küresinin on
durumunu değiştirmek için toggleOrb
kullanın. toggleOrb
, tüm küreler açıksa ayrıca bir terminal durumunu tetikleyebilir.
function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
Kodunuzun 3. Adım için kaynak kodumuzla eşleşip eşleşmediğini iki kez kontrol edin.
Bu, oyun için "tek oyuncu" modunu tamamlar. Bu noktada tamamen işlevsel bir sanal gerçeklik oyununuz var. Ancak şimdi çok oyunculu bileşeni yazmanız ve oyun mekaniği aracılığıyla işbirliğini teşvik etmeniz gerekecek.
4. Oyuncu Nesnesi Oluşturun
Bu adımda, oyuncu kimliğine sahip bir oyuncu için bir soyutlama oluşturacağız. Bu oyuncu kimliği daha sonra sunucu tarafından atanacaktır.
Şimdilik, bu sadece global bir değişken olacak. orbs
tanımladıktan hemen sonra bir oyuncu kimliği tanımlayın:
var orbs = ... var current_player_id = 1;
Kodunuzun 4. Adım için kaynak kodumuzla eşleşip eşleşmediğini iki kez kontrol edin. Sonraki adımda, oynatıcının hangi küreleri kontrol edebileceğini belirlemek için bu oyuncu kimliği kullanılacaktır.
5. Koşullu Küreleri Aç/Kapat
Bu adımda, küre değiştirme davranışını değiştireceğiz. Spesifik olarak, oyuncu 1 tek sayılı küreleri kontrol edebilir ve oyuncu 2 çift sayılı küreleri kontrol edebilir. İlk olarak, bu mantığı kürelerin durum değiştirdiği her iki yerde de uygulayın:
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
İkinci olarak, clickOrb
allowedToToggle
tanımlayın. Mevcut oyuncu oyuncu 1 ise, tek sayılı kimlikler bir doğruluk-y değeri döndürür ve böylece 1. oyuncunun tek sayılı küreleri kontrol etmesine izin verilir. Oyuncu 2 için bunun tersi geçerlidir. Diğer tüm oyuncuların küreleri kontrol etmesine izin verilmez.
function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }
Kodunuzun 5. Adım için kaynak kodumuzla eşleşip eşleşmediğini iki kez kontrol edin. Varsayılan olarak, oyuncu 1. oyuncudur. Bu, 1. oyuncu olarak önizlemenizde yalnızca tek sayılı küreleri kontrol edebileceğiniz anlamına gelir. Bu, oyun mekaniği ile ilgili bölümü sonlandırıyor.
Bir sonraki bölümde, bir sunucu aracılığıyla iki oyuncu arasındaki iletişimi kolaylaştıracağız.
6. WebSocket ile Sunucu Kurulumu
Bu adımda, (1) oyuncu kimliklerini ve (2) geçiş mesajlarını takip etmek için basit bir sunucu kuracaksınız. Bu mesajlar oyun durumunu içerecektir, böylece oyuncular her birinin diğerinin gördüklerini gördüğünden emin olabilir.
İstemci tarafı kaynak kodu olarak önceki index.html
dosyanıza başvuracağız. Bu adımda koda sunucu tarafı kaynak kodu olarak değineceğiz. glitch.com'a gidin, sağ üstteki "yeni proje"ye tıklayın ve açılır menüden "merhaba-ekspres"e tıklayın.
Soldaki panelden “package.json” öğesini seçin ve dependencies
socket-io
ekleyin. dependencies
sözlüğünüz şimdi aşağıdakilerle eşleşmelidir.
"dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },
Sol panelden “index.js”yi seçin ve bu dosyanın içeriğini aşağıdaki minimal socket.io Hello World ile değiştirin:
const express = require("express"); const app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); /** * Run application on port 3000 */ var port = process.env.PORT || 3000; http.listen(port, function(){ console.log('listening on *:', port); });
Yukarıdakiler, temel bir ekspres uygulama için soket.io'yu 3000 numaralı bağlantı noktasında kurar. Ardından, biri aktif oyuncuların listesini korumak için, diğeri ise atanmamış en küçük oyuncu kimliğini korumak için olmak üzere iki global değişken tanımlayın.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
Ardından, yeni bir oyuncu kimliği oluşturan ve yeni oyuncu kimliğini playerIds
dizisine ekleyerek "alındı" olarak işaretleyen getPlayerId
işlevini tanımlayın. Özellikle, işlev en küçükPlayerId öğesini işaretler ve ardından bir sonraki en smallestPlayerId
alınmamış tamsayıyı arayarak smallestPlayerId
öğesini günceller.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
En smallestPlayerId
uygun şekilde güncelleyen ve sağlanan playerId
başka bir oyuncunun bu kimliği alabilmesi için serbest removePlayer
işlevini tanımlayın.
function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }
Son olarak, yukarıdaki yöntem çiftini kullanarak yeni oyuncuları kaydeden ve bağlantısı kesilmiş oyuncuların kaydını iptal eden bir çift soket olay işleyicisi tanımlayın.
/** * Handle socket interactions */ io.on('connection', function(socket) { socket.on('newPlayer', function() { socket.playerId = getPlayerId(); console.log("new player: ", socket.playerId); socket.emit('playerId', socket.playerId); }); socket.on('disconnect', function() { if (socket.playerId === undefined) return; console.log("disconnected player: ", socket.playerId); removePlayer(socket.playerId); }); });
Kodunuzun Adım 6 için kaynak kodumuzla eşleşip eşleşmediğini iki kez kontrol edin. Bu, temel oyuncu kaydı ve kayıt silme işlemini tamamlar. Her istemci artık sunucu tarafından oluşturulan oyuncu kimliğini kullanabilir.
Bir sonraki adımda, istemciyi sunucu tarafından yayılan oyuncu kimliğini alacak ve kullanacak şekilde değiştireceğiz.
7. Oyuncu Kimliğini Uygulayın
Bu sonraki iki adımda, çok oyunculu deneyimin ilkel bir sürümünü tamamlayacağız. Başlamak için, oyuncu kimliği atamasını istemci tarafına entegre edin. Özellikle, her müşteri sunucudan bir oyuncu kimliği isteyecektir. Adım 4'te ve daha önce çalıştığımız istemci tarafı index.html
geri dönün.
socket.io
head
içe aktarın:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
Belge yükleme işleyicisinden sonra, soketin örneğini oluşturun ve bir newPlayer
olayı yayınlayın. Yanıt olarak, sunucu tarafı, playerId
olayını kullanarak yeni bir oyuncu kimliği oluşturacaktır. Aşağıda, lightful.glitch.me
yerine Glitch proje önizlemeniz için URL'yi kullanın. Aşağıdaki demo URL'yi kullanabilirsiniz, ancak yaptığınız herhangi bir kod değişikliği elbette yansıtılmayacaktır.
$(document).ready(function() { ... }); socket = io("https://lightful.glitch.me"); socket.emit('newPlayer'); socket.on('playerId', function(player_id) { current_player_id = player_id; console.log(" * You are now player", current_player_id); });
Kodunuzun 7. Adım için kaynak kodumuzla eşleştiğini doğrulayın. Şimdi, çok oyunculu bir oyunun iki tarafını oynamak için oyununuzu iki farklı tarayıcıya veya sekmeye yükleyebilirsiniz. Oyuncu 1 tek sayılı küreleri kontrol edebilecek ve oyuncu 2 çift sayılı küreleri kontrol edebilecek.
Ancak, 1. oyuncu için küreler arasında geçiş yapmanın 2. oyuncu için küre durumunu etkilemeyeceğini unutmayın. Ardından, oyun durumlarını senkronize etmemiz gerekiyor.
8. Oyun Durumunu Senkronize Et
Bu adımda, 1. ve 2. oyuncuların aynı küre durumlarını görmeleri için oyun durumlarını senkronize edeceğiz. 1. oyuncu için küre 1 açıksa, 2. oyuncu için de açık olmalıdır. İstemci tarafında, küre geçişlerini hem anons edeceğiz hem de dinleyeceğiz. Duyurmak için, değiştirilen kürenin kimliğini ileteceğiz.
Her iki toggleOrb
çağrısından önce, aşağıdaki socket.emit
çağrısını ekleyin.
$(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }
Ardından, küre geçişlerini dinleyin ve ilgili küreyi değiştirin. playerId
soket olay dinleyicisinin hemen altına, toggleOrb
olayı için başka bir dinleyici ekleyin.
socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });
Bu, istemci tarafı kodunda yapılan değişiklikleri tamamlar. Kodunuzun, Adım 8 için kaynak kodumuzla eşleşip eşleşmediğini iki kez kontrol edin.
Sunucu tarafının artık değiştirilen küre kimliğini alması ve yayınlaması gerekiyor. Sunucu tarafı index.js
aşağıdaki dinleyiciyi ekleyin. Bu dinleyici doğrudan soket disconnect
dinleyicisinin altına yerleştirilmelidir.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
Kodunuzun 8. Adım için kaynak kodumuzla eşleştiğini iki kez kontrol edin. Şimdi, bir pencerede yüklenen 1. oyuncu ve ikinci bir pencerede yüklenen 2. oyuncu aynı oyun durumunu görecektir. Bununla çok oyunculu bir sanal gerçeklik oyununu tamamladınız. Ayrıca iki oyuncu, hedefi tamamlamak için işbirliği yapmalıdır. Nihai ürün aşağıdakilerle eşleşecektir.
Çözüm
Bu, çok oyunculu bir sanal gerçeklik oyunu yaratma konusundaki eğitimimizi tamamlıyor. Bu süreçte, A-Frame VR'de 3 boyutlu modelleme ve WebSockets kullanan gerçek zamanlı çok oyunculu deneyimler de dahil olmak üzere bir dizi konuya değindiniz.
Dokunduğumuz kavramlardan yola çıkarak, iki oyuncu için daha sorunsuz bir deneyim nasıl sağlarsınız? Bu, oyun durumunun senkronize olup olmadığını kontrol etmeyi ve aksi takdirde kullanıcıyı uyarmayı içerebilir. Terminal durumu ve oynatıcı bağlantı durumu için basit görsel göstergeler de yapabilirsiniz.
Oluşturduğumuz çerçeve ve tanıttığımız kavramlar göz önüne alındığında, artık bu soruları yanıtlayacak ve çok daha fazlasını inşa edecek araçlara sahipsiniz.
Bitmiş kaynak kodunu burada bulabilirsiniz.