diff --git a/Assets/Main.unity b/Assets/Main.unity
index 0e7c822..314b3e8 100644
--- a/Assets/Main.unity
+++ b/Assets/Main.unity
@@ -682,6 +682,37 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 403055228}
m_CullTransparentMesh: 1
+--- !u!1 &424481165
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 424481166}
+ m_Layer: 0
+ m_Name: ===============
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &424481166
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 424481165}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &437810536
GameObject:
m_ObjectHideFlags: 0
@@ -981,6 +1012,21 @@ MonoBehaviour:
Vertical
15
+
+
+ MQTT
+
+ Broker
+ Host "localhost"
+
+ Broker
+ Port 1883
+
+ Topic
+ "unity/#"
+
+ Is Connected false
+
'
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 52d992bef73842844aaf4142ab3f3f5d, type: 2}
@@ -1078,6 +1124,13 @@ MonoBehaviour:
- distance
- horizontal
- vertical
+ - target: {fileID: 952249953}
+ groupLabel: MQTT
+ fields:
+ - brokerHost
+ - brokerPort
+ - topic
+ - IsConnected
--- !u!222 &587767521
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -1560,6 +1613,57 @@ Transform:
- {fileID: 961739753}
m_Father: {fileID: 520906050}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &952249952
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 952249954}
+ - component: {fileID: 952249953}
+ m_Layer: 0
+ m_Name: MQTT
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &952249953
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 952249952}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 2d9fec57d2f43b54cb83a1310af13701, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::UltraCombos.MqttBridge
+ brokerHost: localhost
+ brokerPort: 1883
+ topic: unity/#
+ verbose: 1
+ onMessage:
+ m_PersistentCalls:
+ m_Calls: []
+--- !u!4 &952249954
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 952249952}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &961739749
GameObject:
m_ObjectHideFlags: 0
@@ -1694,8 +1798,7 @@ Transform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
- m_Children:
- - {fileID: 1160234427}
+ m_Children: []
m_Father: {fileID: 924830808}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1142443635
@@ -1731,77 +1834,6 @@ Transform:
- {fileID: 1553319432}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!1 &1160234425
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 1160234427}
- - component: {fileID: 1160234426}
- - component: {fileID: 1160234428}
- m_Layer: 0
- m_Name: Main Volume
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!114 &1160234426
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1160234425}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 172515602e62fb746b5d573b38a5fe58, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- m_IsGlobal: 0
- priority: 0
- blendDistance: 0
- weight: 1
- sharedProfile: {fileID: 11400000, guid: 2f46645358cbbd04eaa1121591e09687, type: 2}
---- !u!4 &1160234427
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1160234425}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 961739753}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!65 &1160234428
-BoxCollider:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1160234425}
- m_Material: {fileID: 0}
- m_IncludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_ExcludeLayers:
- serializedVersion: 2
- m_Bits: 0
- m_LayerOverridePriority: 0
- m_IsTrigger: 0
- m_ProvidesContacts: 0
- m_Enabled: 1
- serializedVersion: 3
- m_Size: {x: 1, y: 1, z: 1}
- m_Center: {x: 0, y: 0, z: 0}
--- !u!1 &1173268649
GameObject:
m_ObjectHideFlags: 0
@@ -2018,6 +2050,55 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1300422186}
m_CullTransparentMesh: 1
+--- !u!1 &1371171025
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1371171027}
+ - component: {fileID: 1371171026}
+ m_Layer: 0
+ m_Name: Global Volume
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1371171026
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1371171025}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 172515602e62fb746b5d573b38a5fe58, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.Volume
+ m_IsGlobal: 1
+ priority: 0
+ blendDistance: 0
+ weight: 1
+ sharedProfile: {fileID: 11400000, guid: 2f46645358cbbd04eaa1121591e09687, type: 2}
+--- !u!4 &1371171027
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1371171025}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1553319431
GameObject:
m_ObjectHideFlags: 0
@@ -3761,8 +3842,11 @@ SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 476511608}
+ - {fileID: 1371171027}
+ - {fileID: 952249954}
- {fileID: 605636970}
- {fileID: 848515248}
+ - {fileID: 424481166}
- {fileID: 604712594}
- {fileID: 505702790}
- {fileID: 1142443636}
diff --git a/Assets/Scripts/MqttBridge.cs b/Assets/Scripts/MqttBridge.cs
new file mode 100644
index 0000000..db43fb8
--- /dev/null
+++ b/Assets/Scripts/MqttBridge.cs
@@ -0,0 +1,173 @@
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using MQTTnet;
+using MQTTnet.Client;
+using UnityEngine;
+using UnityEngine.Events;
+
+namespace UltraCombos
+{
+ public class MqttBridge : MonoBehaviour
+ {
+ [SerializeField] private string brokerHost = "localhost";
+ [SerializeField] private int brokerPort = 1883;
+ [SerializeField] private string topic = "unity/#";
+ [SerializeField] private bool verbose = true;
+
+ [System.Serializable]
+ public class MessageEvent : UnityEvent { }
+ public MessageEvent onMessage;
+ public bool IsConnected => mqttClient?.IsConnected ?? false;
+
+ private IMqttClient mqttClient;
+ private CancellationTokenSource cts;
+ private string clientId;
+
+ // Messages received on MQTT thread → dispatched on main thread in Update()
+ private readonly ConcurrentQueue<(string topic, string payload)> messageQueue
+ = new ConcurrentQueue<(string, string)>();
+
+ private void Awake()
+ {
+ // Cache Unity main-thread-only API before any async/thread-pool usage
+ clientId = $"unity_{SystemInfo.deviceUniqueIdentifier}";
+ }
+
+ private async void Start()
+ {
+ cts = new CancellationTokenSource();
+ // ConfigureAwait(false): continuation doesn't need to resume on Unity main thread
+ await ConnectAsync().ConfigureAwait(false);
+ }
+
+ private void Update()
+ {
+ // Drain the queue on the main thread so handlers can safely touch Unity objects
+ while (messageQueue.TryDequeue(out var msg))
+ {
+ onMessage?.Invoke(msg.topic, msg.payload);
+ HandleMessage(msg.topic, msg.payload);
+ }
+ }
+
+ private void OnDestroy()
+ {
+ // Must be synchronous: at shutdown the Unity SynchronizationContext stops processing,
+ // so async void OnDestroy would hang forever waiting for a continuation that never runs.
+ cts?.Cancel();
+ if (mqttClient?.IsConnected == true)
+ {
+ try { mqttClient.DisconnectAsync().Wait(System.TimeSpan.FromSeconds(2)); }
+ catch { }
+ }
+ mqttClient?.Dispose();
+ }
+
+ // ── Connection ────────────────────────────────────────────────────────
+
+ private async Task ConnectAsync()
+ {
+ if (mqttClient != null)
+ {
+ mqttClient.ApplicationMessageReceivedAsync -= OnApplicationMessageReceived;
+ mqttClient.ConnectedAsync -= OnConnected;
+ mqttClient.DisconnectedAsync -= OnDisconnected;
+ mqttClient.Dispose();
+ mqttClient = null;
+ }
+
+ var factory = new MqttFactory();
+ mqttClient = factory.CreateMqttClient();
+
+ var options = new MqttClientOptionsBuilder()
+ .WithTcpServer(brokerHost, brokerPort)
+ .WithClientId(clientId)
+ .WithCleanSession()
+ .Build();
+
+ mqttClient.ApplicationMessageReceivedAsync += OnApplicationMessageReceived;
+ mqttClient.ConnectedAsync += OnConnected;
+ mqttClient.DisconnectedAsync += OnDisconnected;
+
+ try
+ {
+ using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
+ connectCts.CancelAfter(System.TimeSpan.FromSeconds(5));
+ await mqttClient.ConnectAsync(options, connectCts.Token).ConfigureAwait(false);
+ }
+ catch (System.Exception e)
+ {
+ Debug.LogError($"[MQTT] Connect failed: {e.Message}");
+ }
+ }
+
+ private async Task OnConnected(MqttClientConnectedEventArgs e)
+ {
+ Debug.Log("[MQTT] Connected.");
+ await SubscribeAsync(topic).ConfigureAwait(false);
+ }
+
+ private async Task OnDisconnected(MqttClientDisconnectedEventArgs e)
+ {
+ Debug.LogWarning($"[MQTT] Disconnected. Reconnecting in 3s…");
+ if (cts.IsCancellationRequested) return;
+
+ try
+ {
+ await Task.Delay(System.TimeSpan.FromSeconds(3), cts.Token)
+ .ConfigureAwait(false);
+ await ConnectAsync().ConfigureAwait(false);
+ }
+ catch (TaskCanceledException) { }
+ }
+
+ // ── Subscribe / Publish ───────────────────────────────────────────────
+
+ private async Task SubscribeAsync(string topicFilter)
+ {
+ var filter = new MqttTopicFilterBuilder()
+ .WithTopic(topicFilter)
+ .Build();
+
+ await mqttClient.SubscribeAsync(filter, cts.Token).ConfigureAwait(false);
+ Debug.Log($"[MQTT] Subscribed to: {topicFilter}");
+ }
+
+ public async Task PublishAsync(string publishTopic, string payload)
+ {
+ if (!IsConnected)
+ {
+ Debug.LogWarning("[MQTT] Publish skipped — not connected.");
+ return;
+ }
+
+ var message = new MqttApplicationMessageBuilder()
+ .WithTopic(publishTopic)
+ .WithPayload(payload)
+ .Build();
+
+ await mqttClient.PublishAsync(message, cts.Token).ConfigureAwait(false);
+ }
+
+ // ── Receive ───────────────────────────────────────────────────────────
+
+ private Task OnApplicationMessageReceived(MqttApplicationMessageReceivedEventArgs e)
+ {
+ var incomingTopic = e.ApplicationMessage.Topic;
+ var payload = e.ApplicationMessage.ConvertPayloadToString();
+ // Enqueue so it's processed on the main thread in Update()
+ messageQueue.Enqueue((incomingTopic, payload));
+ return Task.CompletedTask;
+ }
+
+ // Override this in a subclass, or subscribe to OnMessage instead
+ protected virtual void HandleMessage(string incomingTopic, string payload)
+ {
+ if (verbose)
+ {
+ Debug.Log($"[MQTT] {incomingTopic}: {payload}");
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/MqttBridge.cs.meta b/Assets/Scripts/MqttBridge.cs.meta
new file mode 100644
index 0000000..295f71c
--- /dev/null
+++ b/Assets/Scripts/MqttBridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2d9fec57d2f43b54cb83a1310af13701
\ No newline at end of file