Mobil Kontrolcü Geliştirme (Controller)
RemoteNex sisteminde mobil kontrolcüler, oyuncuların telefonlarında çalışan web tabanlı (HTML/CSS/JavaScript) arayüzlerdir.
Bu arayüzler React Native WebView içinde çalışır ve SignalR sunucusu üzerinden Unity oyunu ile haberleşir.
HTML5 + Vanilla JavaScript (Framework gerekmez) React Native WebView (Wrapper) SignalR (Backend iletişim)
📡 İletişim Protokolü
1️⃣ Kontrolcüden Sunucuya (Input Gönderme)
function send(msg) {
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(msg);
} else {
console.log("OUT:", msg); // Debug için
}
}
Mesaj Formatları:
// Hareket inputları
send("MOVE:UP:PRESS");
send("MOVE:UP:RELEASE");
send("MOVE:DOWN:PRESS");
send("MOVE:LEFT:PRESS");
// Özel komutlar
send("MOVE:CMD:START_GAME");
send("MOVE:CMD:RESTART_GAME");
send("MOVE:CMD:BUZZ"); // Quiz oyunları için
send("MOVE:CMD:ANS_2"); // Cevap seçimi
Tüm mesajlar MOVE: ile başlamalı. Sunucu bu prefix'i bekler.
2️⃣ Sunucudan Kontrolcüye (Durum Güncellemeleri)
Sunucu Unity'den gelen durum değişikliklerini kontrolcülere iletir:
window.handleServerMessage = function(rawData) {
if (!rawData) return;
// Mesajdan STATE kısmını çıkar
let msg = rawData.includes("STATE:")
? rawData.substring(rawData.indexOf("STATE:"))
: rawData;
let parts = msg.split(':');
let action = parts[1];
// Durum kontrolü
if (action === "LOBBY") {
showScreen('lobby-screen');
}
else if (action === "GAME_STARTED") {
showScreen('game-area');
}
else if (action === "GAME_OVER") {
showScreen('result-screen');
}
else if (action === "SCORE") {
// Skor güncelle
let p1Score = parts[2];
let p2Score = parts[3];
updateScoreDisplay(p1Score, p2Score);
}
};
// WebView'dan gelen mesajları dinle
document.addEventListener("message", function(event) {
window.handleServerMessage(event.data);
});
window.addEventListener("message", function(event) {
window.handleServerMessage(event.data);
});
Sunucudan Gelebilecek Mesajlar:
STATE:LOBBY // Lobiye dön
STATE:GAME_STARTED // Oyun başladı
STATE:GAME_OVER // Oyun bitti
STATE:SCORE:5:3 // P1: 5, P2: 3
STATE:BUZZ_WINNER:abc123 // Buzzer'ı kazanan
STATE:LOCK_INPUT // Inputları kilitle
STATE:UNLOCK_INPUT // Inputları aç
🎨 Ekran Yönetimi
Çok Ekranlı Yapı (Overlay Pattern)
<!-- Lobby Ekranı -->
<div id="lobby-screen" class="overlay-screen visible">
<button id="btn-start" class="master-btn t-txt">OYUNU BAŞLAT</button>
</div>
<!-- Oyun Ekranı -->
<div id="game-area">
<div id="btn-up" class="control-btn" data-dir="UP">▲</div>
<div id="btn-down" class="control-btn" data-dir="DOWN">▼</div>
</div>
<!-- Sonuç Ekranı -->
<div id="result-screen" class="overlay-screen">
<div class="info-text t-txt">OYUN BİTTİ</div>
<button id="btn-restart" class="master-btn t-txt">TEKRAR OYNA</button>
</div>
CSS:
.overlay-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
background: #000;
z-index: 100;
}
.visible {
display: flex !important;
}
JavaScript:
function showScreen(id) {
// Tüm ekranları gizle
['lobby-screen', 'game-area', 'result-screen'].forEach(sid => {
document.getElementById(sid).classList.remove('visible');
document.getElementById(sid).style.display = 'none';
});
// İstenen ekranı göster
const el = document.getElementById(id);
el.classList.add('visible');
el.style.display = (id === 'game-area') ? 'flex' : 'flex';
}
🎮 Input Sistemi
Modern Pointer Events (Önerilen)
Eski touchstart/touchend yerine Pointer Events kullanın:
function attachButtonEvents(btn, commandPress, commandRelease) {
btn.addEventListener('pointerdown', function(e) {
e.preventDefault();
btn.setPointerCapture(e.pointerId);
btn.classList.add('pressed');
triggerHaptic(); // Titreşim feedback'i
if (commandPress) send(commandPress);
// START_GAME komutuysa ekranı değiştir
if (commandPress && commandPress.includes("START_GAME")) {
showScreen('game-area');
}
});
const release = function(e) {
e.preventDefault();
btn.releasePointerCapture(e.pointerId);
if (btn.classList.contains('pressed')) {
btn.classList.remove('pressed');
if (commandRelease) send(commandRelease);
}
};
btn.addEventListener('pointerup', release);
btn.addEventListener('pointercancel', release);
btn.addEventListener('pointerleave', release);
}
✅ Touch, mouse ve stylus destekler ✅ Multi-touch sorunlarını çözer ✅ Ghost click problemini önler ✅ Daha iyi performans
Kullanım Örnekleri
// Hareket butonları
const controls = document.querySelectorAll('.control-btn');
controls.forEach(btn => {
const dir = btn.getAttribute('data-dir');
attachButtonEvents(btn, `MOVE:${dir}:PRESS`, `MOVE:${dir}:RELEASE`);
});
// Özel komut butonları
const btnStart = document.getElementById('btn-start');
attachButtonEvents(btnStart, "MOVE:CMD:START_GAME", null);
const btnRestart = document.getElementById('btn-restart');
attachButtonEvents(btnRestart, "MOVE:CMD:RESTART_GAME", null);
📱 Mobil Optimizasyonlar
1. Context Menu Engelleme
window.oncontextmenu = function(event) {
event.preventDefault();
event.stopPropagation();
return false;
};
2. Haptic Feedback (Titreşim)
function triggerHaptic() {
if (navigator.vibrate) {
navigator.vibrate(40); // 40ms titreşim
}
}
3. Touch Action Engelleme
* {
touch-action: none;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
4. Viewport Ayarları
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
🌍 Çok Dilli Destek (SmartTranslate)
Sistem Mimarisi
- Sayfa yüklenirken Türkçe metinleri topla
- React Native'e çeviri isteği gönder
- Çevrilen metinleri uygula
- Yükleme ekranını gizle
Implementasyon
const SmartTranslate = {
map: {},
init: function() {
// Çevrilecek metinleri topla
const elements = document.querySelectorAll('.t-txt');
let texts = [];
elements.forEach(el => {
let txt = el.innerText.trim();
if (txt && !texts.includes(txt)) texts.push(txt);
});
// Dinamik metinler (runtime'da gösterilecekler)
const dynamicTerms = ["KAZANDIN", "KAYBETTİN", "OYUN BİTTİ"];
dynamicTerms.forEach(t => {
if (!texts.includes(t)) texts.push(t);
});
// React Native'e çeviri isteği gönder
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(
"REQ_TRANS:" + JSON.stringify(texts)
);
// Timeout: 5 saniye sonra loading'i gizle
setTimeout(() => { this.hideOverlay(); }, 5000);
} else {
this.hideOverlay();
}
},
apply: function(dictionary) {
this.map = dictionary;
// Tüm çevrilecek elementleri güncelle
document.querySelectorAll('.t-txt').forEach(el => {
if (!el.getAttribute('data-org')) {
el.setAttribute('data-org', el.innerText.trim());
}
const key = el.getAttribute('data-org');
if (this.map[key]) {
el.innerText = this.map[key];
}
});
this.hideOverlay();
},
hideOverlay: function() {
const overlay = document.getElementById('translation-overlay');
if (overlay && overlay.style.display !== 'none') {
overlay.style.opacity = '0';
setTimeout(() => { overlay.style.display = 'none'; }, 500);
}
},
get: function(text) {
return this.map[text] || text;
}
};
// Kısayol fonksiyon
function _t(text) {
return SmartTranslate.get(text);
}
HTML'de Kullanım
<!-- Yükleme Ekranı -->
<div id="translation-overlay">
<div class="spinner"></div>
</div>
<!-- Çevrilecek metinler -->
<button class="t-txt">OYUNU BAŞLAT</button>
<div class="t-txt">OYUN BİTTİ</div>
<span class="t-txt">TEKRAR OYNA</span>
React Native Callback
// React Native bu fonksiyonu çağırır
window.applyTranslations = function(jsonString) {
try {
const dict = JSON.parse(jsonString);
SmartTranslate.apply(dict);
} catch(e) {
console.error("Translation error:", e);
SmartTranslate.hideOverlay();
}
};
📐 Orientation (Yönlendirme) Kontrolü
window.onload = function() {
SmartTranslate.init();
if (window.ReactNativeWebView) {
// Ekran yönünü ayarla
window.ReactNativeWebView.postMessage("CMD:ORIENT:PORTRAIT");
// veya
// window.ReactNativeWebView.postMessage("CMD:ORIENT:LANDSCAPE");
// Hazır olduğunu bildir
setTimeout(() => {
window.ReactNativeWebView.postMessage("CMD:READY");
}, 500);
}
}
Orientation Seçenekleri:
CMD:ORIENT:PORTRAIT- Dikey (Pong, Quiz)CMD:ORIENT:LANDSCAPE- Yatay (Racing, Platformer)
📋 Controller Kontrol Listesi
Bir kontrolcü geliştirirken bunlar mutlaka olmalı:
Temel Gereksinimler
- ✅
send()fonksiyonu - ✅
handleServerMessage()fonksiyonu - ✅ Pointer events ile input yönetimi
- ✅ Ekran geçiş sistemi (
showScreen()) - ✅
window.onloadveCMD:READYsinyali
Optimizasyonlar
- ✅ Context menu engelleme
- ✅ Touch action engelleme (CSS)
- ✅ Viewport ayarları
- ⚡ Haptic feedback (Önerilen)
- ⚡ SmartTranslate sistemi (Önerilen)
- ⚡ Orientation kontrolü (Oyuna göre)
Mesaj Protokolleri
- ✅
MOVE:ACTION:STATEformatı - ✅
MOVE:CMD:COMMANDformatı - ✅
STATE:mesajlarını dinleme
🐛 Sık Yapılan Hatalar
1. Pointer Capture Unutulması
// ❌ YANLIŞ - Multi-touch'ta sorun çıkar
btn.addEventListener('pointerdown', (e) => {
send("MOVE:UP:PRESS");
});
// ✅ DOĞRU
btn.addEventListener('pointerdown', (e) => {
e.preventDefault();
btn.setPointerCapture(e.pointerId); // Bu satır kritik!
send("MOVE:UP:PRESS");
});
2. Message Listener Eksikliği
// ❌ YANLIŞ - Sadece biri yeterli değil
document.addEventListener("message", handleServerMessage);
// ✅ DOĞRU - İkisi de olmalı
document.addEventListener("message", function(e) {
window.handleServerMessage(e.data);
});
window.addEventListener("message", function(e) {
window.handleServerMessage(e.data);
});
3. Translation Overlay Unutulması
// ❌ YANLIŞ - Kullanıcı sonsuz loading görür
SmartTranslate.init();
// Hiç overlay.hide() çağrılmamış!
// ✅ DOĞRU - Timeout ile garanti
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage("REQ_TRANS:" + JSON.stringify(texts));
setTimeout(() => { this.hideOverlay(); }, 5000); // Fallback
}
🎯 Örnek: Tam Controller Şablonu
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>My Game Controller</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
* {
touch-action: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body { margin: 0; height: 100vh; overflow: hidden; }
.overlay-screen {
position: absolute;
width: 100%;
height: 100%;
display: none;
}
.visible { display: flex !important; }
</style>
</head>
<body>
<!-- Yükleme Ekranı -->
<div id="translation-overlay"><div class="spinner"></div></div>
<!-- Lobby -->
<div id="lobby-screen" class="overlay-screen visible">
<button id="btn-start" class="t-txt">BAŞLAT</button>
</div>
<!-- Oyun -->
<div id="game-area">
<div data-dir="UP">▲</div>
<div data-dir="DOWN">▼</div>
</div>
<!-- Sonuç -->
<div id="result-screen" class="overlay-screen">
<button id="btn-restart" class="t-txt">TEKRAR</button>
</div>
<script>
// SmartTranslate (yukarıdaki kod)
const SmartTranslate = { /* ... */ };
// Temel fonksiyonlar
function send(msg) {
if (window.ReactNativeWebView)
window.ReactNativeWebView.postMessage(msg);
}
function triggerHaptic() {
if (navigator.vibrate) navigator.vibrate(40);
}
function showScreen(id) { /* ... */ }
// Sunucu mesajları
window.handleServerMessage = function(data) { /* ... */ };
document.addEventListener("message", (e) => handleServerMessage(e.data));
window.addEventListener("message", (e) => handleServerMessage(e.data));
// Buton events
function attachButtonEvents(btn, cmdPress, cmdRelease) { /* ... */ }
// Başlangıç
window.onload = function() {
SmartTranslate.init();
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage("CMD:ORIENT:PORTRAIT");
setTimeout(() => {
window.ReactNativeWebView.postMessage("CMD:READY");
}, 500);
}
}
window.oncontextmenu = (e) => { e.preventDefault(); return false; };
// Butonları bağla
document.querySelectorAll('[data-dir]').forEach(btn => {
const dir = btn.getAttribute('data-dir');
attachButtonEvents(btn, `MOVE:${dir}:PRESS`, `MOVE:${dir}:RELEASE`);
});
</script>
</body>
</html>