Wednesday, September 10, 2014

CodeFest.at Blog-Eintrag: SignalR und Unity in euren Multiplayer-Indie-Games

Ihr, als .Net-Entwickler, kennt SignalR möglicherweise bereits als Microsoft-Programmierbibliothek um Nachrichten in Echtzeit zu verschiedenen Clients zu übertragen. Das klingt zuerst einmal schwierig, ist es in Wirklichkeit aber nicht.

Grübelt ihr noch über ein Einsatzszenario? Nun, wie wäre es mit einem großen Monitor, oder Fernseher für euer Support-Team? Auf diesem wird der aktuelle Status von verschiedenen System angezeigt. Sollte eines der Systeme etwas Ungewöhnliches entdecken, dann schickt es zum SignalR-Hub eine Meldung. Alle angeschlossenen Clients erhalten diese Meldung umgehend und können dementsprechend reagieren.
clip_image002
Ich hoffe, ihr seid ein wenig neugierig geworden und wollt jetzt wissen, wie das genau funktioniert? Nun das hat mir mein Blogger-Kollege Toni Pohl schon hier in diesem Blog-Eintrag von letztem Jahr abgenommen. Ich empfehle euch diesen Eintrag zu lesen, dann können wir anschließend an dieser Stelle weiter machen.

Unity und SignalR

Ich möchte euch zeigen, wie man Unity mit SignalR verwenden kann. Für Multi-Player-Games bietet es sich ja geradezu an. Mit einer eventuell bereits vorhandenen Azure-Subscription habt ihr noch dazu alles, was ihr als Infrastruktur in Produktion benötigt. Dazu aber möglicherweise ein anderes Mal mehr. Ich würde gerne mit euch gemeinsam einen Teaser für ein Spiel schreiben. Die heutige Funktionalität ist recht einfach zusammengefasst:
· Unity als Entwicklungstool.
· Ein witziger Hintergrund.
· Ein Sprite (in diesem Fall eine geöffnete Hand), welches sich mit der Maus bewegen lässt.
· Wenn die linke Maustaste gedrückt wird, dann soll eine Faust gebildet werden, also in etwa so, als würde man nach einem Objekt greifen.
· Ein Web-Client, der den gleichen Hintergrund verwendet.
· Ebenfalls die geöffnete Hand, sowie die geschlossene Hand.
· Wenn das Unity-Spiel startet, dann soll sich auf der Webseite die Hand genauso bewegen, wie in dem Unity-Client.
· Wird im Unity-Spiel die Maustaste gedrückt, dann wird die Hand zu einer Faust, das soll der Web-Client widerspiegeln.
Lange Rede, kurzer Sinn, das soll so funktionieren, wie ich es hier auf youtube kurz vorzeige.
Was benötigt ihr dazu? Ich nehme an, wenn ihr diesem Blog folgt, dann habt ihr das schon installiert:
· Unity, die Testversion von http://unity3d.com/ reicht vollkommen.
· Visual Studio 2013, wenn ihr die Visual Studio Integration mit Unity verwenden wollt.
· Visual Studio Tools für Unity von hier: http://unityvs.com/ diese bitte herunterladen und installieren.
Für die ganz Ungeduldigen unter euch, den Source Code gibt es wieder hier zum Download.

Spiele-Sprites

Für den Spiel-Teaser selbst benötigen wir noch ein Hintergrund-Bild, dieses findet ihr als .psd-Datei hier, hat die Größe von 800x500 Pixel und sieht so aus:
clip_image004
Damit wir auch etwas Bewegung in die Sache bringen - die beiden Bilder für die sich bewegende Hand:
clip_image005 clip_image006
Das war es bereits, es kann also losgehen mit dem Unity-Client.
Ein kleiner Hinweis noch, solltet ihr noch nie mit Unity gearbeitet haben, dann lege ich euch meinen letzten Blog-Eintrag ans Herz, da werden einige der Schritte, die ich hier nur kurz anreiße – vor allem beim Anlegen des Projektes – genauer beschrieben. Diesen findet ihr hier.

Der Unity Client Teil 1 – noch ohne SignalR

Im ersten Teil wollen wir den Unity-Client so weit bringen, dass die Hand mit der Maus bewegt werden kann und beim Klicken der Maus sich die Hand schließt und auch wieder öffnet. Im Unity-Client müssen wir somit folgendes tun:
· Ein neues 2D-Unity-Projekt erstellen.
· Den Hintergrund und die Hand in die Spielszene einfügen.
· Ein „wenig“ Physik für die Hand hinzufügen.
· Ein Script erstellen, damit die Hand bewegt werden kann.
· Wenn die Maus gedrückt wird, schließt sich die Hand und öffnet sich wieder, wenn die Maustaste losgelassen wird.

Ein neues 2D-Unity-Projekt erstellen

Im geöffneten Unity erstelle ich ein neues 2D-Projekt und gebe diesem den Namen SignalRUnitySample.
clip_image007
Eine gewisse Ordnerstruktur in Unity ist immer eine gute Sache, um die Dateien nicht irgendwie herumfliegen zu lassen. Das sollte ungefähr so aussehen:
clip_image008
Ich habe auch gleich die drei Bilder in den Ordner Textures mit Drag & Drop hineingezogen.

Den Hintergrund und die Hand in die Spielszene einfügen

Dazu ziehen wir den Hintergrund aus dem Assets\Textures-Folder auf die Main Camera in der Hierarchy-Ansicht. Danach holen wir aus dem Assets\Textures-Folder den Sprite mit dem Namen OpenHand als eigenständiges Objekt – ich habe dieses dann umbenannt auf Player.
clip_image009
Ich habe die Anzeigegröße in Unity noch angepasst, um das Resultat mit dem im Browser besser vergleichen zu können. Dazu klickt ihr in Unity auf Game und in der DropDown fügt ihr mittels Add eine neue Auflösung mit 800x500 Pixel ein.
clip_image010
Danach skaliert ihr noch den Hintergrund und die Hand, dementsprechend. D. h. den Hintergrund skaliert ihr auf der X- und Y-Achse 2 mal. Das macht ihr auch mit dem Player-Objekt (der Hand). Des Weiteren sollte die Hand im aktuellen Layer weiter vorne liegen (Wert 1), damit wir diese auch sehen und diese nicht hinter dem Hintergrund gezeichnet wird.
clip_image012 clip_image014
Jetzt sollte noch etwas Bewegung in die Sache kommen.

Physik und Skript für das Player-Objekt

Wie schon bei dem PhoneCatcher-Spiel das letzte Mal, benötigt unser Player-Objekt einen RigidBody 2D, an dem aber „Is Kinematic“ gesetzt wird, um die Schwerkraft abzuschalten. Die Spieler-Hand wird ja mit der Maus, durch den Spieler selbst, bewegt. Dieser wird mit dem Add Component-Button hinzugefügt (an dem Player Objekt).
clip_image016 clip_image017
Jetzt noch ein Skript mit dem Namen PlayerController, wiederum mit dem Add Component an dem Player Objekt. Falls ihr das alles richtig gemacht habt, dann sieht das im Inspector bisher so aus:
clip_image018
Das Skript ist bisher ebenfalls keine große Überraschung, da es sich an dem vom letzten Mal orientiert. Wir benötigen die Grenzen des Spielfeldes, damit die Hand nicht darüber hinaus bewegt werden kann. Zusätzlich muss das Spieler-Sprite ausgetauscht werden, wenn die Maustaste gedrückt wird – und wieder zurückgesetzt werden, damit wieder die geöffnete Hand sichtbar ist, wenn die Maus losgelassen wird.
using UnityEngine;
public class PlayerController : MonoBehaviour { 
    public Camera cam;
    private float maxWidth;
    private float maxHeight;
 
    bool isHandOpen = true;
    public Sprite clickedSprite;
    private Sprite standardSprite;
 
    void Start()
    {
        if (cam == null)
            cam = Camera.main;
        var corner = new Vector3(Screen.width, Screen.height, 0f);
        var targetWidth = cam.ScreenToWorldPoint(corner);
        float playerWidth = renderer.bounds.extents.x;
        float playerHeight = renderer.bounds.extents.y;
        maxWidth = targetWidth.x - playerWidth;
        maxHeight = targetWidth.y - playerHeight;
 
        standardSprite = this.GetComponent().sprite;
    }
    void FixedUpdate()
    {
        var currentPosition = cam.ScreenToWorldPoint(Input.mousePosition);
        float targetWidth = Mathf.Clamp(currentPosition.x, -maxWidth, maxWidth);
        float targetHeight = Mathf.Clamp(currentPosition.y, -maxHeight, maxHeight);
        var newPosition = new Vector3(targetWidth, targetHeight, 0f);
        rigidbody2D.MovePosition(newPosition);
 
        if (Input.GetMouseButtonDown(0))
        {
            isHandOpen = false;
            MouseDown();
        }
        else if(Input.GetMouseButtonUp(0))
        {
            isHandOpen = true;
            MouseUp();
        }
    }
 
    private void MouseDown()
    {
        Debug.Log("MouseDown");
        this.GetComponent().sprite = clickedSprite;
    }
 
    private void MouseUp()
    {
        Debug.Log("MouseUp");
        this.GetComponent().sprite = standardSprite;
    }
}
Zurück in Unity muss noch das Sprite für die Public-Variablen clickedSprite befüllt werden, dazu muss aus Assets\Textures das ClosedHand-Sprite mittels Drag & Drop auf die Skript-Variable gezogen werden.
clip_image019
Wenn das geklappt hat, dann solltet ihr das einmal ausprobieren. Die Hand müsste über den Bildschirm flitzen, wenn ihr die Maus bewegt. Wenn die Maustaste gedrückt wird, dann seht ihr die Faust. Nach dem Loslassen der Maustaste ist wieder die geöffnete Hand sichtbar. Der Unity-Client ist erst einmal fertig, jetzt müssen wir den SignalR-Teil erstellen.

Echtzeit-Kommunikation mit SignalR

Der Unity-Client wird sich am SignalR-Hub, der als Web-Applikation läuft, anmelden und Meldungen absetzen. Der SignalR-Hub informiert alle angemeldeten Clients und diese werden dementsprechend reagieren. Wir benötigen also eine neue Web-Applikation, ihr könnt dafür entweder eine komplett neue Solution in Visual Studio anlegen, oder auch die von Unity angelegte Solution verwenden. Wie auch immer ihr euch entscheidet, wir benötigen eine neue ASP.NET Web Application, ich habe den Namen SignalRUnityWeb ausgewählt. Diese soll leer sein und per Default noch keinerlei Funktionalität zur Verfügung stellen.
clip_image021clip_image023
Wie mittlerweile fast schön üblich benötigen wir zwei NuGet Packages. Dazu drückt am Web-Projekt die rechte Maustaste und wählt „Manage NuGet Packages…“ an.
Das erste Package ist Microsoft ASP.NET SignalR, derzeit in der Version 2.1.1
clip_image025
Das zweite Package ist Microsoft.Owin.Host.SystemWeb in der Version 3.0.0
clip_image027
Jetzt können wir mit dem Implementieren loslegen, wir benötigen eine neue SignalR Hub Class (v2), dazu drückt ihr die rechte Maustaste am Web-Projekt und wählt Add-> SignalR Hub Class (v2), sollte das bei euch nicht zu sehen sein, dann bitte über Add->New Item und sucht nach SignalR – das nächste Mal, taucht der Eintrag dann in der Liste auf.
clip_image029
Diese sollte den Namen SignalRSampleHub bekommen.
clip_image030
Wir passen die Default-Implementierung nur rudimentär an, ich hätte gerne eine Send-Methode mit zwei Parametern, und zwar WER hat die Meldung abgesetzt und WAS ist der Inhalt.
using Microsoft.AspNet.SignalR;
namespace SignalRUnityWeb
{
    public class SignalRSampleHub : Hub
    {
        public void Send(string name, string message)
        {
            // Call the broadcastMessage method to update clients.
            Clients.All.broadcastMessage(name, message);
        }
    }
}
Wenn wir SignalR 2.0 verwenden, benötigen wir noch eine OWIN Startup class, wiederum mit der rechten Maustaste am Web-Projekt und Add-> OWIN Startup class, bzw. über Add->New Item.
clip_image031
Der Name soll Startup.cs sein.
clip_image032
Die Implementierung ist kurz und bündig und stellt unseren SignalR-Hub zur Laufzeit zur Verfügung.
using Microsoft.Owin;
using Owin; 
[assembly: OwinStartup(typeof(SignalRUnityWeb.Startup))]
namespace SignalRUnityWeb
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}
Wenn ihr die Web-Applikation ausführt, so wird das leider nicht klappen, da es da noch einen Versionskonflikt mit der aktuellen Microsoft.Owin-DLL gibt.
clip_image034
Diesen können wir in der Web.config lösen, indem wir der .NET-Laufzeitumgebung mitteilen, sodass die Version 3.0 der DLL auch funktioniert.

  
    
    
  
  
    
      
                                publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="3.0.0.0" />
      
    
  

Jetzt benötigen wir noch eine Webseite in unserer Web-Applikation, ich würde vorschlagen wir nennen dieseindex.html. Diese soll das Hintergrundbild und die Hand darstellen, sowie sich an unserem SignalR-Hub anmelden. Wenn eine neue Meldung eintrifft, dann muss die Hand bewegt werden und bei gedrückter Maustaste das Bild gewechselt werden. Dank Microsoft und jQuery sind das nur ein paar Zeilen JavaScript-Code.
<!DOCTYPE html>


    SignalR-Unity Sample
    
 


    
 
    
        
    
 
    
    
    
    
    
    
    
    
    


Nun sollten noch die drei Bilder, die in dem Unity-Client verwendet werden, in die Web-Applikation kopiert werden, und zwar in einen Unterordner mit dem Namen Images.
clip_image035
Solltet ihr eine eigene Visual Studio Solution für diese Web-Applikation erstellt haben, dann startet diese nun. Der Signal-R Hub ist damit erstellt und aktiv. Die Webseite ist nun am Hub als erster Client angemeldet und wartet auf Meldungen vom SignalR-Hub.
clip_image036
Kommen wir zum letzten Abschnitt, SignalR und Unity zu verbinden.

Der Unity Client Teil 2 – Kommunikation mit SignalR

Zurück in Unity legen wir ein leeres GameObject an und geben diesem den Namen SignalRObject.
clip_image037clip_image038
Leere Game-Objekte sind sehr praktisch, in unserem Fall ist es so, dass wir nur ein Script benötigen (um den SignalR-Hub zu informieren), aber keinerlei sichtbaren Aktionen am Bildschirm ausführen werden. Also fügen wir gleich das Skript hinzu, wir geben diesem den Namen SignalRUnityController.
clip_image039
Speichert bitte nun eure Spiel-Szene im Scenes Folder, ich habe dieser den Namen GameScene gegeben. Das ist an dieser Stelle WICHTIG, da ihr sonst möglicherweise durch einen Absturz von Unity alles, was ihr bisher gemacht habt, verlieren könnt. Das liegt weniger an Unity, als an der Programmbibliothek, die wir uns im nächsten Schritt zu unserem Projekt dazuholen.
clip_image040
Bearbeiten wir nun gleich das Skript in Visual Studio 2013 und erweitern es, um den SignalR-Hub anzusprechen.
Leider verwendet Unity nicht die .Net 4.0 Laufzeitumgebung, sondern Mono mit der .NET 3.5 Runtime. Dadurch können wir leider das Microsoft NuGet Package (Microsoft.AspNet.SignalR.Client) für SignalR nicht in unserem Unity C# Projekt verwenden. Es gibt aber auf Github ein Projekt, welches für unsere Zwecke ausreichend ist, in diesem wurde die SignalR-Bibliothek auf .NET 2.0 „heruntermigriert“. Dieses findet ihr hier:https://github.com/jenyayel/SignalR.Client.20
Ich habe dieses Projekt meiner Unity-Solution als eigenständiges Projekt hinzugefügt und danach die Referenz zu dem C#-Projekt dazugegeben.
clip_image042clip_image044
Jetzt können wir die SignalRUnityController skripten. Ich dachte mir, dass wir die SignalR-Funktionalität abschaltbar machen, das kann sehr praktisch sein, wenn man den Unity-Client etwas „tweaked“, was ja vor allem gegen Ende in der Spiele-Entwicklung recht viel Zeit in Anspruch nimmt. Die URL zum SignalR-Hub sollte ebenfalls konfigurierbar sein (dort setzt auch bitte eure URL ein, die ist in eurem Web-Projekt vermutlich anders als meine – zumindest der verwendete Port). Der Rest ist sehr ähnlich dem JavaScript-Client, der Unity Client muss sich am Hub anmelden und möchte (im Gegensatz zum JavaScript-Client) Informationen senden. Die Informationen bestehen aus einem String (wer sendet, der UnityClient) und einem weiteren String, dieser wird allerdings als JSON versendet. Dadurch ist diese Klasse in mehreren meiner Projekte einsetzbar.
using SignalR.Client._20.Hubs;
using UnityEngine; 
public class SignalRUnityController : MonoBehaviour {
    public bool useSignalR = true;
    public string signalRUrl = "http://localhost:5225/";
 
    private HubConnection _hubConnection = null;
    private IHubProxy _hubProxy;
    private Subscription _subscription; 
    void Start()
    {
        if (useSignalR)
            StartSignalR();
    }
    void StartSignalR()
    {
        if (_hubConnection == null)
        {
            _hubConnection = new HubConnection(signalRUrl);
 
            _hubProxy = _hubConnection.CreateProxy("SignalRSampleHub");
            _subscription = _hubProxy.Subscribe("broadcastMessage");
            _subscription.Data += data =>
            {
                Debug.Log("signalR called us back");
            };
            _hubConnection.Start();
        }
        else
            Debug.Log("Signalr already connected...");
    }
    public void Send(string method, string message)
    {
        if (!useSignalR)
            return;
 
        var json = "{" + string.Format("\"action\": \"{0}\", \"value\": {1}", method, message) + "}";
        _hubProxy.Invoke("Send", "UnityClient", json);
    }
}
Was jetzt noch fehlt ist, dass die Informationen aus der FixedUpdate-Methode im PlayerController in der Send-Methode der SignalRUnityController-Klasse landen.
Dazu reichen uns insgesamt vier Zeilen Code. Das Trickreiche daran ist zum einen, dass wir eine Referenz auf das leere Gabe-Objekt benötigen, an dem das Skript zum Senden der Informationen hängt, zum anderen müssen die Koordinaten vom Unity-Format in Weltkoordinaten umgerechnet werden. Abschließend noch einen JSON-String gebaut und es kann auch schon losgehen.
using UnityEngine;
public class PlayerController : MonoBehaviour {
public Camera cam;
private float maxWidth;
private float maxHeight;
bool isHandOpen = true;
public Sprite clickedSprite;
private Sprite standardSprite;
private SignalRUnityController _signalr;
void Start()
    {
if (cam == null)
            cam = Camera.main;
_signalr = (SignalRUnityController)GameObject.Find("SignalRObject")
            .GetComponent(typeof(SignalRUnityController));
var corner = new Vector3(Screen.width, Screen.height, 0f);
var targetWidth = cam.ScreenToWorldPoint(corner);
float playerWidth = renderer.bounds.extents.x;
float playerHeight = renderer.bounds.extents.y;
        maxWidth = targetWidth.x - playerWidth;
        maxHeight = targetWidth.y - playerHeight;
        standardSprite = this.GetComponent().sprite;
    }
void FixedUpdate()
    {
var currentPosition = cam.ScreenToWorldPoint(Input.mousePosition);
float targetWidth = Mathf.Clamp(currentPosition.x, -maxWidth, maxWidth);
float targetHeight = Mathf.Clamp(currentPosition.y, -maxHeight, maxHeight);
var newPosition = new Vector3(targetWidth, targetHeight, 0f);
        rigidbody2D.MovePosition(newPosition);
if (Input.GetMouseButtonDown(0))
        {
            isHandOpen = false;
            MouseDown();
        }
else if(Input.GetMouseButtonUp(0))
        {
            isHandOpen = true;
            MouseUp();
        }
var worldCoordinates = cam.WorldToScreenPoint(newPosition);
var json = "{" + string.Format("\"x\": \"{0}\", \"y\": \"{1}\", \"handOpen\": \"{2}\"",
            worldCoordinates.x, worldCoordinates.y, isHandOpen) + "}";
        _signalr.Send("Position", json);
    }
private void MouseDown()
    {
Debug.Log("MouseDown");
this.GetComponent().sprite = clickedSprite;
    }
private void MouseUp()
    {
Debug.Log("MouseUp");
this.GetComponent().sprite = standardSprite;
    }
}
Damit Unity auch unsere beiden Referenzen kennt, die in unserem Projekt nun notwendig sind, müssen die beiden DLLs
· Newtonsoft.Json.dll
· SignalR.Client.20.dll
in den Assets-Folder kopiert werden. Ihr findet diese im bin\Debug-Verzeichnis von dem SignalR.Client.20 Projekt.
clip_image046
clip_image048
Bevor ihr in Unity den Client startet, achtet bitte darauf, dass die Web-Applikation mit dem SignalR-Hub läuft, da sonst Unity hängen bleibt und ihr Unity erneut starten müsst. ACHTUNG, falls ihr eure Spielszene nicht zuvor gespeichert habt, dann verliert ihr alle ungespeicherten Schritte.
Wenn die Web-Applikation und der Unity-Client läuft und ihr die Maus bewegt, werden sich die Hände gemeinsam bewegen.
Falls ihr SignalR mit anderen Clients verwenden wollt, dann solltet ihr einen Blick auf die folgenden Native-Implementierungen werfen:
· iOS (3rd Party): https://github.com/DyKnow/SignalR-ObjC
· Android (offizieller Android-Client): https://github.com/SignalR/java-client
· Windows Phone 8/8.1 (offizieller .Net Client): https://github.com/SignalR/SignalR
Für Unity muss noch ein Wrapper um diese Bibliotheken herum geschrieben werden, dazu findet ihr Näheres in der Unity-Dokumentation:

Zusammenfassung


Microsoft SignalR und Unity ergeben die perfekte Lösung für Multiplayer-Spiele. Mit wenigen Schritten ist die Echtzeit-Kommunikation bereit gestellt. Ich bin schon gespannt, welche tollen Spiele ihr mit dieser Kombination bauen werdet. Ich freue mich darauf und hoffe ihr habt genauso viel Spaß wie ich beim Entwickeln eurer Indie-Games!

Ein kurzes Video, wie es aussieht findet ihr hier:
http://www.youtube.com/watch?v=QwaOUHUM2Fk

Den Source-Code findet ihr hier:
https://github.com/BerndtHamboeck/UnitySignalRVS2013


No comments:

CSharpCodeFormatter