Unity Oyun Entegrasyonu (Host)
Bu döküman, Unity oyununuzun RemoteNex sistemi ile nasıl haberleşeceğini, SignalR sunucusu ile nasıl veri alışverişi yapacağını ve mobil kontrolcülerden gelen inputları nasıl işleyeceğini açıklar.
Unity (Host) ↔ SignalR Server ↔ Mobil Kontrolcüler (HTML/React Native)
📡 RemoteNex API Referansı
Unity'deki oyununuz, RemoteNex adlı bir singleton sınıf üzerinden sunucu ile iletişim kurar. Bu sınıf projenizde hazır gelir.
Temel Metodlar
// Sunucuya mesaj gönderme
RemoteNex.SendData(string message);
// Sunucudan gelen mesajları dinleme (Event)
RemoteNex.OnInputReceived += YourHandlerMethod;
🎯 Zorunlu Sistem Gereksinimleri
Her oyun manager'ında mutlaka bulunması gereken yapılar:
1️⃣ Event Subscription (Zorunlu)
void OnEnable()
{
RemoteNex.OnInputReceived += OnData;
}
void OnDisable()
{
RemoteNex.OnInputReceived -= OnData;
}
OnDisable'da event'i temizlemezseniz memory leak oluşur!
2️⃣ Oyuncu Veri Modeli (Zorunlu)
[System.Serializable]
public class PlayerData
{
public string id; // Benzersiz Connection ID
public string displayName; // Oyuncu ismi
public string role; // "M" (Master) veya "N" (Normal)
}
private Dictionary<string, PlayerData> players = new Dictionary<string, PlayerData>();
Connection ID ile hızlı erişim için Dictionary kullanılır. List yerine Dictionary tercih edin.
3️⃣ Merkezi Mesaj Handler (Zorunlu)
void OnData(string raw)
{
// 1. Oyuncu listesi güncellemesi
if (raw.Contains("PLAYERS:"))
{
ParsePlayerList(raw);
return;
}
// 2. Rate limiting (Sunucuyu koruma)
if (Time.time - lastProcessTime < 0.05f) return;
lastProcessTime = Time.time;
// 3. Oyun komutları
if (raw.Contains("START_GAME") || raw.Contains("RESTART_GAME"))
{
StartGame();
return;
}
// 4. Input komutları
if (raw.Contains(":MOVE:"))
{
ProcessMovement(raw);
}
}
📨 Mesaj Protokolleri
Sunucudan Gelen Mesaj Tipleri
1. Oyuncu Listesi
FORMAT: ROOM_CODE:PLAYERS:NAME1:ROLE1:ID1,NAME2:ROLE2:ID2,...
ÖRNEK:
123456:PLAYERS:Ali:M:abc123def456,Veli:N:xyz789ghi012
AÇIKLAMA:
- Ali → Master (M)
- Veli → Normal oyuncu (N)
Parse Kodu:
void ParsePlayerList(string raw)
{
int idx = raw.IndexOf("PLAYERS:");
if (idx == -1) return;
string listPart = raw.Substring(idx + 8);
string[] userList = listPart.Split(',');
foreach (var user in userList)
{
string[] parts = user.Split(new char[] { ':' },
System.StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 3)
{
string playerName = parts[0].Trim();
string playerRole = parts[1].Trim();
string playerId = parts[2].Trim();
if (!players.ContainsKey(playerId))
{
players.Add(playerId, new PlayerData
{
id = playerId,
displayName = playerName,
role = playerRole
});
}
else
{
// Oyuncu zaten var, sadece güncelle
players[playerId].displayName = playerName;
players[playerId].role = playerRole;
}
}
}
}
2. Input Mesajları (Hareket)
FORMAT: SENDER_ID:MOVE:ACTION:STATE
ÖRNEKLER:
abc123def456:MOVE:UP:PRESS
abc123def456:MOVE:UP:RELEASE
xyz789ghi012:MOVE:DOWN:PRESS
Parse Kodu:
void ProcessMovement(string raw)
{
string[] parts = raw.Split(':');
if (parts.Length < 4) return;
string senderId = parts[0].Trim();
string action = parts[2].Trim(); // UP, DOWN, LEFT, RIGHT, FIRE, etc.
string state = parts[3].Trim(); // PRESS, RELEASE
if (!players.ContainsKey(senderId))
{
Debug.LogWarning($"Unknown player: {senderId}");
return;
}
// Değer hesaplama
float value = 0;
if (state == "PRESS")
{
value = (action == "UP") ? 1 : (action == "DOWN") ? -1 : 0;
}
// Oyuncuya göre işlem
bool isMaster = players[senderId].role == "M";
if (isMaster)
{
moveP1 = value;
}
else
{
moveP2 = value;
}
}
3. Özel Komutlar
FORMAT: SENDER_ID:MOVE:CMD:COMMAND_NAME
ÖRNEKLER:
abc123def456:MOVE:CMD:START_GAME
abc123def456:MOVE:CMD:RESTART_GAME
xyz789ghi012:MOVE:CMD:BUZZ (Quiz oyunları için)
xyz789ghi012:MOVE:CMD:ANS_2 (Cevap seçimi)
Parse Kodu:
void OnData(string raw)
{
// ... önceki kontroller
// CMD formatı kontrolü
string[] parts = raw.Split(':');
for (int i = 0; i < parts.Length; i++)
{
if (parts[i] == "CMD" && i + 1 < parts.Length)
{
string command = parts[i + 1].Trim();
if (command == "START_GAME") StartGame();
else if (command == "RESTART_GAME") StartGame();
else if (command == "BUZZ") HandleBuzz(parts[0]);
else if (command.StartsWith("ANS_"))
{
int answerIndex = int.Parse(command.Split('_')[1]);
CheckAnswer(parts[0], answerIndex);
}
}
}
}
📤 Unity'den Sunucuya Mesaj Gönderme
Oyun Durumu Güncellemeleri
// Lobi durumu
RemoteNex.SendData("STATE:LOBBY");
// Oyun başladı
RemoteNex.SendData("STATE:GAME_STARTED");
// Oyun bitti
RemoteNex.SendData("STATE:GAME_OVER");
// Skor güncellemesi
RemoteNex.SendData($"STATE:SCORE:{scoreP1}:{scoreP2}");
Güvenilir Mesaj Gönderimi
Kritik durum değişikliklerinde mesajı 5 kez tekrarla:
IEnumerator SendStateRoutine(string message)
{
for (int i = 0; i < 5; i++)
{
RemoteNex.SendData(message);
yield return new WaitForSeconds(0.15f);
}
}
// Kullanım
StartCoroutine(SendStateRoutine("STATE:GAME_STARTED"));
UDP benzeri yapıda mesaj kaybolabilir. Kritik durumları 5 kez göndererek garantiye alıyoruz.
🎮 Oyun Durum Yönetimi
Temel Durum Döngüsü
private bool isGameActive = false;
void SetupLobby()
{
isGameActive = false;
if (lobbyPanel) lobbyPanel.SetActive(true);
if (gamePanel) gamePanel.SetActive(false);
if (gameOverPanel) gameOverPanel.SetActive(false);
ResetGameData();
RemoteNex.SendData("STATE:LOBBY");
}
public void StartGame()
{
isGameActive = true;
if (lobbyPanel) lobbyPanel.SetActive(false);
if (gamePanel) gamePanel.SetActive(true);
if (gameOverPanel) gameOverPanel.SetActive(false);
foreach (var player in players.Values)
player.score = 0;
UpdateScoreUI();
StartCoroutine(SendStateRoutine("STATE:GAME_STARTED"));
}
void EndGame()
{
isGameActive = false;
if (gamePanel) gamePanel.SetActive(false);
if (gameOverPanel) gameOverPanel.SetActive(true);
var winner = players.Values.OrderByDescending(p => p.score).FirstOrDefault();
RemoteNex.SendData($"STATE:GAME_OVER:{winner?.id ?? "NOBODY"}");
}
⚡ Rate Limiting (Performans Optimizasyonu)
Saniyede yüzlerce input gelebilir. Bunları filtreleyin:
private float lastProcessTime = 0f;
void OnData(string raw)
{
// PLAYERS mesajları her zaman işlenmeli
if (raw.Contains("PLAYERS:"))
{
ParsePlayerList(raw);
return;
}
// Diğer mesajlar için rate limit
if (Time.time - lastProcessTime < 0.05f) return; // 50ms = 20 FPS
lastProcessTime = Time.time;
// ... geri kalan kod
}
Fizik tabanlı oyunlar (Pong): 0.005f (200 FPS) Turn-based oyunlar (Quiz): 0.1f (10 FPS)
🔐 Master/Guest Sistemi
Role Tabanlı Erişim Kontrolü
bool IsMasterPlayer(string playerId)
{
if (!players.ContainsKey(playerId)) return false;
string role = players[playerId].role;
return role == "M" || role == "Master" || role == "m";
}
void ProcessMovement(string raw)
{
string senderId = ExtractSenderId(raw);
// Sadece Master oyun başlatabilir
if (raw.Contains("START_GAME") && !IsMasterPlayer(senderId))
{
Debug.LogWarning("Non-master tried to start game!");
return;
}
// Normal işlemler devam eder...
}
📋 Kontrol Listesi
Oyununuzda bunlar mutlaka olmalı:
- ✅
OnEnable/OnDisableevent subscription - ✅
PlayerDatasınıfı veDictionary<string, PlayerData> - ✅
OnDatamerkezi mesaj handler - ✅
ParsePlayerListmetodu - ✅
ProcessMovementmetodu - ✅
SetupLobby / StartGame / EndGamedurum metodları - ✅
RemoteNex.SendDataile durum senkronizasyonu - ✅
isGameActiveflag kontrolü - ✅ Rate limiting implementasyonu
- ✅ Master/Guest role kontrolü (Opsiyonel)
🐛 Sık Yapılan Hatalar
1. Event Subscription Unutulması
// ❌ YANLIŞ
void Start()
{
RemoteNex.OnInputReceived += OnData;
}
// ✅ DOĞRU
void OnEnable()
{
RemoteNex.OnInputReceived += OnData;
}
void OnDisable()
{
RemoteNex.OnInputReceived -= OnData;
}
2. String Parse Hatası
// ❌ YANLIŞ - IndexOutOfRange riski
string senderId = parts[0];
// ✅ DOĞRU - Güvenli
if (parts.Length > 0)
{
string senderId = parts[0].Trim();
}
3. Dictionary Kontrolsüz Erişim
// ❌ YANLIŞ - KeyNotFoundException fırlatabilir
var player = players[senderId];
// ✅ DOĞRU
if (players.ContainsKey(senderId))
{
var player = players[senderId];
}