Skip to main content

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.

Sistem Mimarisi

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;
}
Kritik Uyarı

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>();
Neden Dictionary?

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"));
Neden Tekrar?

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
}
Öneri

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/OnDisable event subscription
  • PlayerData sınıfı ve Dictionary<string, PlayerData>
  • OnData merkezi mesaj handler
  • ParsePlayerList metodu
  • ProcessMovement metodu
  • SetupLobby / StartGame / EndGame durum metodları
  • RemoteNex.SendData ile durum senkronizasyonu
  • isGameActive flag 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];
}