Um gêmeo digital com Unity leva dados reais de máquinas para dentro de um motor de jogos, onde você conta com renderização baseada em física, suporte a VR e AR e um editor maduro para construir cenas detalhadas. Neste tutorial, você vai conectar uma cena Unity ao MQTT com a biblioteca MQTTnet, animar uma bomba com telemetria ao vivo, publicar comandos de partida e parada para o dispositivo e, depois, proteger a conexão com TLS. Você vai testar tudo no servidor MQTT público em broker.connectmq.com usando um pequeno simulador em Python.
Quando o Unity É a Ferramenta Certa para um Gêmeo Digital
Motores de navegador como o Three.js são a forma mais fácil de colocar um gêmeo diante de muitas pessoas. O Unity se destaca quando o gêmeo é uma aplicação por si só:
| Necessidade | Unity | Navegador (Three.js / Babylon.js) |
|---|---|---|
| Treinamento de operadores, óculos de VR ou AR | Forte, suporte a XR integrado | Possível via WebXR, com menos ferramentas |
| Cenas muito detalhadas, física, iluminação | Forte | Bom, limitado pelos recursos do navegador |
| Acesso sem instalação para muitos usuários | Exige um build por plataforma | Forte, basta uma URL |
| MQTT puro sobre TCP | Sim, em builds desktop e mobile | Não, apenas WebSocket |
| Competências da equipe | C# e o editor do Unity | JavaScript e ferramentas web |
Se o seu gêmeo precisa abrir a partir de um link em qualquer navegador, comece pelo tutorial de gêmeo digital com Three.js. Se for uma aplicação de sala de controle, um simulador de treinamento ou uma experiência em óculos, o Unity é uma escolha forte. Para os conceitos por trás de ambos, veja nosso guia de tecnologia de gêmeo digital.
O Que Você Vai Construir
Uma bomba em uma cena Unity que:
- Assina
connectmq/demo/unity-7f3a/pump1/telemetrye lêrpm,temperatureerunningdo JSON. - Gira o rotor na velocidade informada e muda a cor do corpo de azul para vermelho à medida que esquenta.
- Publica comandos
STARTeSTOPemconnectmq/demo/unity-7f3a/pump1/cmda partir de botões da interface. - Reconecta automaticamente quando a rede cai.
Substitua unity-7f3a por um sufixo aleatório seu para que seus tópicos sejam únicos no servidor MQTT público compartilhado.
Passo 1: Adicione o MQTTnet ao Projeto
O MQTTnet é um cliente MQTT .NET maduro e de código aberto que roda no perfil .NET Standard 2.1 do Unity. Este tutorial usa a API 4.3.x.
- Em Project Settings > Player > Other Settings, defina Api Compatibility Level como .NET Standard 2.1.
- Baixe o pacote
MQTTnet4.3.x do NuGet (o arquivo.nupkgé um arquivo zip), extraialib/netstandard2.1/MQTTnet.dlle coloque-o emAssets/Plugins/. Como alternativa, instale-o com o gerenciador de pacotes NuGetForUnity. - Se você compila com IL2CPP (mobile e a maioria dos consoles), adicione
Assets/link.xmlpara que o code stripping não remova tipos que o MQTTnet carrega por reflexão:
<linker>
<assembly fullname="MQTTnet" preserve="all"/>
</linker>
Observe que o MQTTnet 5 renomeou vários tipos (por exemplo, MqttFactory passou a ser MqttClientFactory). Se você atualizar mais tarde, ajuste as chamadas de factory; a estrutura geral do script permanece a mesma.
Passo 2: Simule a Bomba
Execute este script Python em qualquer computador para gerar telemetria e reagir a comandos. Ele usa o 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)
Duas mensagens por segundo ficam bem dentro do limite do servidor MQTT público de 10 mensagens por segundo por cliente.
Passo 3: O Script do Gêmeo Digital no Unity
Crie Assets/Scripts/PumpTwin.cs. Anexe-o a um GameObject vazio e arraste o Transform do rotor e o Renderer do corpo da bomba para os campos do inspector. Um cilindro para o corpo e um cubo achatado como pá do rotor bastam para começar.
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();
// Roda em uma thread de segundo plano: apenas enfileire, nunca toque em objetos Unity aqui.
_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()
{
// Processa as mensagens na 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); // suavização independente da taxa de quadros
_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);
}
}
// Conecte estes métodos a botões da UI (OnClick) no inspector.
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();
}
}
Pressione Play com o simulador em execução. O rotor acelera, o corpo esquenta de azul para vermelho e, ao clicar em um botão Stop ligado a SendStop(), o simulador para a bomba; em menos de um segundo, o gêmeo desacelera o rotor e o corpo começa a esfriar. Se você encerrar o simulador por completo, o corpo fica cinza após cinco segundos.
Como Funciona a Integração MQTT no Unity
Threads são a armadilha número um. O MQTTnet dispara ApplicationMessageReceivedAsync em uma thread do thread pool, mas quase todas as APIs do Unity (transforms, materiais, Debug.DrawLine, instanciação) precisam ser chamadas a partir da thread principal. Chamá-las a partir do handler gera erros ou, pior, falhas intermitentes. O script evita isso colocando os payloads brutos em uma ConcurrentQueue e processando-os em Update().
A suavização esconde o ritmo da rede. A telemetria chega duas vezes por segundo, mas o Unity renderiza 60 quadros por segundo ou mais. Interpolar em direção ao alvo mais recente torna o movimento contínuo. O fator exponencial mantém a suavização idêntica, independentemente da taxa de quadros.
Comandos usam QoS 1, telemetria QoS 0. Uma amostra de telemetria perdida é substituída meio segundo depois, enquanto um comando de parada perdido é um problema real. Leia níveis de QoS do MQTT explicados para os detalhes.
O dispositivo é a fonte da verdade. O gêmeo nunca altera o próprio estado quando você pressiona Stop. Ele espera a bomba informar running: false. Isso mantém o gêmeo fiel à realidade se o dispositivo rejeitar ou perder um comando, um princípio aprofundado em sincronização de estado do gêmeo digital.
Escalando o Gêmeo Digital no Unity
Um script por máquina funciona para uma demo, mas uma planta tem muitos ativos. Uma estrutura mais limpa para cenas maiores:
- Um gerenciador de conexão por aplicação, um singleton que detém o
IMqttClient, assina com um curinga comoplant1/#e roteia as mensagens por tópico. - Um componente de vínculo por ativo (
TwinBinding), com um sufixo de tópico e uma referência às partes que ele controla. O gerenciador busca os vínculos em um dicionário indexado por tópico. - ScriptableObjects para a configuração, para que as configurações do broker e os prefixos de tópico sejam diferentes entre builds de desenvolvimento e de produção sem mudanças de código.
- Mensagens retidas nos tópicos de estado, para que o gêmeo mostre o estado correto no momento em que inicia.
Mapear campos para malhas, materiais e animações é o mesmo problema em qualquer motor; como vincular dados IoT a um modelo 3D aborda os padrões em detalhes.
Notas por Plataforma e Solução de Problemas
- Builds WebGL não conseguem abrir sockets TCP, e o transporte WebSocket do MQTTnet depende de APIs .NET que não estão disponíveis no Unity WebGL. Para um build de navegador, chame um cliente MQTT em JavaScript, como o MQTT.js, por meio de um plugin
.jslib, ou construa a versão para navegador com um motor web. - O Android precisa da permissão INTERNET, que o Unity adiciona automaticamente quando detecta código de rede; confira-a se as conexões falharem silenciosamente no dispositivo.
- Connect failed: NotAuthorized significa credenciais ou client ID incorretos. Connect failed: timeout geralmente significa que a porta está bloqueada; teste com
nc -vz broker.connectmq.com 8883. - Loop de desconexão a cada poucos segundos significa que duas instâncias compartilham o mesmo client ID, por exemplo o editor e um build standalone. Deixe
clientIdvazio em desenvolvimento para obter um aleatório.
Protegendo a Conexão
O servidor MQTT público é anônimo e pode ser lido por qualquer pessoa, então use-o apenas para testes e protótipos. Para uma planta real, como uma estação de bombeamento de saneamento ou uma usina, crie um broker MQTT gerenciado privado e conecte-se via TLS. Com a ConnectMQ, o portal fornece um nome de usuário, uma senha e um client ID gerados; no inspector, defina Port como 8883, marque Use Tls e preencha YOUR_USERNAME, YOUR_PASSWORD e YOUR_CLIENT_ID. O certificado do broker é emitido pela Let's Encrypt, então o .NET o valida contra o repositório de confiança do sistema operacional, sem arquivos extras. Mantenha as credenciais fora do controle de versão carregando-as de um arquivo de configuração ou de uma variável de ambiente em tempo de execução.
Comece com a ConnectMQ
Um gêmeo digital com Unity precisa de um broker sempre acessível a partir da planta e de cada estação de trabalho de operador. A ConnectMQ oferece exatamente isso como broker MQTT gerenciado: crie um broker gratuito em app.connectmq.com no plano Serverless, sem cartão de crédito, com MQTT 5, TLS e até 25 conexões. O cliente MQTT no navegador e o explorador de tópicos ao vivo facilitam verificar o que seus dispositivos publicam antes de vincular os dados no Unity. Você também pode continuar prototipando no servidor MQTT público (gratuito) até estar pronto.
Perguntas frequentes
Qual biblioteca MQTT funciona melhor no Unity?
O MQTTnet é a opção mais usada em builds desktop e mobile do Unity, porque oferece suporte a MQTT 3.1.1 e 5.0, TLS e APIs assíncronas no .NET Standard 2.1. Para builds WebGL, você precisa de um cliente JavaScript integrado por meio de um plugin.
Por que recebo erros ao atualizar objetos no handler de mensagens?
O handler roda em uma thread de segundo plano, e a maioria das APIs do Unity só funciona na thread principal. Enfileire as mensagens no handler e aplique-as em Update().
Um gêmeo digital com Unity pode controlar equipamentos reais?
Sim, publicando comandos em tópicos que o dispositivo assina. Use QoS 1, confirme ações destrutivas na interface e faça o dispositivo publicar de volta o seu estado real, para que o gêmeo reflita o que de fato aconteceu.
Isso funciona com óculos de VR?
Sim. O script MQTT é independente da configuração de renderização, então o mesmo componente funciona em uma cena configurada para XR, desde que o target de build dos óculos ofereça suporte às APIs de rede utilizadas.



