diff --git a/Runtime/Patterns/EventBus.meta b/Runtime/Patterns/EventBus.meta new file mode 100644 index 0000000..71c1c89 --- /dev/null +++ b/Runtime/Patterns/EventBus.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 33d8b0482db19412b86d3dc2cf9669b5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Patterns/EventBus/EventBus.cs b/Runtime/Patterns/EventBus/EventBus.cs new file mode 100644 index 0000000..d19067f --- /dev/null +++ b/Runtime/Patterns/EventBus/EventBus.cs @@ -0,0 +1,34 @@ +using System; + +namespace StansAssets.Foundation.Patterns +{ + /// + /// Basic implementation of the . + /// + public sealed class EventBus : IEventBus + { + /// + /// IEventBus.Subscribe + /// + public void Subscribe(Action listener) where T : IEvent + { + EventBusDispatcher.Subscribe(this, listener); + } + + /// + /// IEventBus.Unsubscribe + /// + public void Unsubscribe(Action listener) where T : IEvent + { + EventBusDispatcher.Unsubscribe(this, listener); + } + + /// + /// IEventBus.Post + /// + public void Post(T @event) where T : IEvent + { + EventBusDispatcher.Dispatch(this, @event); + } + } +} diff --git a/Runtime/Patterns/EventBus/EventBus.cs.meta b/Runtime/Patterns/EventBus/EventBus.cs.meta new file mode 100644 index 0000000..f75b5cb --- /dev/null +++ b/Runtime/Patterns/EventBus/EventBus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60ef0718f9c13442aa226404620adc3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Patterns/EventBus/EventBusDispatcher.cs b/Runtime/Patterns/EventBus/EventBusDispatcher.cs new file mode 100644 index 0000000..d337b5b --- /dev/null +++ b/Runtime/Patterns/EventBus/EventBusDispatcher.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace StansAssets.Foundation.Patterns +{ + static class EventBusDispatcher where T : IEvent + { + static readonly Dictionary> s_Actions = new Dictionary>(); + + public static void Subscribe(EventBus bus, Action listener) + { + if (!s_Actions.ContainsKey(bus)) + { + s_Actions.Add(bus, delegate { }); + } + + s_Actions[bus] += listener; + } + + public static void Unsubscribe(EventBus bus, Action listener) + { + if (s_Actions.ContainsKey(bus)) + { + s_Actions[bus] -= listener; + } + } + + public static void Dispatch(EventBus bus, T @event) + { + if (s_Actions.TryGetValue(bus, out var action)) + { + action.Invoke(@event); + } + } + } +} diff --git a/Runtime/Patterns/EventBus/EventBusDispatcher.cs.meta b/Runtime/Patterns/EventBus/EventBusDispatcher.cs.meta new file mode 100644 index 0000000..8d59c2f --- /dev/null +++ b/Runtime/Patterns/EventBus/EventBusDispatcher.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bdaf016f43334d9c8c04a4f9da859a0a +timeCreated: 1601522598 \ No newline at end of file diff --git a/Runtime/Patterns/EventBus/IEvent.cs b/Runtime/Patterns/EventBus/IEvent.cs new file mode 100644 index 0000000..14a1b52 --- /dev/null +++ b/Runtime/Patterns/EventBus/IEvent.cs @@ -0,0 +1,7 @@ +namespace StansAssets.Foundation.Patterns +{ + /// + /// Interface represents and even distributed via + /// + public interface IEvent { } +} diff --git a/Runtime/Patterns/EventBus/IEvent.cs.meta b/Runtime/Patterns/EventBus/IEvent.cs.meta new file mode 100644 index 0000000..a57b5da --- /dev/null +++ b/Runtime/Patterns/EventBus/IEvent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: be2d2da731e84f86ab84a9e560a6b4bb +timeCreated: 1601522008 \ No newline at end of file diff --git a/Runtime/Patterns/EventBus/IEventBus.cs b/Runtime/Patterns/EventBus/IEventBus.cs new file mode 100644 index 0000000..c3ce4d2 --- /dev/null +++ b/Runtime/Patterns/EventBus/IEventBus.cs @@ -0,0 +1,15 @@ +namespace StansAssets.Foundation.Patterns +{ + /// + /// An interface for the event bus pattern. + /// + public interface IEventBus : IReadOnlyEventBus + { + /// + /// Posts and event. + /// + /// An event instance to post. + /// Event Type. + void Post(T @event) where T : IEvent; + } +} diff --git a/Runtime/Patterns/EventBus/IEventBus.cs.meta b/Runtime/Patterns/EventBus/IEventBus.cs.meta new file mode 100644 index 0000000..8f56074 --- /dev/null +++ b/Runtime/Patterns/EventBus/IEventBus.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 11ea7afdb7fb4770ac4518fcc4b92416 +timeCreated: 1602421908 \ No newline at end of file diff --git a/Runtime/Patterns/EventBus/IReadOnlyEventBus.cs b/Runtime/Patterns/EventBus/IReadOnlyEventBus.cs new file mode 100644 index 0000000..086b708 --- /dev/null +++ b/Runtime/Patterns/EventBus/IReadOnlyEventBus.cs @@ -0,0 +1,25 @@ +using System; + +namespace StansAssets.Foundation.Patterns +{ + /// + /// Interface allows to subscribe and unsubscribe from event bus events. + /// But hides and ability to dispatch an event. + /// + public interface IReadOnlyEventBus + { + /// + /// Subscribes listener to a certain event type. + /// + /// Listener instance. + /// An event type to subscribe for. + void Subscribe(Action listener) where T : IEvent; + + /// + /// Unsubscribes listener to a certain event type. + /// + /// Listener instance. + /// An event type to unsubscribe for. + void Unsubscribe(Action listener) where T : IEvent; + } +} diff --git a/Runtime/Patterns/EventBus/IReadOnlyEventBus.cs.meta b/Runtime/Patterns/EventBus/IReadOnlyEventBus.cs.meta new file mode 100644 index 0000000..5d35474 --- /dev/null +++ b/Runtime/Patterns/EventBus/IReadOnlyEventBus.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 690d4da188cd473390a9252cb47f743c +timeCreated: 1601522351 \ No newline at end of file diff --git a/Runtime/Patterns/EventBus/README.md b/Runtime/Patterns/EventBus/README.md new file mode 100644 index 0000000..8aebba9 --- /dev/null +++ b/Runtime/Patterns/EventBus/README.md @@ -0,0 +1,124 @@ +## Event Bus + +This is a well-known pattern that can be very handy in-game and app development. The main reason we should use `EventBus` is loose coupling. Sometimes, you want to process specific events that are interested in multiple parts of your application, like the presentation layer, business layer, and data layer, so EventBus provides an easy solution. + +The package offers a very simple, light, and at the same time high-performant implementation of this pattern. + +### Best Practices +Like any other pattern, it's very easy to misuse it or use it for not appropriate cases. Of course, it's up to you how to use it in own application, but here are few best practices when working with event buses. + +* Consider using event bus when it’s difficult to couple the communicating components directly +* Avoid having components which are both publishers and subscribers. See `IReadOnlyEventBus` +* Avoid “events chains” (i.e. flows that involve multiple sequential events) +* Write tests to compensate for insufficient coupling and enforce inter-components integration + +### Use Examples + +#### Subscribe and Post + +First of all you need to make a new `EventBus` instance: +``` +var eventBus = new EventBus(); +``` + +Before you can post any events make sure you declare few event classes to work with. +``` +public class SampleEvent : IEvent +{ + public string Data { get; set; } +} + +public class AnotherSampleEvent : IEvent +{ + public string Data { get; set; } + public int IntData { get; set; } +} +``` + +Now you can subscribe to event: +``` +eventBus.Subscribe((e) => +{ + Debug.Log(e.Data); +}); +``` + +And post an event: +``` +var e = new SampleEvent { Data = "Hello World" }; +eventBus.Post(e); +``` + +You may also use `Unsubscribe` method when you no longer need the subscription. + +#### Static Bus +This is the simplest an fastest implementation for the event bus pattern. +Since this is static bus *DO NOT USE* it when you making a package, since it may conflict with user project. + +It only make sense to use it inside the project you maintain and own. +Here is how the same subscribe & post flow will look like when using `StaticBus`: +``` +var e = new SampleEvent { Data = "Hello World" }; +StaticBus.Subscribe((e) => +{ + Debug.Log(e.Data); +}); + + +StaticBus.Post(e); +``` + +#### Using with pool +Another interesting example wold be to look at how you can combine events with pool: + +``` + public class SamplePooledEvent : IEvent +{ + static readonly DefaultPool s_EventsPool = new DefaultPool(); + public string Data { get; private set; } + + public static SamplePooledEvent GetPooled(string data) + { + var e = s_EventsPool.Get(); + e.Data = data; + return e; + } + + public static void Release(SamplePooledEvent e) + { + s_EventsPool.Release(e); + } +} +``` + +Now here is how posting event would look like: +``` +var e = SamplePooledEvent.GetPooled("Hello World"); +StaticBus.Post(e); +SamplePooledEvent.Release(e); +``` + +We can go further and add `IDisposable` wrapper around it: +``` +public class SamplePooledEvent : IEvent +{ + static readonly DefaultPool s_EventsPool = new DefaultPool(); + public string Data { get; private set; } + + public static DefaultPool.PooledObject GetPoolable(string data, out SamplePooledEvent e) + { + e = s_EventsPool.Get(); + e.Data = data; + + var poolable = new DefaultPool.PooledObject(e, s_EventsPool); + return poolable; + } +} +``` +Now it's even easier to post an event: +``` +using (SamplePooledEvent.GetPoolable("Hello World", out var evt)) +{ + StaticBus.Post(e); +} +``` diff --git a/Runtime/Patterns/EventBus/README.md.meta b/Runtime/Patterns/EventBus/README.md.meta new file mode 100644 index 0000000..97f665b --- /dev/null +++ b/Runtime/Patterns/EventBus/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5328070ce7a0141caabc5d47e3f0c924 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Patterns/EventBus/StaticBus.cs b/Runtime/Patterns/EventBus/StaticBus.cs new file mode 100644 index 0000000..95ae47b --- /dev/null +++ b/Runtime/Patterns/EventBus/StaticBus.cs @@ -0,0 +1,44 @@ +using System; + +namespace StansAssets.Foundation.Patterns +{ + /// + /// This is the simplest an fastest implementation for the event bus pattern. + /// Since this is static bus DO NOT USE it when you making a package, + /// Since it may conflict with user project. + /// + /// It only make sense to use it inside the project you maintain and own. + /// + /// Event Type. + public static class StaticBus where T : IEvent + { + static Action s_Action = delegate { }; + + /// + /// Subscribes listener to a certain event type. + /// + /// Listener instance. + public static void Subscribe(Action listener) + { + s_Action += listener; + } + + /// + /// Unsubscribes listener to a certain event type. + /// + /// Listener instance. + public static void Unsubscribe(Action listener) + { + s_Action -= listener; + } + + /// + /// Posts and event. + /// + /// An event instance to post. + public static void Post(T @event) + { + s_Action.Invoke(@event); + } + } +} diff --git a/Runtime/Patterns/EventBus/StaticBus.cs.meta b/Runtime/Patterns/EventBus/StaticBus.cs.meta new file mode 100644 index 0000000..768a643 --- /dev/null +++ b/Runtime/Patterns/EventBus/StaticBus.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 011203038f234cb487873c312e0221ad +timeCreated: 1601522411 \ No newline at end of file diff --git a/Runtime/Patterns/Pooling/ObjectPool.cs b/Runtime/Patterns/Pooling/ObjectPool.cs index 483db32..e8e73ef 100644 --- a/Runtime/Patterns/Pooling/ObjectPool.cs +++ b/Runtime/Patterns/Pooling/ObjectPool.cs @@ -21,12 +21,17 @@ public class ObjectPool where T : class /// } /// /// - public struct PooledObject : IDisposable, IEquatable + public readonly struct PooledObject : IDisposable, IEquatable { readonly T m_ToReturn; readonly ObjectPool m_Pool; - internal PooledObject(T value, ObjectPool pool) + /// + /// Creates `IDisposable` wrapper around poolable object. + /// + /// + /// + public PooledObject(T value, ObjectPool pool) { m_ToReturn = value; m_Pool = pool; diff --git a/Tests/Editor/Patterns/EventBusTests.cs b/Tests/Editor/Patterns/EventBusTests.cs new file mode 100644 index 0000000..c907ab3 --- /dev/null +++ b/Tests/Editor/Patterns/EventBusTests.cs @@ -0,0 +1,129 @@ +using NUnit.Framework; + +namespace StansAssets.Foundation.Patterns.EditorTests +{ + public class EventBusTests + { + public class SamplePooledEvent : IEvent + { + static readonly DefaultPool s_EventsPool = new DefaultPool(); + public string Data { get; private set; } + + public static SamplePooledEvent GetPooled(string data) + { + var e = s_EventsPool.Get(); + e.Data = data; + return e; + } + + public static DefaultPool.PooledObject GetPoolable(string data, out SamplePooledEvent e) + { + e = s_EventsPool.Get(); + e.Data = data; + + var poolable = new DefaultPool.PooledObject(e, s_EventsPool); + return poolable; + } + + public static void Release(SamplePooledEvent e) + { + s_EventsPool.Release(e); + } + } + + + public class SampleEvent : IEvent + { + public string Data { get; set; } + } + + public class AnotherSampleEvent : IEvent + { + public string Data { get; set; } + public int IntData { get; set; } + } + + bool m_EventReceived; + + [SetUp] + public void Setup() + { + m_EventReceived = false; + } + + [Test] + public void PostEventTest() + { + var eventBus = new EventBus(); + + var e = new SampleEvent { Data = "Hello World" }; + var e2 = new AnotherSampleEvent { Data = "Hello World 2" }; + + eventBus.Subscribe(OnSampleEvent); + Assert.IsFalse(m_EventReceived); + + eventBus.Post(e); + Assert.IsTrue(m_EventReceived); + + m_EventReceived = false; + eventBus.Post(e2); + Assert.IsFalse(m_EventReceived); + + eventBus.Unsubscribe(OnSampleEvent); + eventBus.Post(e); + Assert.IsFalse(m_EventReceived); + } + + [Test] + public void StaticBusTest() + { + var e = new SampleEvent { Data = "Hello World" }; + var e2 = new AnotherSampleEvent { Data = "Hello World 2" }; + + StaticBus.Subscribe(OnSampleEvent); + Assert.IsFalse(m_EventReceived); + + StaticBus.Post(e); + Assert.IsTrue(m_EventReceived); + + m_EventReceived = false; + StaticBus.Post(e2); + Assert.IsFalse(m_EventReceived); + + StaticBus.Unsubscribe(OnSampleEvent); + StaticBus.Post(e); + Assert.IsFalse(m_EventReceived); + } + + [Test] + public void TestEventBusWithPoolableEvent() + { + var e = SamplePooledEvent.GetPooled("Hello World"); + StaticBus.Subscribe(OnSampleEvent); + StaticBus.Post(e); + SamplePooledEvent.Release(e); + Assert.IsTrue(m_EventReceived); + + + m_EventReceived = false; + using (SamplePooledEvent.GetPoolable("Hello World", out var evt)) + { + StaticBus.Post(e); + } + Assert.IsTrue(m_EventReceived); + } + + void OnSampleEvent(SamplePooledEvent e) + { + Assert.That(e.Data, Is.EqualTo("Hello World")); + m_EventReceived = true; + } + + + void OnSampleEvent(SampleEvent e) + { + Assert.That(e.Data, Is.EqualTo("Hello World")); + m_EventReceived = true; + } + } +} diff --git a/Tests/Editor/Patterns/EventBusTests.cs.meta b/Tests/Editor/Patterns/EventBusTests.cs.meta new file mode 100644 index 0000000..bf28158 --- /dev/null +++ b/Tests/Editor/Patterns/EventBusTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 514c379a8fed429d939926df4ad68879 +timeCreated: 1602422693 \ No newline at end of file