Un jumeau numérique Unity amène les données réelles d'une machine dans un moteur de jeu, avec à la clé un rendu physique réaliste, la prise en charge de la VR et de l'AR et un éditeur mature pour construire des scènes détaillées. Dans ce tutoriel, vous allez connecter une scène Unity à MQTT avec la bibliothèque MQTTnet, animer une pompe à partir de la télémétrie en direct, renvoyer des commandes de marche et d'arrêt à l'appareil, puis sécuriser la connexion avec TLS. Vous testerez le tout sur le serveur MQTT public gratuit broker.connectmq.com à l'aide d'un petit simulateur Python.
Quand Unity est-il le bon outil pour un jumeau numérique ?
Les moteurs pour navigateur comme Three.js sont le moyen le plus simple de mettre un jumeau sous les yeux de nombreuses personnes. Unity excelle lorsque le jumeau est une application à part entière :
| Besoin | Unity | Navigateur (Three.js / Babylon.js) |
|---|---|---|
| Formation des opérateurs, casques VR ou AR | Excellent, prise en charge XR intégrée | Possible via WebXR, moins d'outillage |
| Scènes très détaillées, physique, éclairage | Excellent | Bon, limité par les ressources du navigateur |
| Accès sans installation pour de nombreux utilisateurs | Nécessite un build par plateforme | Excellent, une simple URL |
| MQTT brut sur TCP | Oui, sur les builds desktop et mobiles | Non, WebSocket uniquement |
| Compétences de l'équipe | C# et l'éditeur Unity | JavaScript et outillage web |
Si votre jumeau doit s'ouvrir depuis un lien dans n'importe quel navigateur, commencez plutôt par le tutoriel jumeau numérique avec Three.js. S'il s'agit d'une application de salle de contrôle, d'un simulateur de formation ou d'une expérience sur casque, Unity est un excellent choix. Pour les concepts communs aux deux, consultez notre guide de la technologie du jumeau numérique.
Ce que vous allez construire
Une pompe dans une scène Unity qui :
- S'abonne à
connectmq/demo/unity-7f3a/pump1/telemetryet litrpm,temperatureetrunningdans le JSON. - Fait tourner sa roue à la vitesse indiquée et fait passer la couleur du corps du bleu au rouge à mesure qu'elle chauffe.
- Publie des commandes
STARTetSTOPsurconnectmq/demo/unity-7f3a/pump1/cmddepuis des boutons d'interface. - Se reconnecte automatiquement en cas de coupure réseau.
Remplacez unity-7f3a par votre propre suffixe aléatoire afin que vos topics soient uniques sur le serveur MQTT public gratuit, qui est partagé.
Étape 1 : ajouter MQTTnet au projet
MQTTnet est un client MQTT .NET open source et mature, qui fonctionne avec le profil .NET Standard 2.1 d'Unity. Ce tutoriel utilise l'API 4.3.x.
- Dans Project Settings > Player > Other Settings, réglez Api Compatibility Level sur .NET Standard 2.1.
- Téléchargez le paquet
MQTTnet4.3.x depuis NuGet (le fichier.nupkgest une archive zip), extrayezlib/netstandard2.1/MQTTnet.dllet placez-le dansAssets/Plugins/. Vous pouvez aussi l'installer avec le gestionnaire de paquets NuGetForUnity. - Si vous compilez avec IL2CPP (mobile et la plupart des consoles), ajoutez
Assets/link.xmlpour que le code stripping ne supprime pas les types que MQTTnet charge par réflexion :
<linker>
<assembly fullname="MQTTnet" preserve="all"/>
</linker>
Notez que MQTTnet 5 a renommé plusieurs types (par exemple, MqttFactory est devenu MqttClientFactory). Si vous mettez à jour plus tard, adaptez les appels à la factory ; la structure générale du script reste identique.
Étape 2 : simuler la pompe
Exécutez ce script Python sur n'importe quel ordinateur pour produire de la télémétrie et réagir aux commandes. Il utilise 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)
Deux messages par seconde, c'est bien en deçà de la limite de 10 messages par seconde et par client du serveur MQTT public gratuit.
Étape 3 : le script du jumeau numérique Unity
Créez Assets/Scripts/PumpTwin.cs. Attachez-le à un GameObject vide, puis faites glisser le Transform de votre roue et le Renderer du corps de la pompe dans les champs de l'inspecteur. Un cylindre pour le corps et un cube aplati en guise de pale suffisent pour commencer.
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();
// S'exécute sur un thread d'arrière-plan : uniquement mettre en file, ne jamais toucher aux objets Unity ici.
_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()
{
// Vider la file des messages sur le thread principal.
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); // lissage indépendant de la fréquence d'images
_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);
}
}
// Reliez ces méthodes à des boutons d'interface (OnClick) dans l'inspecteur.
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();
}
}
Appuyez sur Play pendant que le simulateur tourne. La roue se met à tourner, le corps passe progressivement du bleu au rouge, et un clic sur un bouton Stop relié à SendStop() fait arrêter la pompe par le simulateur ; en moins d'une seconde, le jumeau ralentit la roue et le corps commence à refroidir. Si vous arrêtez complètement le simulateur, le corps devient gris au bout de cinq secondes.
Comment fonctionne l'intégration MQTT dans Unity
Le threading est le piège numéro un. MQTTnet déclenche ApplicationMessageReceivedAsync sur un thread du pool, alors que presque toutes les API Unity (transforms, matériaux, Debug.DrawLine, instanciation) doivent être appelées depuis le thread principal. Les appeler depuis le handler provoque des erreurs ou, pire, des échecs intermittents. Le script contourne le problème en plaçant les payloads bruts dans une ConcurrentQueue et en les traitant dans Update().
Le lissage masque le rythme du réseau. La télémétrie arrive deux fois par seconde, mais Unity affiche 60 images par seconde ou plus. Interpoler vers la dernière valeur cible rend le mouvement continu. Le facteur exponentiel garantit un lissage identique quelle que soit la fréquence d'images.
Les commandes utilisent la QoS 1, la télémétrie la QoS 0. Un échantillon de télémétrie perdu est remplacé une demi-seconde plus tard, alors qu'une commande d'arrêt perdue pose un vrai problème. Lisez les niveaux de QoS MQTT expliqués pour les détails.
L'appareil fait foi. Le jumeau ne modifie jamais son propre état quand vous appuyez sur Stop. Il attend que la pompe indique running: false. Le jumeau reste ainsi fidèle à la réalité si l'appareil rejette ou manque une commande, un principe approfondi dans la synchronisation d'état d'un jumeau numérique.
Faire évoluer le jumeau numérique Unity
Un script par machine suffit pour une démo, mais une usine compte de nombreux équipements. Voici une structure plus propre pour les scènes de grande taille :
- Un gestionnaire de connexion unique par application, un singleton qui possède l'
IMqttClient, s'abonne avec un caractère générique commeplant1/#et achemine les messages selon leur topic. - Un composant de liaison par équipement (
TwinBinding) avec un suffixe de topic et une référence aux parties qu'il pilote. Le gestionnaire retrouve les liaisons dans un dictionnaire indexé par topic. - Des ScriptableObjects pour la configuration, afin que les paramètres du broker et les préfixes de topics diffèrent entre les builds de développement et de production sans modifier le code.
- Des messages retenus sur les topics d'état, pour que le jumeau affiche le bon état dès son démarrage.
Associer des champs à des maillages, des matériaux et des animations est le même problème quel que soit le moteur ; lier des données IoT à un modèle 3D détaille les patterns.
Notes par plateforme et dépannage
- Les builds WebGL ne peuvent pas ouvrir de sockets TCP, et le transport WebSocket de MQTTnet s'appuie sur des API .NET indisponibles dans Unity WebGL. Pour un build navigateur, appelez un client MQTT JavaScript comme MQTT.js via un plugin
.jslib, ou construisez la version navigateur avec un moteur web. - Android requiert la permission INTERNET, qu'Unity ajoute automatiquement lorsqu'il détecte du code réseau ; vérifiez-la si les connexions échouent silencieusement sur l'appareil.
- Connect failed: NotAuthorized signifie des identifiants ou un client ID erronés. Connect failed: timeout signifie généralement que le port est bloqué ; testez avec
nc -vz broker.connectmq.com 8883. - Une boucle de déconnexion toutes les quelques secondes indique que deux instances partagent le même client ID, par exemple l'éditeur et un build autonome. Laissez
clientIdvide en développement pour en obtenir un aléatoire.
Sécuriser la connexion
Le serveur MQTT public gratuit est anonyme et lisible par tous ; réservez-le donc aux tests et aux prototypes. Pour une vraie usine, créez un broker privé sur un broker MQTT managé et connectez-vous en TLS. Avec ConnectMQ, le portail vous fournit un nom d'utilisateur, un mot de passe et un client ID générés ; dans l'inspecteur, réglez Port sur 8883, cochez Use Tls et renseignez YOUR_USERNAME, YOUR_PASSWORD et YOUR_CLIENT_ID. Le certificat du broker est émis par Let's Encrypt : .NET le valide donc avec le magasin de certificats de confiance du système d'exploitation, sans fichier supplémentaire. Gardez les identifiants hors du contrôle de version en les chargeant à l'exécution depuis un fichier de configuration ou une variable d'environnement.
Démarrer avec ConnectMQ
Un jumeau numérique Unity a besoin d'un broker toujours joignable depuis l'usine et depuis chaque poste opérateur. ConnectMQ fournit exactement cela sous forme de broker MQTT managé : créez un broker gratuit sur app.connectmq.com avec le plan Serverless, sans carte bancaire, avec MQTT 5, TLS et jusqu'à 25 connexions. Son client MQTT intégré au navigateur et son explorateur de topics en direct permettent de vérifier facilement ce que publient vos appareils avant de les lier dans Unity. Vous pouvez aussi continuer à prototyper sur le serveur MQTT public gratuit jusqu'à ce que vous soyez prêt.
Questions fréquentes
Quelle bibliothèque MQTT fonctionne le mieux dans Unity ?
MQTTnet est l'option la plus répandue pour les builds Unity desktop et mobiles, car elle prend en charge MQTT 3.1.1 et 5.0, TLS et des API asynchrones sur .NET Standard 2.1. Pour les builds WebGL, il faut un client JavaScript relié via un plugin.
Pourquoi ai-je des erreurs quand je modifie des objets dans le handler de messages ?
Le handler s'exécute sur un thread d'arrière-plan et la plupart des API Unity ne fonctionnent que sur le thread principal. Mettez les messages en file dans le handler et appliquez-les dans Update().
Un jumeau numérique Unity peut-il piloter de vrais équipements ?
Oui, en publiant des commandes sur des topics auxquels l'appareil est abonné. Utilisez la QoS 1, demandez confirmation pour les actions destructrices dans l'interface et faites republier par l'appareil son état réel, afin que le jumeau reflète ce qui s'est réellement passé.
Est-ce que cela fonctionne avec des casques VR ?
Oui. Le script MQTT est indépendant de la configuration de rendu ; le même composant fonctionne donc dans une scène configurée pour la XR, à condition que la cible de build du casque prenne en charge les API réseau utilisées.



