Skip to content

Commit 7a4433c

Browse files
committed
Add core implementation of ui thread dispatcher
1 parent 3aa4af6 commit 7a4433c

File tree

10 files changed

+387
-0
lines changed

10 files changed

+387
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using UnityEngine;
4+
5+
namespace ThreadDispatcher.Core
6+
{
7+
/// <summary>
8+
/// Handles thread-safe action queue processing.
9+
/// </summary>
10+
public class ActionQueueProcessor : IDisposable
11+
{
12+
private readonly Queue<Action> _queue = new Queue<Action>();
13+
private readonly object _lock = new object();
14+
private readonly ILogger _logger;
15+
16+
public ActionQueueProcessor(ILogger logger = null)
17+
{
18+
_logger = logger ?? new UnityLogger();
19+
}
20+
21+
public int Count
22+
{
23+
get
24+
{
25+
lock (_lock)
26+
{
27+
return _queue.Count;
28+
}
29+
}
30+
}
31+
32+
public void Enqueue(Action action)
33+
{
34+
if (action == null)
35+
{
36+
_logger.LogWarning("Attempted to enqueue null action");
37+
return;
38+
}
39+
40+
lock (_lock)
41+
{
42+
_queue.Enqueue(action);
43+
}
44+
}
45+
46+
public void ProcessAll()
47+
{
48+
lock (_lock)
49+
{
50+
while (_queue.Count > 0)
51+
{
52+
var action = _queue.Dequeue();
53+
try
54+
{
55+
action?.Invoke();
56+
}
57+
catch (Exception ex)
58+
{
59+
_logger.LogError($"Error executing queued action: {ex}");
60+
}
61+
}
62+
}
63+
}
64+
65+
public void Clear()
66+
{
67+
lock (_lock)
68+
{
69+
_queue.Clear();
70+
}
71+
}
72+
73+
public void Dispose()
74+
{
75+
Clear();
76+
}
77+
}
78+
}

Runtime/Core/CoroutineRunner.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using System.Collections;
3+
using UnityEngine;
4+
5+
namespace ThreadDispatcher.Core
6+
{
7+
/// <summary>
8+
/// Handles coroutine-based delayed actions.
9+
/// </summary>
10+
public class CoroutineRunner : MonoBehaviour
11+
{
12+
public void RunDelayed(Action action, float delay)
13+
{
14+
if (action != null)
15+
{
16+
StartCoroutine(DelayedAction(action, delay));
17+
}
18+
}
19+
20+
public void RunEndOfFrame(Action action)
21+
{
22+
if (action != null)
23+
{
24+
StartCoroutine(EndOfFrameAction(action));
25+
}
26+
}
27+
28+
public void RunNextFrame(Action action)
29+
{
30+
if (action != null)
31+
{
32+
StartCoroutine(NextFrameAction(action));
33+
}
34+
}
35+
36+
private IEnumerator DelayedAction(Action action, float delay)
37+
{
38+
yield return new WaitForSeconds(delay);
39+
action?.Invoke();
40+
}
41+
42+
private IEnumerator EndOfFrameAction(Action action)
43+
{
44+
yield return new WaitForEndOfFrame();
45+
action?.Invoke();
46+
}
47+
48+
private IEnumerator NextFrameAction(Action action)
49+
{
50+
yield return null;
51+
action?.Invoke();
52+
}
53+
}
54+
}

Runtime/Core/ThreadChecker.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Threading;
2+
3+
namespace ThreadDispatcher.Core
4+
{
5+
/// <summary>
6+
/// Utility for checking thread identity.
7+
/// </summary>
8+
public class ThreadChecker
9+
{
10+
private readonly int _mainThreadId;
11+
12+
public ThreadChecker()
13+
{
14+
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
15+
}
16+
17+
public bool IsMainThread => Thread.CurrentThread.ManagedThreadId == _mainThreadId;
18+
}
19+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace ThreadDispatcher.Extensions
5+
{
6+
/// <summary>
7+
/// Extension methods for async/await integration.
8+
/// </summary>
9+
public static class UIThreadExtensions
10+
{
11+
public static UIThreadAwaiter SwitchToMainThread()
12+
{
13+
return new UIThreadAwaiter();
14+
}
15+
}
16+
17+
/// <summary>
18+
/// Custom awaiter for async/await pattern.
19+
/// </summary>
20+
public struct UIThreadAwaiter : INotifyCompletion
21+
{
22+
public bool IsCompleted => UIThreadDispatcher.Instance.IsMainThread;
23+
24+
public void OnCompleted(Action continuation)
25+
{
26+
UIThreadDispatcher.Instance.Enqueue(continuation);
27+
}
28+
29+
public void GetResult() { }
30+
31+
public UIThreadAwaiter GetAwaiter() => this;
32+
}
33+
}

Runtime/Interfaces/ILogger.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
namespace ThreadDispatcher
3+
{
4+
/// <summary>
5+
/// Logging interface for dependency injection.
6+
/// </summary>
7+
public interface ILogger
8+
{
9+
void Log(string message);
10+
void LogWarning(string message);
11+
void LogError(string message);
12+
}
13+
}

Runtime/Interfaces/IThreadDispatcher.cs

Whitespace-only changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "ThreadDispatcher.Runtime",
3+
"rootNamespace": "ThreadDispatcher",
4+
"references": [],
5+
"includePlatforms": [],
6+
"excludePlatforms": [],
7+
"allowUnsafeCode": false,
8+
"overrideReferences": false,
9+
"precompiledReferences": [],
10+
"autoReferenced": true,
11+
"defineConstraints": [],
12+
"versionDefines": [],
13+
"noEngineReferences": false
14+
}

Runtime/UIThreadDispatcher.cs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System;
2+
using UnityEngine;
3+
using ThreadDispatcher.Core;
4+
5+
namespace ThreadDispatcher
6+
{
7+
/// <summary>
8+
/// Main dispatcher for marshalling calls to Unity's main thread.
9+
/// </summary>
10+
public class UIThreadDispatcher : MonoBehaviour, IThreadDispatcher
11+
{
12+
private static UIThreadDispatcher _instance;
13+
private static readonly object _lock = new object();
14+
15+
private ActionQueueProcessor _queueProcessor;
16+
private ThreadChecker _threadChecker;
17+
private CoroutineRunner _coroutineRunner;
18+
private ILogger _logger;
19+
20+
public static UIThreadDispatcher Instance
21+
{
22+
get
23+
{
24+
if (_instance == null)
25+
{
26+
lock (_lock)
27+
{
28+
if (_instance == null)
29+
{
30+
Initialize();
31+
}
32+
}
33+
}
34+
return _instance;
35+
}
36+
}
37+
38+
public static IThreadDispatcher Dispatcher => Instance;
39+
40+
public bool IsMainThread => _threadChecker?.IsMainThread ?? true;
41+
public int QueueSize => _queueProcessor?.Count ?? 0;
42+
43+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
44+
private static void Initialize()
45+
{
46+
if (_instance != null) return;
47+
48+
var go = new GameObject("[UIThreadDispatcher]");
49+
_instance = go.AddComponent<UIThreadDispatcher>();
50+
DontDestroyOnLoad(go);
51+
}
52+
53+
void Awake()
54+
{
55+
if (_instance != null && _instance != this)
56+
{
57+
Destroy(gameObject);
58+
return;
59+
}
60+
61+
InitializeComponents();
62+
}
63+
64+
private void InitializeComponents()
65+
{
66+
_logger = new UnityLogger();
67+
_threadChecker = new ThreadChecker();
68+
_queueProcessor = new ActionQueueProcessor(_logger);
69+
_coroutineRunner = gameObject.AddComponent<CoroutineRunner>();
70+
}
71+
72+
void Update()
73+
{
74+
_queueProcessor?.ProcessAll();
75+
}
76+
77+
public void Enqueue(Action action)
78+
{
79+
_queueProcessor?.Enqueue(action);
80+
}
81+
82+
public void RunOnMainThread(Action action)
83+
{
84+
if (action == null)
85+
{
86+
_logger?.LogWarning("Attempted to run null action");
87+
return;
88+
}
89+
90+
if (IsMainThread)
91+
{
92+
action();
93+
}
94+
else
95+
{
96+
Enqueue(action);
97+
}
98+
}
99+
100+
public void EnqueueDelayed(Action action, float delay)
101+
{
102+
_coroutineRunner?.RunDelayed(action, delay);
103+
}
104+
105+
public void EnqueueEndOfFrame(Action action)
106+
{
107+
_coroutineRunner?.RunEndOfFrame(action);
108+
}
109+
110+
public void EnqueueNextFrame(Action action)
111+
{
112+
_coroutineRunner?.RunNextFrame(action);
113+
}
114+
115+
void OnDestroy()
116+
{
117+
if (_instance == this)
118+
{
119+
_queueProcessor?.Dispose();
120+
_instance = null;
121+
}
122+
}
123+
124+
// Static convenience methods
125+
public static void EnqueueAction(Action action) => Instance.Enqueue(action);
126+
public static void RunOnMain(Action action) => Instance.RunOnMainThread(action);
127+
public static void EnqueueWithDelay(Action action, float delay) => Instance.EnqueueDelayed(action, delay);
128+
public static void EnqueueAtEndOfFrame(Action action) => Instance.EnqueueEndOfFrame(action);
129+
public static void EnqueueForNextFrame(Action action) => Instance.EnqueueNextFrame(action);
130+
}
131+
}

Runtime/UnityLogger.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace ThreadDispatcher
4+
{
5+
/// <summary>
6+
/// Default Unity logger implementation.
7+
/// </summary>
8+
public class UnityLogger : ILogger
9+
{
10+
public void Log(string message) => UnityEngine.Debug.Log(message);
11+
public void LogWarning(string message) => UnityEngine.Debug.LogWarning(message);
12+
public void LogError(string message) => UnityEngine.Debug.LogError(message);
13+
}
14+
}

0 commit comments

Comments
 (0)