Digitaler Zwilling

Unity Digital Twin mit MQTT verbinden: Live-Telemetrie und Befehle

Verbinden Sie eine Unity-Szene per MQTTnet mit einem MQTT-Broker, animieren Sie eine Pumpe aus Live-Telemetrie im Main Thread, senden Sie Start- und Stopp-Befehle an das Gerät zurück und wechseln Sie für die Produktion auf TLS.

Unity Digital Twin einer Industriepumpe, die sich anhand von Live-MQTT-Telemetrie dreht und die Farbe wechselt
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Ein Unity Digital Twin holt echte Maschinendaten in eine Game Engine, die physikalisch basiertes Rendering, VR- und AR-Unterstützung und einen ausgereiften Editor für detaillierte Szenen bietet. In diesem Tutorial verbinden Sie eine Unity-Szene mit der Bibliothek MQTTnet mit MQTT, animieren eine Pumpe aus Live-Telemetrie, senden Start- und Stopp-Befehle an das Gerät zurück und sichern die Verbindung anschließend mit TLS ab. Getestet wird gegen den öffentlichen MQTT-Broker broker.connectmq.com mit einem kleinen Python-Simulator.

Wann Unity das richtige Werkzeug für einen Digital Twin ist

Browser-Engines wie Three.js sind der einfachste Weg, einen Zwilling vielen Menschen zugänglich zu machen. Unity spielt seine Stärken aus, wenn der Zwilling eine eigenständige Anwendung ist:

Anforderung Unity Browser (Three.js / Babylon.js)
Bedienerschulung, VR- oder AR-Headsets Stark, integrierte XR-Unterstützung Über WebXR möglich, weniger Tooling
Sehr detaillierte Szenen, Physik, Beleuchtung Stark Gut, begrenzt durch Browser-Budgets
Zugriff ohne Installation für viele Nutzer Benötigt einen Build pro Plattform Stark, nur eine URL
Rohes MQTT über TCP Ja, in Desktop- und Mobile-Builds Nein, nur WebSocket
Kompetenzen im Team C# und der Unity-Editor JavaScript und Web-Tooling

Wenn sich Ihr Zwilling per Link in jedem Browser öffnen lassen muss, beginnen Sie stattdessen mit dem Tutorial zum Three.js Digital Twin. Handelt es sich um eine Leitstandanwendung, einen Trainingssimulator oder eine Headset-Anwendung, ist Unity eine starke Wahl. Die Konzepte hinter beiden Ansätzen erklärt unser Leitfaden zur Digital-Twin-Technologie.

Was Sie bauen

Eine Pumpe in einer Unity-Szene, die:

  1. connectmq/demo/unity-7f3a/pump1/telemetry abonniert und rpm, temperature und running aus JSON liest.
  2. ihr Laufrad mit der gemeldeten Drehzahl rotieren lässt und die Gehäusefarbe beim Erwärmen von Blau nach Rot verschiebt.
  3. per UI-Buttons START- und STOP-Befehle an connectmq/demo/unity-7f3a/pump1/cmd veröffentlicht.
  4. sich automatisch neu verbindet, wenn das Netzwerk ausfällt.

Ersetzen Sie unity-7f3a durch ein eigenes zufälliges Suffix, damit Ihre Topics auf dem gemeinsam genutzten öffentlichen MQTT-Broker eindeutig sind.

Schritt 1: MQTTnet zum Projekt hinzufügen

MQTTnet ist ein ausgereifter Open-Source-MQTT-Client für .NET, der auf dem .NET-Standard-2.1-Profil von Unity läuft. Dieses Tutorial verwendet die API der Version 4.3.x.

  1. Setzen Sie unter Project Settings > Player > Other Settings das Api Compatibility Level auf .NET Standard 2.1.
  2. Laden Sie das Paket MQTTnet 4.3.x von NuGet herunter (die .nupkg-Datei ist ein ZIP-Archiv), entpacken Sie lib/netstandard2.1/MQTTnet.dll und legen Sie die Datei in Assets/Plugins/ ab. Alternativ installieren Sie es mit dem Paketmanager NuGetForUnity.
  3. Wenn Sie mit IL2CPP bauen (Mobile und die meisten Konsolen), fügen Sie Assets/link.xml hinzu, damit das Code Stripping keine Typen entfernt, die MQTTnet per Reflection lädt:
<linker>
  <assembly fullname="MQTTnet" preserve="all"/>
</linker>

Beachten Sie, dass MQTTnet 5 mehrere Typen umbenannt hat (zum Beispiel wurde MqttFactory zu MqttClientFactory). Wenn Sie später aktualisieren, passen Sie die Factory-Aufrufe an; die Gesamtstruktur des Skripts bleibt gleich.

Schritt 2: Die Pumpe simulieren

Führen Sie dieses Python-Skript auf einem beliebigen Rechner aus, um Telemetrie zu erzeugen und auf Befehle zu reagieren. Es verwendet paho-mqtt 2.x (pip install "paho-mqtt>=2.0").

import json
import math
import time

import paho.mqtt.client as mqtt

PREFIX = "connectmq/demo/unity-7f3a/pump1"
state = {"running": True}


def on_connect(client, userdata, flags, reason_code, properties):
    print("Simulator connected:", reason_code)
    client.subscribe(f"{PREFIX}/cmd", qos=1)


def on_message(client, userdata, msg):
    cmd = msg.payload.decode().strip().upper()
    if cmd in ("START", "STOP"):
        state["running"] = cmd == "START"
        print("Command:", cmd)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="unity-sim-7f3a")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_start()

t = 0.0
temperature = 30.0
while True:
    t += 0.5
    if state["running"]:
        rpm = 1450 + 120 * math.sin(t / 5)
        temperature = min(temperature + 0.4, 85.0)
    else:
        rpm = 0.0
        temperature = max(temperature - 0.8, 25.0)
    payload = {"rpm": round(rpm), "temperature": round(temperature, 1),
               "running": state["running"]}
    client.publish(f"{PREFIX}/telemetry", json.dumps(payload), qos=0)
    time.sleep(0.5)

Zwei Nachrichten pro Sekunde liegen deutlich unter dem Limit des öffentlichen MQTT-Brokers von 10 Nachrichten pro Sekunde und Client.

Schritt 3: Das Unity-Digital-Twin-Skript

Legen Sie Assets/Scripts/PumpTwin.cs an. Hängen Sie es an ein leeres GameObject und ziehen Sie anschließend den Transform Ihres Laufrads und den Renderer des Pumpengehäuses in die Inspector-Felder. Für den Anfang reichen ein Zylinder als Gehäuse und ein abgeflachter Würfel als Laufradschaufel.

using System;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Formatter;
using MQTTnet.Protocol;
using UnityEngine;

[Serializable]
public class PumpTelemetry
{
    public float rpm;
    public float temperature;
    public bool running;
}

public class PumpTwin : MonoBehaviour
{
    [Header("Broker")]
    public string host = "broker.connectmq.com";
    public int port = 1883;
    public bool useTls = false;
    public string username = "";
    public string password = "";
    public string clientId = "";
    public string topicPrefix = "connectmq/demo/unity-7f3a/pump1";

    [Header("Scene")]
    public Transform impeller;
    public Renderer pumpBody;
    public Color coldColor = new Color(0.2f, 0.5f, 0.9f);
    public Color hotColor = new Color(0.9f, 0.2f, 0.15f);
    public Color offlineColor = Color.gray;

    private IMqttClient _client;
    private CancellationTokenSource _cts;
    private readonly ConcurrentQueue<string> _inbox = new ConcurrentQueue<string>();
    private PumpTelemetry _target = new PumpTelemetry { temperature = 20f };
    private float _rpm, _temperature = 20f, _lastMessageTime = -999f;

    private async void Start()
    {
        if (string.IsNullOrEmpty(clientId))
            clientId = "unity-" + Guid.NewGuid().ToString("N").Substring(0, 8);

        _cts = new CancellationTokenSource();
        _client = new MqttFactory().CreateMqttClient();

        // Läuft in einem Hintergrund-Thread: nur einreihen, hier niemals Unity-Objekte anfassen.
        _client.ApplicationMessageReceivedAsync += e =>
        {
            var seg = e.ApplicationMessage.PayloadSegment;
            if (seg.Count > 0)
                _inbox.Enqueue(Encoding.UTF8.GetString(seg.Array, seg.Offset, seg.Count));
            return Task.CompletedTask;
        };

        _client.DisconnectedAsync += async e =>
        {
            if (_cts.IsCancellationRequested) return;
            Debug.LogWarning($"MQTT disconnected: {e.Reason}. Retrying in 3 s");
            await Task.Delay(3000);
            if (!_cts.IsCancellationRequested) await ConnectAsync();
        };

        await ConnectAsync();
    }

    private async Task ConnectAsync()
    {
        var builder = new MqttClientOptionsBuilder()
            .WithTcpServer(host, port)
            .WithClientId(clientId)
            .WithProtocolVersion(MqttProtocolVersion.V500)
            .WithCleanSession()
            .WithKeepAlivePeriod(TimeSpan.FromSeconds(30));
        if (!string.IsNullOrEmpty(username)) builder.WithCredentials(username, password);
        if (useTls) builder.WithTlsOptions(o => o.UseTls());

        try
        {
            await _client.ConnectAsync(builder.Build(), _cts.Token);
            var subscribe = new MqttFactory().CreateSubscribeOptionsBuilder()
                .WithTopicFilter(f => f
                    .WithTopic($"{topicPrefix}/telemetry")
                    .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce))
                .Build();
            await _client.SubscribeAsync(subscribe, _cts.Token);
            Debug.Log("MQTT connected and subscribed");
        }
        catch (Exception ex)
        {
            Debug.LogWarning($"MQTT connect failed: {ex.Message}");
        }
    }

    private void Update()
    {
        // Nachrichten im Main Thread abarbeiten.
        while (_inbox.TryDequeue(out var json))
        {
            try
            {
                _target = JsonUtility.FromJson<PumpTelemetry>(json);
                _lastMessageTime = Time.time;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Bad payload: {ex.Message}");
            }
        }

        float k = 1f - Mathf.Exp(-Time.deltaTime * 4f); // bildratenunabhängige Glättung
        _rpm = Mathf.Lerp(_rpm, _target.rpm, k);
        _temperature = Mathf.Lerp(_temperature, _target.temperature, k);

        if (impeller != null)
            impeller.Rotate(Vector3.up, _rpm / 60f * 360f * Time.deltaTime * 0.1f, Space.Self);

        if (pumpBody != null)
        {
            bool stale = Time.time - _lastMessageTime > 5f;
            float t = Mathf.InverseLerp(25f, 85f, _temperature);
            pumpBody.material.color = stale ? offlineColor : Color.Lerp(coldColor, hotColor, t);
        }
    }

    // Im Inspector mit UI-Buttons (OnClick) verknüpfen.
    public void SendStart() => _ = PublishCommand("START");
    public void SendStop() => _ = PublishCommand("STOP");

    private async Task PublishCommand(string command)
    {
        if (_client == null || !_client.IsConnected)
        {
            Debug.LogWarning("Not connected, command not sent");
            return;
        }
        var message = new MqttApplicationMessageBuilder()
            .WithTopic($"{topicPrefix}/cmd")
            .WithPayload(command)
            .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
            .Build();
        await _client.PublishAsync(message, _cts.Token);
    }

    private async void OnDestroy()
    {
        _cts?.Cancel();
        if (_client != null && _client.IsConnected)
            await _client.DisconnectAsync();
        _client?.Dispose();
    }
}

Drücken Sie Play, während der Simulator läuft. Das Laufrad dreht hoch, das Gehäuse erwärmt sich von Blau nach Rot, und ein Klick auf einen mit SendStop() verknüpften Stop-Button lässt den Simulator die Pumpe anhalten; innerhalb einer Sekunde bremst der Zwilling das Laufrad ab und das Gehäuse beginnt abzukühlen. Beenden Sie den Simulator ganz, wird das Gehäuse nach fünf Sekunden grau.

So funktioniert die Unity-MQTT-Integration

Threading ist die Stolperfalle Nummer eins. MQTTnet löst ApplicationMessageReceivedAsync in einem Thread-Pool-Thread aus, aber fast jede Unity-API (Transforms, Materialien, Debug.DrawLine, Instanziierung) muss aus dem Main Thread aufgerufen werden. Ruft man sie aus dem Handler auf, gibt es Fehler oder, schlimmer noch, sporadische Ausfälle. Das Skript umgeht das, indem es rohe Payloads in eine ConcurrentQueue legt und sie in Update() verarbeitet.

Glättung verbirgt den Netzwerkrhythmus. Telemetrie trifft zweimal pro Sekunde ein, Unity rendert aber 60 oder mehr Bilder pro Sekunde. Das Interpolieren (Lerp) zum jeweils neuesten Zielwert macht die Bewegung kontinuierlich. Der exponentielle Faktor hält die Glättung unabhängig von der Bildrate identisch.

Befehle mit QoS 1, Telemetrie mit QoS 0. Ein verlorener Telemetriewert wird eine halbe Sekunde später ersetzt, ein verlorener Stopp-Befehl ist dagegen ein echtes Problem. Details finden Sie unter MQTT-QoS-Stufen erklärt.

Das Gerät ist die Quelle der Wahrheit. Der Zwilling ändert seinen eigenen Zustand nie, wenn Sie Stop drücken. Er wartet, bis die Pumpe running: false meldet. So bleibt der Zwilling ehrlich, falls das Gerät einen Befehl ablehnt oder verpasst – ein Prinzip, das in Zustandssynchronisation von Digital Twins vertieft wird.

Den Unity Digital Twin skalieren

Ein Skript pro Maschine genügt für eine Demo, ein Werk hat aber viele Assets. Eine sauberere Struktur für größere Szenen:

  1. Ein Verbindungsmanager pro Anwendung, ein Singleton, das den IMqttClient besitzt, mit einer Wildcard wie plant1/# abonniert und Nachrichten nach Topic weiterleitet. Im Werk ist das idealerweise ein Teilbaum Ihres Unified Namespace (UNS), dessen Topic-Struktur der ISA-95-Hierarchie folgt, etwa acme/werk-stuttgart/montage/linie-1/#; so bindet die Szene genau die Linie, die sie darstellt.
  2. Eine Binding-Komponente pro Asset (TwinBinding) mit einem Topic-Suffix und einer Referenz auf die Teile, die sie steuert. Der Manager sucht Bindings in einem nach Topic geschlüsselten Dictionary.
  3. ScriptableObjects für die Konfiguration, damit sich Broker-Einstellungen und Topic-Präfixe zwischen Entwicklungs- und Produktions-Builds ohne Codeänderungen unterscheiden.
  4. Retained Messages auf Zustands-Topics, damit der Zwilling vom Start weg den richtigen Zustand zeigt.

Felder auf Meshes, Materialien und Animationen abzubilden, ist unabhängig von der Engine dasselbe Problem; IoT-Daten an ein 3D-Modell binden behandelt die Muster im Detail.

Plattformhinweise und Fehlerbehebung

  • WebGL-Builds können keine TCP-Sockets öffnen, und der WebSocket-Transport von MQTTnet setzt auf .NET-APIs, die in Unity WebGL nicht verfügbar sind. Für einen Browser-Build rufen Sie einen JavaScript-MQTT-Client wie MQTT.js über ein .jslib-Plugin auf oder bauen die Browserversion mit einer Web-Engine.
  • Android benötigt die INTERNET-Berechtigung, die Unity automatisch hinzufügt, wenn Netzwerkcode erkannt wird; prüfen Sie sie, falls Verbindungen auf dem Gerät stillschweigend scheitern.
  • Connect failed: NotAuthorized bedeutet falsche Zugangsdaten oder Client ID. Connect failed: timeout bedeutet meist einen blockierten Port; testen Sie mit nc -vz broker.connectmq.com 8883.
  • Eine Trennungsschleife alle paar Sekunden bedeutet, dass sich zwei Instanzen eine Client ID teilen, zum Beispiel der Editor und ein Standalone-Build. Lassen Sie clientId in der Entwicklung leer, um eine zufällige zu erhalten.

Die Verbindung absichern

Der öffentliche MQTT-Broker ist anonym und für jeden lesbar, nutzen Sie ihn also nur für Tests und Prototypen. Für ein echtes Werk erstellen Sie einen privaten Broker als verwalteten MQTT-Broker und verbinden sich über TLS. Mit ConnectMQ erhalten Sie im Portal einen generierten Benutzernamen, ein Passwort und eine Client ID; setzen Sie im Inspector Port auf 8883, aktivieren Sie Use Tls und tragen Sie YOUR_USERNAME, YOUR_PASSWORD und YOUR_CLIENT_ID ein. Das Zertifikat des Brokers wird von Let's Encrypt ausgestellt, sodass .NET es ohne zusätzliche Dateien gegen den Trust Store des Betriebssystems validiert. Halten Sie Zugangsdaten aus der Versionsverwaltung heraus, indem Sie sie zur Laufzeit aus einer Konfigurationsdatei oder Umgebungsvariable laden.

Starten Sie mit ConnectMQ

Ein Unity Digital Twin braucht einen Broker, der aus dem Werk und von jedem Bedienerarbeitsplatz aus jederzeit erreichbar ist. ConnectMQ bietet genau das als verwalteter MQTT-Broker: Erstellen Sie einen kostenlosen Broker unter app.connectmq.com im Serverless-Plan, ohne Kreditkarte, mit MQTT 5, TLS und bis zu 25 Verbindungen. Der MQTT-Client im Browser und der Live-Topic-Explorer machen es leicht zu prüfen, was Ihre Geräte veröffentlichen, bevor Sie es in Unity binden. Bis dahin können Sie auch weiter mit dem kostenlosen öffentlichen MQTT-Broker prototypisieren.

Häufig gestellte Fragen

Welche MQTT-Bibliothek funktioniert in Unity am besten?

MQTTnet ist die am weitesten verbreitete Option für Desktop- und Mobile-Builds in Unity, weil sie MQTT 3.1.1 und 5.0, TLS und asynchrone APIs auf .NET Standard 2.1 unterstützt. Für WebGL-Builds benötigen Sie einen JavaScript-Client, der über ein Plugin angebunden wird.

Warum erhalte ich Fehler, wenn ich Objekte im Message Handler aktualisiere?

Der Handler läuft in einem Hintergrund-Thread, und die meisten Unity-APIs sind nur im Main Thread erlaubt. Reihen Sie Nachrichten im Handler in eine Queue ein und wenden Sie sie in Update() an.

Kann ein Unity Digital Twin echte Anlagen steuern?

Ja, indem er Befehle auf Topics veröffentlicht, die das Gerät abonniert. Verwenden Sie QoS 1, lassen Sie destruktive Aktionen in der UI bestätigen und sorgen Sie dafür, dass das Gerät seinen tatsächlichen Zustand zurückmeldet, damit der Zwilling widerspiegelt, was wirklich passiert ist.

Funktioniert das mit VR-Headsets?

Ja. Das MQTT-Skript ist unabhängig vom Rendering-Setup, sodass dieselbe Komponente in einer für XR konfigurierten Szene funktioniert, solange das Build-Ziel des Headsets die verwendeten Netzwerk-APIs unterstützt.

Verwandte Artikel

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.