Ein Unity MQTT Client verwandelt eine statische 3D-Szene in eine Live-Ansicht realer Anlagen: Ein Lüfter dreht sich mit der gemessenen Drehzahl, ein Tank wechselt mit der Temperatur die Farbe, eine Tür öffnet sich, wenn ein Sensor es meldet. In diesem Tutorial fügen Sie einem Unity-Projekt MQTTnet hinzu, abonnieren Sensor-Topics auf broker.connectmq.com und aktualisieren GameObjects sicher vom Main Thread von Unity aus. Es ist dieselbe Grundlage, auf der browserbasierte und Desktop-Zwillinge aufbauen.
Wie Live-Daten in eine Unity-Szene gelangen
Der Ablauf hat drei Teile, und am mittleren scheitern die meisten ersten Versuche:
Sensor or simulator --MQTT--> Broker --MQTT--> MQTTnet (background thread)
|
ConcurrentDictionary (latest value per topic)
|
MonoBehaviour.Update() (main thread) --> Transforms, materials
Die API von Unity ist nicht threadsicher. Netzwerk-Callbacks in MQTTnet laufen auf Threads des Thread-Pools, und wer von dort einen Transform oder ein Material anfasst, erhält UnityException: ... can only be called from the main thread – oder schlimmer: einen stillen Fehlschlag. Die Lösung besteht darin, eingehende Werte in einer threadsicheren Struktur abzulegen und sie in Update() anzuwenden.
Für die Visualisierung zählt meist nur der neueste Wert pro Topic, nicht jede einzelne Nachricht. Ein Dictionary mit dem Topic als Schlüssel liefert „der neueste Wert gewinnt“ gratis und hält die Frame-Zeit konstant, selbst wenn ein Sensor schneller veröffentlicht als Ihre Bildrate.
MQTTnet zu Unity hinzufügen
Die Scripting-Runtime von Unity zielt auf .NET Standard 2.1 (Project Settings > Player > Api Compatibility Level). Verwenden Sie ein MQTTnet-4.x-Release, das einen .NET-Standard-Build mitliefert; neuere Hauptversionen zielen möglicherweise nur auf modernes .NET und lassen sich in Unity nicht laden.
Zwei Installationswege:
- NuGetForUnity: Paket installieren, NuGet > Manage NuGet Packages öffnen, nach MQTTnet suchen und eine 4.x-Version wählen.
- Manuell: Die
.nupkgvon nuget.org herunterladen, in.zipumbenennen,lib/netstandard2.1/MQTTnet.dllentpacken und inAssets/Plugins/ablegen.
Wenn Sie mit IL2CPP bauen (Pflicht für iOS, üblich für Android und Konsolen), legen Sie eine link.xml in Assets/ an, damit der Code-Stripper keine Teile von MQTTnet entfernt, die per Reflection genutzt werden:
<linker>
<assembly fullname="MQTTnet" preserve="all" />
</linker>
Das MQTT-Manager-Skript
Legen Sie Assets/Scripts/MqttManager.cs an. Es verbindet sich über TLS, abonniert ein Topic-Präfix, speichert den neuesten Payload pro Topic und verbindet sich mit Backoff neu.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Protocol;
using UnityEngine;
public class MqttManager : MonoBehaviour
{
public static MqttManager Instance { get; private set; }
[SerializeField] private string host = "broker.connectmq.com";
[SerializeField] private int port = 8883;
[SerializeField] private bool useTls = true;
[SerializeField] private string topicPrefix = "connectmq/demo/u9n1ty";
[SerializeField] private string username = "";
[SerializeField] private string password = "";
private readonly ConcurrentDictionary<string, string> _latest = new();
private IMqttClient _client;
private CancellationTokenSource _cts;
private string _clientId;
public bool IsConnected => _client != null && _client.IsConnected;
public bool TryGet(string relativeTopic, out string payload) =>
_latest.TryGetValue($"{topicPrefix}/{relativeTopic}", out payload);
private void Awake()
{
if (Instance != null) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
}
private void Start()
{
// Unity-APIs wie Application müssen auf dem Main Thread gelesen werden
_clientId = $"unity-{Application.platform}-{Guid.NewGuid().ToString("N").Substring(0, 8)}";
_cts = new CancellationTokenSource();
_client = new MqttFactory().CreateMqttClient();
_client.ApplicationMessageReceivedAsync += e =>
{
_latest[e.ApplicationMessage.Topic] = e.ApplicationMessage.ConvertPayloadToString();
return Task.CompletedTask;
};
_ = Task.Run(() => ConnectionLoop(_cts.Token));
}
private async Task ConnectionLoop(CancellationToken ct)
{
var builder = new MqttClientOptionsBuilder()
.WithTcpServer(host, port)
.WithClientId(_clientId)
.WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V500)
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30));
if (useTls) builder.WithTlsOptions(o => o.UseTls());
if (!string.IsNullOrEmpty(username)) builder.WithCredentials(username, password);
var options = builder.Build();
var delay = 1;
while (!ct.IsCancellationRequested)
{
try
{
if (!_client.IsConnected)
{
await _client.ConnectAsync(options, ct);
await _client.SubscribeAsync(new MqttClientSubscribeOptionsBuilder()
.WithTopicFilter(f => f.WithTopic($"{topicPrefix}/#")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce))
.Build(), ct);
Debug.Log("MQTT connected");
delay = 1;
}
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
Debug.LogWarning($"MQTT connect failed: {ex.Message}");
delay = Math.Min(delay * 2, 30);
}
try { await Task.Delay(TimeSpan.FromSeconds(delay), ct); }
catch (OperationCanceledException) { break; }
}
}
public Task PublishAsync(string relativeTopic, string payload, bool retain = false)
{
if (!IsConnected) return Task.CompletedTask;
var msg = new MqttApplicationMessageBuilder()
.WithTopic($"{topicPrefix}/{relativeTopic}")
.WithPayload(payload)
.WithRetainFlag(retain)
.Build();
return _client.PublishAsync(msg);
}
private async void OnDestroy()
{
if (Instance != this) return;
_cts?.Cancel();
if (_client != null && _client.IsConnected)
{
try { await _client.DisconnectAsync(); } catch { }
}
_client?.Dispose();
}
}
Anmerkungen zu den Entscheidungen:
- QoS 0 ist für hochfrequente Visualisierungsdaten richtig. Geht der Wert eines Frames verloren, ersetzt ihn kurz darauf der nächste. Verwenden Sie QoS 1 für Befehle und Alarme.
- Die Client ID wird in
Start()auf dem Main Thread erzeugt, weilConnectionLoopauf einem Thread-Pool-Thread läuft, auf dem Unity-APIs wieApplicationundSystemInfonicht aufgerufen werden dürfen. Das zufällige Suffix verhindert, dass sich zwei Editor-Instanzen oder zwei Geräte gegenseitig vom Broker werfen. DontDestroyOnLoadhält eine Verbindung über Szenenwechsel hinweg, statt jedes Mal neu zu verbinden.- Das Trennen in
OnDestroyist im Editor wichtig: Ohne es hinterlässt jeder Play/Stop-Zyklus eine Geisterverbindung, bis das Keep-alive abläuft, und auf einem Broker mit Verbindungskontingent stoßen Sie schnell an die Grenze.
Topics an GameObjects binden
Jetzt kommt der spannende Teil. Eine wiederverwendbare Komponente, die einen Transform anhand eines Drehzahl-Topics rotieren lässt:
using System.Globalization;
using UnityEngine;
public class SpinFromTopic : MonoBehaviour
{
[SerializeField] private string topic = "fan1/rpm";
[SerializeField] private Vector3 axis = Vector3.up;
private float _targetRpm, _currentRpm;
private void Update()
{
if (MqttManager.Instance != null &&
MqttManager.Instance.TryGet(topic, out var raw) &&
float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var rpm))
{
_targetRpm = rpm;
}
// Weich zum Zielwert übergehen, damit Sprünge zwischen Nachrichten natürlich wirken
_currentRpm = Mathf.Lerp(_currentRpm, _targetRpm, Time.deltaTime * 2f);
transform.Rotate(axis, _currentRpm * 6f * Time.deltaTime); // rpm * 360 / 60
}
}
Und eine, die einen Renderer anhand eines JSON-Temperatur-Payloads einfärbt:
using UnityEngine;
public class ColorFromTemperature : MonoBehaviour
{
[System.Serializable] private class Reading { public float temperature; }
[SerializeField] private string topic = "tank1/telemetry";
[SerializeField] private float min = 20f, max = 90f;
[SerializeField] private Gradient gradient;
private Renderer _renderer;
private string _lastRaw;
private void Awake() => _renderer = GetComponent<Renderer>();
private void Update()
{
if (MqttManager.Instance == null || !MqttManager.Instance.TryGet(topic, out var raw)) return;
if (raw == _lastRaw) return; // nichts Neues
_lastRaw = raw;
var reading = JsonUtility.FromJson<Reading>(raw);
var t = Mathf.InverseLerp(min, max, reading.temperature);
_renderer.material.color = gradient.Evaluate(t);
}
}
Zwei Details, die Sie übernehmen sollten: Parsen Sie Zahlen mit CultureInfo.InvariantCulture (sonst scheitert 12.5 auf Rechnern mit Komma als Dezimaltrennzeichen – also etwa bei deutschen Systemeinstellungen), und überspringen Sie die Arbeit, wenn sich der Payload nicht geändert hat. JsonUtility ist schnell und allokationsarm, verarbeitet aber nur Felder serialisierbarer Klassen; für verschachteltes oder dynamisches JSON ist Newtonsoft Json (als Unity-Paket verfügbar) flexibler.
Mit Testdaten füttern
Legen Sie ein leeres GameObject mit MqttManager, einen Zylinder mit SpinFromTopic und eine Kapsel mit ColorFromTemperature an. Drücken Sie Play und simulieren Sie dann Sensoren im Terminal:
# Lüfter mit 120 U/min, dann mit 600 U/min drehen
mosquitto_pub -h broker.connectmq.com -p 1883 -i unity-sim-1 -t connectmq/demo/u9n1ty/fan1/rpm -m 120
mosquitto_pub -h broker.connectmq.com -p 1883 -i unity-sim-1 -t connectmq/demo/u9n1ty/fan1/rpm -m 600
# Tank in einer Schleife aufheizen (bash)
for t in $(seq 20 5 90); do
mosquitto_pub -h broker.connectmq.com -p 1883 -i unity-sim-2 \
-t connectmq/demo/u9n1ty/tank1/telemetry -m "{\"temperature\": $t}"
sleep 0.5
done
Der Lüfter sollte sanft auf Touren kommen und der Tank entlang Ihres Farbverlaufs wandern. Der öffentliche MQTT-Broker erlaubt 10 Nachrichten pro Sekunde und Client; halten Sie Simulatoren darunter und denken Sie daran, dass alles auf dem öffentlichen MQTT-Broker für alle sichtbar ist – er ist für Tests und Prototypen gedacht, nicht für Produktivdaten. Weitere Tricks zum Veröffentlichen finden Sie in den Beispielen für mosquitto_pub und mosquitto_sub.
Befehle aus der Szene senden
Ein Zwilling wird mehr als ein Viewer, wenn Nutzer über ihn handeln können. Verknüpfen Sie einen UI-Button oder einen Klick auf ein 3D-Objekt mit MqttManager.Instance.PublishAsync("pump1/cmd", "{\"action\":\"stop\"}") und lassen Sie das echte Gerät (oder Ihren Simulator) durch Veröffentlichen seines neuen Zustands bestätigen. Animieren Sie anhand des bestätigten Zustands, nicht anhand des Klicks, damit die Szene nie etwas zeigt, das die Anlage nicht getan hat. Verwenden Sie für Befehle QoS 1 und erwägen Sie eine Message Expiry, damit ein Gerät, das sich viel später wieder verbindet, keine veraltete Anweisung ausführt.
Plattformhinweise für einen Unity MQTT Client
| Zielplattform | Funktioniert MQTTnet über TCP/TLS? | Hinweise |
|---|---|---|
| Windows, macOS, Linux Standalone | Ja | Einfachstes Ziel; hier zuerst testen |
| Android | Ja | IL2CPP braucht link.xml; Internetzugriff in den Player Settings erlauben |
| iOS | Ja | IL2CPP und link.xml; TLS auf 8883 bevorzugen |
| WebGL | Nein | Browser erlauben keine rohen TCP-Sockets. MQTT über WebSocket mit einer JavaScript-Brücke (eine .jslib, die eine Browser-MQTT-Bibliothek kapselt) zu wss://broker.connectmq.com:8084/mqtt nutzen |
| VR-Headsets (Android-basiert) | Ja | Wie Android; Frame-Zeit im Blick behalten, Handler minimal halten |
WebGL überrascht viele. Ist Ihr eigentliches Ziel ein browserbasierter Zwilling, prüfen Sie, ob ein Web-3D-Stack besser passt; der Leitfaden zu MQTT über WebSocket erklärt die Browserseite.
Von der Live-Szene zum digitalen Zwilling
Objekte über Topics zu animieren ist der erste Schritt. Ein nützlicher Zwilling braucht außerdem eine konsistente Topic-Struktur, Retained-Zustände, damit die Szene im Moment des Ladens korrekt ist, und einen Weg, Befehle zurückzusenden. ConnectMQs eigene 3D-Digital-Twin-Webdemo – eine Fabrik mit CNC-Maschine, Roboterarm, Ofen, Tank, Förderband und Pumpe – folgt genau diesem Ansatz mit Live-Daten über sicheres WebSocket. Topics nach ISA-95-Ebenen zu organisieren (enterprise/site/area/line/cell) erleichtert zudem die Abbildung auf die Szenenhierarchie. Genau diese Struktur bildet im Werk den Unified Namespace (UNS) auf dem MQTT-Broker, etwa acme/werk-stuttgart/montage/linie-1/zelle-3/temperatur: Die Szenenhierarchie in Unity spiegelt dann Standort, Halle, Linie und Zelle, und MES, Dashboards und Zwilling lesen dieselben Topics. Mehr dazu in MQTT Sparkplug B und Unified Namespace. Entwickeln Sie rund um die Szene auch .NET-Werkzeuge, behandelt das MQTTnet-Tutorial MQTT-5-Properties und TLS ausführlicher.
Der Weg in den Produktivbetrieb
Für alles jenseits einer Demo verbinden Sie Unity mit einem privaten Broker. Ein verwalteter MQTT-Broker wie ConnectMQ bietet TLS mit Let's-Encrypt-Zertifikat, MQTT 5 und generierte Zugangsdaten; tragen Sie host, username und password im Inspector ein (oder besser: laden Sie sie zur Laufzeit aus einer Konfigurationsdatei außerhalb des Builds). Entziehen Sie Viewer-Apps nach Möglichkeit die Publish-Rechte, damit ein dekompilierter Build keine gefälschten Sensordaten einspeisen kann. Wenn Sie entscheiden, wo der Broker laufen soll, helfen die Übersicht, was ein MQTT-Broker macht, und der Leitfaden zu MQTT-Client-Bibliotheken bei der Einordnung.
Starten Sie mit ConnectMQ
Verbinden Sie Ihre Unity-Szene in wenigen Minuten mit einem privaten Broker: Registrieren Sie sich unter app.connectmq.com im kostenlosen Serverless-Plan, ganz ohne Kreditkarte, und sehen Sie im Live-Topic-Explorer des Portals genau, was Ihre Szene während der Entwicklung abonniert hat.
Häufig gestellte Fragen
Welche MQTT-Bibliothek sollte ich in Unity verwenden?
MQTTnet 4.x ist eine solide Wahl: Es ist weit verbreitet, unterstützt MQTT 5 und TLS und liefert einen mit Unity kompatiblen .NET-Standard-Build. Meiden Sie Bibliotheken, die den Main Thread blockieren oder APIs nutzen, die Unity nicht unterstützt.
Warum wirft mein Unity MQTT Client „can only be called from the main thread“?
Sie greifen im Message-Handler auf Unity-Objekte zu. Legen Sie den Payload in einer threadsicheren Collection ab und wenden Sie ihn in Update() an, wie in diesem Tutorial gezeigt.
Kann sich ein Unity-WebGL-Build mit MQTT verbinden?
Nicht über TCP-Sockets. Ein WebGL-Build muss MQTT über sicheres WebSocket mit einem JavaScript-Plugin nutzen und sich mit einem WSS-Endpunkt verbinden, etwa Port 8084 mit dem Pfad /mqtt.
Wie viele Nachrichten pro Sekunde verkraftet eine Unity-Szene?
Die Grenze ist meist Ihre Verarbeitung pro Nachricht, nicht MQTT. Mit dem Muster „neuester Wert pro Topic“ leistet die Szene unabhängig von der Nachrichtenrate konstante Arbeit pro Frame, sodass Broker-Plan und Netzwerk zu den praktischen Grenzen werden.



