Tutoriais de IoT

Cliente MQTT no Unity: Controle Cenas 3D com Dados de Sensores ao Vivo

Adicione um cliente MQTT ao Unity com MQTTnet, assine topics de sensores no broker.connectmq.com e anime GameObjects com segurança a partir de dados ao vivo na thread principal, a base de um gêmeo digital.

Cena do editor Unity com um ventilador girando e um tanque com cores por temperatura, controlados por um cliente MQTT Unity
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Um cliente MQTT Unity transforma uma cena 3D estática em uma visão ao vivo de equipamentos reais: um ventilador gira na rotação medida, um tanque muda de cor conforme a temperatura, uma porta se abre quando um sensor indica. Neste tutorial você vai adicionar o MQTTnet a um projeto Unity, assinar topics de sensores no servidor MQTT público (gratuito) broker.connectmq.com e atualizar GameObjects com segurança a partir da thread principal do Unity. É a mesma base usada por gêmeos digitais no navegador e no desktop, por exemplo de uma linha de envase ou de uma estação de bombeamento de saneamento.

Como os dados ao vivo chegam a uma cena Unity

O fluxo tem três partes, e a do meio é onde a maioria das primeiras tentativas dá errado:

Sensor or simulator --MQTT--> Broker --MQTT--> MQTTnet (background thread)
                                                  |
                                       ConcurrentDictionary (latest value per topic)
                                                  |
                                       MonoBehaviour.Update() (main thread) --> Transforms, materials

A API do Unity não é thread-safe. Os callbacks de rede do MQTTnet rodam em threads do thread pool, e mexer em um Transform ou Material a partir delas lança UnityException: ... can only be called from the main thread ou, pior, falha em silêncio. A solução é guardar os valores recebidos em uma estrutura thread-safe e aplicá-los no Update().

Para visualização, normalmente só importa o valor mais recente de cada topic, não todas as mensagens. Um dicionário indexado por topic oferece "o valor mais recente vence" de graça e mantém o tempo de frame constante, mesmo que um sensor publique mais rápido que a sua taxa de quadros.

Adicionando o MQTTnet ao Unity

O runtime de scripts do Unity tem como alvo o .NET Standard 2.1 (Project Settings > Player > Api Compatibility Level). Use uma versão 4.x do MQTTnet, que traz um build .NET Standard; versões principais mais novas podem ter como alvo apenas o .NET moderno e não carregam no Unity.

Duas formas de instalar:

  1. NuGetForUnity: instale o pacote, abra NuGet > Manage NuGet Packages, pesquise MQTTnet e escolha uma versão 4.x.
  2. Manual: baixe o .nupkg do nuget.org, renomeie para .zip, extraia lib/netstandard2.1/MQTTnet.dll e coloque-o em Assets/Plugins/.

Se você compila com IL2CPP (obrigatório para iOS e comum em Android e consoles), adicione um link.xml em Assets/ para que o code stripper não remova partes do MQTTnet usadas via reflexão:

<linker>
  <assembly fullname="MQTTnet" preserve="all" />
</linker>

O script gerenciador de MQTT

Crie Assets/Scripts/MqttManager.cs. Ele se conecta via TLS, assina um prefixo de topic, guarda o payload mais recente de cada topic e reconecta com back-off.

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()
    {
        // APIs do Unity como Application precisam ser lidas na thread principal
        _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();
    }
}

Observações sobre as escolhas:

  • QoS 0 é o certo para dados de visualização em alta frequência. Se o valor de um frame se perder, o próximo o substitui um instante depois. Use QoS 1 para comandos e alarmes.
  • O client ID é montado no Start(), na thread principal, porque o ConnectionLoop roda em uma thread do thread pool, onde APIs do Unity como Application e SystemInfo não podem ser chamadas. O sufixo aleatório evita que duas instâncias do editor ou dois dispositivos derrubem um ao outro do broker.
  • DontDestroyOnLoad mantém uma única conexão entre carregamentos de cena, em vez de reconectar toda vez.
  • Desconectar no OnDestroy é importante no editor: sem isso, cada ciclo de Play/Stop deixa uma conexão fantasma até o keep-alive expirar, e em um broker com cota de conexões você pode esgotá-la rapidamente.

Ligando topics a GameObjects

Agora a parte divertida. Um componente reutilizável que gira um transform a partir de um topic de RPM:

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;
        }
        // Suaviza em direção ao alvo para que os saltos entre mensagens pareçam naturais
        _currentRpm = Mathf.Lerp(_currentRpm, _targetRpm, Time.deltaTime * 2f);
        transform.Rotate(axis, _currentRpm * 6f * Time.deltaTime); // rpm * 360 / 60
    }
}

E outro que colore um renderer a partir de um payload JSON de temperatura:

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; // nada de novo
        _lastRaw = raw;
        var reading = JsonUtility.FromJson<Reading>(raw);
        var t = Mathf.InverseLerp(min, max, reading.temperature);
        _renderer.material.color = gradient.Evaluate(t);
    }
}

Dois detalhes que vale copiar: faça o parse de números com CultureInfo.InvariantCulture (caso contrário 12.5 falha em máquinas que usam vírgula como separador decimal, como as configuradas em português) e evite trabalho quando o payload não mudou. O JsonUtility é rápido e aloca pouca memória, mas só lida com campos de classes serializáveis; para JSON aninhado ou dinâmico, o Newtonsoft Json (disponível como pacote do Unity) é mais flexível.

Alimentando a cena com dados de teste

Crie um GameObject vazio com o MqttManager, um cilindro com SpinFromTopic e uma cápsula com ColorFromTemperature. Aperte Play e simule sensores a partir de um terminal:

# Gira o ventilador a 120 RPM e depois a 600 RPM
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

# Aquece o tanque em um loop (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

O ventilador deve acelerar suavemente e o tanque deve percorrer o seu gradiente. O servidor MQTT público permite 10 mensagens por segundo por cliente, então mantenha os simuladores abaixo disso, e lembre-se de que a área pública é visível para todos e serve para testes e protótipos, não para dados de produção. Mais truques de publicação estão nos exemplos de mosquitto_pub e mosquitto_sub.

Enviando comandos a partir da cena

Um gêmeo digital vira mais do que um visualizador quando os usuários podem agir sobre ele. Ligue um botão de UI ou um clique em um objeto 3D a MqttManager.Instance.PublishAsync("pump1/cmd", "{\"action\":\"stop\"}") e faça o dispositivo real (ou o seu simulador) confirmar publicando o novo estado. Anime a partir do estado confirmado, não do clique, para que a cena nunca mostre algo que o equipamento não fez. Para comandos, use QoS 1 e considere uma expiração de mensagem, para que um dispositivo que reconecte muito depois não execute uma instrução desatualizada.

Notas por plataforma para um cliente MQTT Unity

Alvo Funciona com MQTTnet via TCP/TLS? Observações
Windows, macOS, Linux standalone Sim O alvo mais simples; teste aqui primeiro
Android Sim IL2CPP precisa do link.xml; permita acesso à internet nas Player settings
iOS Sim IL2CPP e link.xml; prefira TLS na 8883
WebGL Não Navegadores não permitem sockets TCP brutos. Use MQTT over WebSocket por meio de uma ponte JavaScript (um .jslib que envolve uma biblioteca MQTT de navegador) até wss://broker.connectmq.com:8084/mqtt
Headsets de VR (baseados em Android) Sim Igual ao Android; fique de olho no tempo de frame e mantenha os handlers mínimos

O WebGL é o que mais surpreende. Se o seu objetivo final é um gêmeo no navegador, avalie se uma stack 3D web não se encaixa melhor; o guia de MQTT over WebSocket explica o lado do navegador.

Da cena ao vivo ao gêmeo digital

Animar objetos a partir de topics é o primeiro passo. Um gêmeo digital útil também precisa de uma estrutura de topics consistente, de estado retido para que a cena esteja correta no momento em que carrega e de uma forma de enviar comandos de volta. A própria demonstração web de gêmeo digital 3D da ConnectMQ, uma fábrica com uma máquina CNC, um braço robótico, um forno, um tanque, uma esteira e uma bomba, segue exatamente essa abordagem, com dados ao vivo via WebSocket seguro. Organizar os topics com os níveis ISA-95 (enterprise/site/area/line/cell) também facilita mapear topics para a hierarquia da cena; leia MQTT Sparkplug B e Unified Namespace para conhecer essa estrutura. Se você também cria ferramentas .NET em torno da cena, o tutorial de MQTTnet aborda propriedades do MQTT 5 e TLS em mais profundidade.

Levando para produção

Para qualquer coisa além de uma demonstração, conecte o Unity a um broker privado. Um broker MQTT gerenciado como a ConnectMQ oferece TLS com certificado Let's Encrypt, MQTT 5 e credenciais geradas; preencha host, username e password no inspector (ou, melhor ainda, carregue-os em tempo de execução a partir de um arquivo de configuração fora do build). Sempre que possível, não dê direito de publicação a apps de visualização, para que um build descompilado não consiga injetar dados falsos de sensores. Se você está decidindo onde o broker deve ficar, a visão geral sobre o que um broker MQTT faz e o guia de bibliotecas de cliente MQTT ajudam a enquadrar as opções.

Comece a usar a ConnectMQ

Aponte sua cena Unity para um broker privado em minutos: cadastre-se em app.connectmq.com no plano Serverless gratuito, sem cartão de crédito, e use o explorador de topics em tempo real do portal para ver exatamente o que a sua cena está assinando enquanto você desenvolve.

Perguntas frequentes

Qual biblioteca MQTT devo usar no Unity?

O MQTTnet 4.x é uma escolha sólida porque é amplamente usado, suporta MQTT 5 e TLS e traz um build .NET Standard compatível com o Unity. Evite bibliotecas que bloqueiam a thread principal ou dependem de APIs que o Unity não suporta.

Por que meu cliente MQTT Unity lança "can only be called from the main thread"?

Você está mexendo em objetos do Unity dentro do handler de mensagens. Guarde o payload em uma coleção thread-safe e aplique-o no Update(), como mostrado neste tutorial.

Um build WebGL do Unity consegue se conectar ao MQTT?

Não com sockets TCP. Um build WebGL precisa usar MQTT via WebSocket seguro por meio de um plugin JavaScript, conectando-se a um endpoint WSS como a porta 8084 com o caminho /mqtt.

Quantas mensagens por segundo uma cena Unity aguenta?

O limite geralmente é o processamento por mensagem, não o MQTT. Com o padrão de valor mais recente por topic, a cena faz um trabalho constante por frame independentemente da taxa de mensagens, então o plano do broker e a rede passam a ser os limites práticos.

Artigos relacionados

Melhores Ferramentas CLI MQTT para Testes, Scripts e Depuração

Uma comparação prática entre mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, a CLI do MQTT.js, curl e mqttui, com o mesmo teste rodando no broker.connectmq.com, para você escolher a ferramenta certa para scripts e depuração.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.