From fc8504544dc1a48ec0228b469efe9e3304f086c0 Mon Sep 17 00:00:00 2001 From: mewlist Date: Thu, 22 Feb 2024 00:30:53 +0900 Subject: [PATCH 1/3] Tickable --- Runtime/Attribute/TickableAttribute.cs | 27 +++++++ Runtime/Attribute/TickableAttribute.cs.meta | 3 + Runtime/DIContainer.cs | 6 +- Runtime/Internals/Ticker.cs | 79 +++++++++++++++++++++ Runtime/Internals/Ticker.cs.meta | 3 + Runtime/TargetMethodsInfo.cs | 16 +++-- Tests/TestObjects.cs | 42 +++++++++++ Tests/TickableTest.cs | 59 +++++++++++++++ Tests/TickableTest.cs.meta | 3 + 9 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 Runtime/Attribute/TickableAttribute.cs create mode 100644 Runtime/Attribute/TickableAttribute.cs.meta create mode 100644 Runtime/Internals/Ticker.cs create mode 100644 Runtime/Internals/Ticker.cs.meta create mode 100644 Tests/TickableTest.cs create mode 100644 Tests/TickableTest.cs.meta diff --git a/Runtime/Attribute/TickableAttribute.cs b/Runtime/Attribute/TickableAttribute.cs new file mode 100644 index 0000000..c22fc36 --- /dev/null +++ b/Runtime/Attribute/TickableAttribute.cs @@ -0,0 +1,27 @@ +using System; +using Mew.Core; +using UnityEngine.PlayerLoop; + +namespace Doinject +{ + public enum TickableTiming + { + EarlyUpdate, + FixedUpdate, + PreUpdate, + Update, + PreLateUpdate, + PostLateUpdate, + } + + [AttributeUsage(AttributeTargets.Method)] + public class TickableAttribute : Attribute + { + public TickableTiming Timing { get; set; } + + public TickableAttribute(TickableTiming timing = TickableTiming.Update) + { + Timing = timing; + } + } +} \ No newline at end of file diff --git a/Runtime/Attribute/TickableAttribute.cs.meta b/Runtime/Attribute/TickableAttribute.cs.meta new file mode 100644 index 0000000..2f8f69a --- /dev/null +++ b/Runtime/Attribute/TickableAttribute.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b729d5a93ded4905b229a03e384aab21 +timeCreated: 1708522829 \ No newline at end of file diff --git a/Runtime/DIContainer.cs b/Runtime/DIContainer.cs index 17c1158..77e1bd2 100644 --- a/Runtime/DIContainer.cs +++ b/Runtime/DIContainer.cs @@ -37,6 +37,7 @@ public class DIContainer : IReadOnlyDIContainer, IAsyncDisposable private MethodInjector MethodInjector { get; } private PropertyInjector PropertyInjector { get; } private FieldInjector FieldInjector { get; } + private Ticker Ticker { get; } public IReadOnlyDictionary ReadOnlyBindings => Resolvers; internal IReadOnlyDictionary ReadOnlyInstanceMap => ResolvedInstanceBag.ReadOnlyInstanceMap; @@ -54,6 +55,7 @@ public DIContainer(IReadOnlyDIContainer parent = null, Scene scene = default) MethodInjector = new MethodInjector(this); PropertyInjector = new PropertyInjector(this); FieldInjector = new FieldInjector(this); + Ticker = new Ticker(); BindFromInstance(this); } @@ -334,6 +336,8 @@ private async ValueTask InvokeOnInjectedCallback(object target, TargetMethodsInf await TaskHelper.NextFrame(); foreach (var methodInfo in methods.OnInjectedMethods) await InvokeCallback(target, methodInfo, InjectionProcessingScope); + + Ticker.Add(target, methods.TickableMethods); } private async ValueTask InvokeCallback(T target, MethodInfo callback, ParallelScope completionSource) @@ -461,7 +465,7 @@ public async ValueTask DisposeAsync() if (CancellationTokenSource.IsCancellationRequested) return; CancellationTokenSource.Cancel(); CancellationTokenSource.Dispose(); - + Ticker.Dispose(); await Task.WhenAll(Resolvers.Select(x => x.Value.DisposeAsync().AsTask())); Resolvers.Clear(); await ResolvedInstanceBag.DisposeAsync(); diff --git a/Runtime/Internals/Ticker.cs b/Runtime/Internals/Ticker.cs new file mode 100644 index 0000000..d8f7865 --- /dev/null +++ b/Runtime/Internals/Ticker.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Mew.Core; +using UnityEngine; + +namespace Doinject +{ + public class TickableMethod + { + public Action Invoke { get; } + + public TickableMethod(object target, MethodBase methodInfo) + { + if (target is MonoBehaviour monoBehaviour) + Invoke = () => + { + if (monoBehaviour) + methodInfo.Invoke(monoBehaviour, null); + }; + else + Invoke = () => methodInfo.Invoke(target, null); + } + + } + + internal class Ticker : IDisposable + { + private readonly Dictionary> invokers = new(); + + public Ticker() + { + MewLoop.Add(InvokeEarlyUpdate); + MewLoop.Add(InvokeFixedUpdate); + MewLoop.Add(InvokePreUpdate); + MewLoop.Add(InvokeUpdate); + MewLoop.Add(InvokePreLateUpdate); + MewLoop.Add(InvokePostLateUpdate); + } + + public void Dispose() + { + MewLoop.Remove(InvokeEarlyUpdate); + MewLoop.Remove(InvokeFixedUpdate); + MewLoop.Remove(InvokePreUpdate); + MewLoop.Remove(InvokeUpdate); + MewLoop.Remove(InvokePreLateUpdate); + MewLoop.Remove(InvokePostLateUpdate); + invokers.Clear(); + } + + private void InvokeEarlyUpdate() => Invoke(TickableTiming.EarlyUpdate); + private void InvokeFixedUpdate() => Invoke(TickableTiming.FixedUpdate); + private void InvokePreUpdate() => Invoke(TickableTiming.PreUpdate); + private void InvokeUpdate() => Invoke(TickableTiming.Update); + private void InvokePreLateUpdate() => Invoke(TickableTiming.PreLateUpdate); + private void InvokePostLateUpdate() => Invoke(TickableTiming.PostLateUpdate); + + public void Add(object target, Dictionary> methodsTickableMethods) + { + foreach (var (timing, methods) in methodsTickableMethods) + foreach (var methodInfo in methods) + { + if (!invokers.ContainsKey(timing)) + invokers[timing] = new List(); + invokers[timing].Add(new TickableMethod(target, methodInfo)); + } + } + + private void Invoke(TickableTiming timing) + { + if (!invokers.TryGetValue(timing, out var methods)) return; + + foreach (var method in methods) + method.Invoke(); + } + } +} \ No newline at end of file diff --git a/Runtime/Internals/Ticker.cs.meta b/Runtime/Internals/Ticker.cs.meta new file mode 100644 index 0000000..870d285 --- /dev/null +++ b/Runtime/Internals/Ticker.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5a375ee9114840b28d46eb0df2eeefb5 +timeCreated: 1708523636 \ No newline at end of file diff --git a/Runtime/TargetMethodsInfo.cs b/Runtime/TargetMethodsInfo.cs index da8218e..c455b3a 100644 --- a/Runtime/TargetMethodsInfo.cs +++ b/Runtime/TargetMethodsInfo.cs @@ -7,6 +7,7 @@ namespace Doinject internal class TargetMethodsInfo { public List InjectMethods { get; } = new(); + public Dictionary> TickableMethods { get; } = new(); public List PostInjectMethods { get; } = new(); public List OnInjectedMethods { get; } = new(); @@ -20,8 +21,7 @@ public TargetMethodsInfo(Type targetType) throw new Exception($"Inject method must be public. {targetType.Name}.{methodInfo.Name}()"); InjectMethods.Add(methodInfo); } - - if (methodInfo.GetCustomAttributes(typeof(PostInjectAttribute), true).Length > 0) + else if (methodInfo.GetCustomAttributes(typeof(PostInjectAttribute), true).Length > 0) { if (!methodInfo.IsPublic) throw new Exception($"PostInject method must be public. {targetType.Name}.{methodInfo.Name}()"); @@ -29,8 +29,7 @@ public TargetMethodsInfo(Type targetType) throw new Exception("PostInject method should not have any parameters"); PostInjectMethods.Add(methodInfo); } - - if (methodInfo.GetCustomAttributes(typeof(OnInjectedAttribute), true).Length > 0) + else if (methodInfo.GetCustomAttributes(typeof(OnInjectedAttribute), true).Length > 0) { if (!methodInfo.IsPublic) throw new Exception($"OnInjected method must be public. {targetType.Name}.{methodInfo.Name}()"); @@ -38,6 +37,15 @@ public TargetMethodsInfo(Type targetType) throw new Exception("OnInjected method should not have any parameters"); OnInjectedMethods.Add(methodInfo); } + else if (methodInfo.GetCustomAttributes(typeof(TickableAttribute), true).Length > 0) + { + var tickableAttr = methodInfo.GetCustomAttribute(typeof(TickableAttribute), true) as TickableAttribute; + if (!methodInfo.IsPublic) + throw new Exception($"Tickable method must be public. {targetType.Name}.{methodInfo.Name}()"); + if (!TickableMethods.TryGetValue(tickableAttr.Timing, out var tickableMethods)) + TickableMethods[tickableAttr.Timing] = new List(); + TickableMethods[tickableAttr.Timing].Add(methodInfo); + } } } } diff --git a/Tests/TestObjects.cs b/Tests/TestObjects.cs index 6912d96..ff6ee87 100644 --- a/Tests/TestObjects.cs +++ b/Tests/TestObjects.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using UnityEngine; namespace Doinject.Tests { @@ -99,6 +100,47 @@ public class FieldInjectionObject [Inject] public InjectedObject injectedObject; } + public class TickableObject + { + public int EarlyUpdateCount { get; private set; } + public int FixedUpdateCount { get; private set; } + public int PreUpdateCount { get; private set; } + public int UpdateCount { get; private set; } + public int PreLateUpdateCount { get; private set; } + public int PostLateUpdateCount { get; private set; } + public bool CountEnabled { get; set; } + + [Tickable(TickableTiming.EarlyUpdate)] public void EarlyUpdate() + { + if (CountEnabled) EarlyUpdateCount++; + } + + [Tickable(TickableTiming.FixedUpdate)] public void FixedUpdate() + { + if (CountEnabled) FixedUpdateCount++; + } + + [Tickable(TickableTiming.PreUpdate)] public void PreUpdate() + { + if (CountEnabled) PreUpdateCount++; + } + + [Tickable(TickableTiming.Update)] public void Update() + { + if (CountEnabled) UpdateCount++; + } + + [Tickable(TickableTiming.PreLateUpdate)] public void PreLateUpdate() + { + if (CountEnabled) PreLateUpdateCount++; + } + + [Tickable(TickableTiming.PostLateUpdate)] public void PostLateUpdate() + { + if (CountEnabled) PostLateUpdateCount++; + } + } + public class FieldInjectionWithNonPublicObject { [Inject] private InjectedObject injectedObject; diff --git a/Tests/TickableTest.cs b/Tests/TickableTest.cs new file mode 100644 index 0000000..a5f50f7 --- /dev/null +++ b/Tests/TickableTest.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Mew.Core.TaskHelpers; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Doinject.Tests +{ + public class TickableTest + { + private DIContainer container; + + public int TargetFrameRate { get; set; } + + [SetUp] + public void Setup() + { + TargetFrameRate = Application.targetFrameRate; + Application.targetFrameRate = 10; + var scene = SceneManager.GetActiveScene(); + container = new DIContainer(parent: null, scene); + } + + + [TearDown] + public async Task TearDown() + { + await container.DisposeAsync(); + Application.targetFrameRate = TargetFrameRate; + } + + [Test] + public async Task UpdateTimingTest() + { + var tickable = new TickableObject(); + container.BindFromInstance(tickable); + var instance = await container.ResolveAsync(); + await TaskHelperInternal.NextFrame(); + instance.CountEnabled = true; + for (var i = 0; i < 10; i++) + await TaskHelperInternal.NextFrame(); + await TaskHelperInternal.NextFrame(); + await TaskHelperInternal.NextFrame(); + instance.CountEnabled = false; + Assert.That(instance.EarlyUpdateCount, Is.GreaterThan(8)); + Assert.That(instance.FixedUpdateCount, Is.GreaterThan(8)); + Assert.That(instance.PreUpdateCount, Is.GreaterThan(8)); + Assert.That(instance.UpdateCount, Is.GreaterThan(8)); + Assert.That(instance.PreLateUpdateCount, Is.GreaterThan(8)); + Assert.That(instance.PostLateUpdateCount, Is.GreaterThan(8)); + + Assert.That(instance.UpdateCount, Is.EqualTo(instance.EarlyUpdateCount + 1)); + Assert.That(instance.UpdateCount, Is.EqualTo(instance.PreUpdateCount + 1)); + Assert.That(instance.UpdateCount, Is.EqualTo(instance.PreLateUpdateCount + 1)); + Assert.That(instance.UpdateCount, Is.EqualTo(instance.PostLateUpdateCount + 1)); + Assert.That(instance.FixedUpdateCount, Is.GreaterThan(instance.UpdateCount)); + } + } +} \ No newline at end of file diff --git a/Tests/TickableTest.cs.meta b/Tests/TickableTest.cs.meta new file mode 100644 index 0000000..baec223 --- /dev/null +++ b/Tests/TickableTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 51134e4e650044979b1d803c5de89d07 +timeCreated: 1708523038 \ No newline at end of file From 969870ee67f010dd6d018160d593212ef5154e39 Mon Sep 17 00:00:00 2001 From: mewlist Date: Thu, 22 Feb 2024 23:03:48 +0900 Subject: [PATCH 2/3] Add tickable document --- Writerside~/di.tree | 1 + Writerside~/di_en.tree | 1 + Writerside~/topics/tickable.md | 47 +++++++++++++++++++++++++++++++ Writerside~/topics/tickable_en.md | 45 +++++++++++++++++++++++++++++ Writerside~/topics/tutorial.md | 22 +++++++++++---- Writerside~/topics/tutorial_en.md | 22 +++++++++++---- 6 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 Writerside~/topics/tickable.md create mode 100644 Writerside~/topics/tickable_en.md diff --git a/Writerside~/di.tree b/Writerside~/di.tree index 17a5689..bb97f60 100644 --- a/Writerside~/di.tree +++ b/Writerside~/di.tree @@ -30,6 +30,7 @@ + \ No newline at end of file diff --git a/Writerside~/di_en.tree b/Writerside~/di_en.tree index 04d3efa..76b0838 100644 --- a/Writerside~/di_en.tree +++ b/Writerside~/di_en.tree @@ -30,6 +30,7 @@ + \ No newline at end of file diff --git a/Writerside~/topics/tickable.md b/Writerside~/topics/tickable.md new file mode 100644 index 0000000..2fa3c98 --- /dev/null +++ b/Writerside~/topics/tickable.md @@ -0,0 +1,47 @@ +# 定期的なコールバック + +Unity のプレイヤーループタイミングに従って、定期的に呼び出されるコールバックを登録できます。 +DI コンテナにより管理されるインスタンスや、注入対象となるインスタンスに対して機能します。 +また、対象のクラスへの注入が完了した以降に初回の呼び出しが行われることが保証されます。 + +## コールバックの登録 + +任意の public なメソッドに ```[Tickable]``` 属性をつけることで、コールバックを登録できます。 +タイミングを指定しなかった場合は、```Update``` タイミングでコールバックされます。 + +```C# +public class SomeClass +{ + // Update タイミングで呼び出される + [Tickable] + public void Tick() + { + ... + } +} +``` + +## コールバックタイミングの指定 + +```Tickable``` の引数にコールバックのタイミングを指定できます。 + +```C# +public class SomeClass +{ + // FixedUpdate タイミングで呼び出される + [Tickable(TickableTiming.FixedUpdate)] + public void Tick() + { + ... + } +} +``` + +コールバックには以下のタイミングを指定できます。 + +* TickableTiming.EarlyUpdate +* TickableTiming.FixedUpdate +* TickableTiming.PreUpdate +* TickableTiming.Update +* TickableTiming.PreLateUpdate +* TickableTiming.PostLateUpdate diff --git a/Writerside~/topics/tickable_en.md b/Writerside~/topics/tickable_en.md new file mode 100644 index 0000000..0652385 --- /dev/null +++ b/Writerside~/topics/tickable_en.md @@ -0,0 +1,45 @@ +# Regular Callbacks + +You can register callbacks that are called regularly according to the timing of Unity's player loop. +It works for instances managed by the DI container and instances to be injected. It is also guaranteed that the first call will be made after the injection to the target class has been completed. + +## Registering Callbacks + +By adding the ```[Tickable]``` attribute to any public method, you can register a callback. If you do not specify the timing, it will be called back at the ```Update``` timing. + +```C# +public class SomeClass +{ + // Called at Update timing + [Tickable] + public void Tick() + { + ... + } +} +``` + +## Specifying Callback Timing + +You can specify the timing of the callback in the arguments of ```Tickable```. + +```C# +public class SomeClass +{ + // Called at FixedUpdate timing + [Tickable(TickableTiming.FixedUpdate)] + public void Tick() + { + ... + } +} +``` + +You can specify the following timings for the callback. + +* TickableTiming.EarlyUpdate +* TickableTiming.FixedUpdate +* TickableTiming.PreUpdate +* TickableTiming.Update +* TickableTiming.PreLateUpdate +* TickableTiming.PostLateUpdate \ No newline at end of file diff --git a/Writerside~/topics/tutorial.md b/Writerside~/topics/tutorial.md index b92e596..89e318f 100644 --- a/Writerside~/topics/tutorial.md +++ b/Writerside~/topics/tutorial.md @@ -11,9 +11,14 @@ Unity Package Manager からインストールすることができます。 Unity のメニューから Window > Package Manager を選択します。 + ボタンをクリックし、Add package from git URL... を選択します。 以下を入力し、Add をクリックします。 - -git@github.com:mewlist/MewCore.git - + + +https://github.com/mewlist/MewCore.git + + +git@github.com:mewlist/MewCore.git + + @@ -24,9 +29,14 @@ git@github.com:mewlist/MewCore.git Unity のメニューから Window > Package Manager を選択します。 + ボタンをクリックし、Add package from git URL... を選択します。 以下を入力し、Add をクリックします。 - -git@github.com:mewlist/Doinject.git - + + +https://github.com/mewlist/Doinject.git + + +git@github.com:mewlist/Doinject.git + + diff --git a/Writerside~/topics/tutorial_en.md b/Writerside~/topics/tutorial_en.md index c3b97de..14c06bd 100644 --- a/Writerside~/topics/tutorial_en.md +++ b/Writerside~/topics/tutorial_en.md @@ -11,9 +11,14 @@ You can install it from the Unity Package Manager. Select Window > Package Manager from the Unity menu. Click the + button and select Add package from git URL.... Enter the following and click Add. - -git@github.com:mewlist/MewCore.git - + + +https://github.com/mewlist/MewCore.git + + +git@github.com:mewlist/MewCore.git + + @@ -24,9 +29,14 @@ git@github.com:mewlist/MewCore.git Select Window > Package Manager from the Unity menu. Click the + button and select Add package from git URL.... Enter the following and click Add. - -git@github.com:mewlist/Doinject.git - + + +https://github.com/mewlist/Doinject.git + + +git@github.com:mewlist/Doinject.git + + From c081ca08db20993bf0a14525b4a81f745b2b486d Mon Sep 17 00:00:00 2001 From: mewlist Date: Thu, 22 Feb 2024 23:12:49 +0900 Subject: [PATCH 3/3] Tickable method can not have arguments --- Runtime/Attribute/TickableAttribute.cs | 2 -- Runtime/TargetMethodsInfo.cs | 2 ++ Tests/TestObjects.cs | 8 ++++++++ Tests/TickableTest.cs | 24 ++++++++++++++++++------ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Runtime/Attribute/TickableAttribute.cs b/Runtime/Attribute/TickableAttribute.cs index c22fc36..21a17ce 100644 --- a/Runtime/Attribute/TickableAttribute.cs +++ b/Runtime/Attribute/TickableAttribute.cs @@ -1,6 +1,4 @@ using System; -using Mew.Core; -using UnityEngine.PlayerLoop; namespace Doinject { diff --git a/Runtime/TargetMethodsInfo.cs b/Runtime/TargetMethodsInfo.cs index c455b3a..fac3fc3 100644 --- a/Runtime/TargetMethodsInfo.cs +++ b/Runtime/TargetMethodsInfo.cs @@ -42,6 +42,8 @@ public TargetMethodsInfo(Type targetType) var tickableAttr = methodInfo.GetCustomAttribute(typeof(TickableAttribute), true) as TickableAttribute; if (!methodInfo.IsPublic) throw new Exception($"Tickable method must be public. {targetType.Name}.{methodInfo.Name}()"); + if (methodInfo.GetParameters().Length > 0) + throw new Exception("Tickable method should not have any parameters"); if (!TickableMethods.TryGetValue(tickableAttr.Timing, out var tickableMethods)) TickableMethods[tickableAttr.Timing] = new List(); TickableMethods[tickableAttr.Timing].Add(methodInfo); diff --git a/Tests/TestObjects.cs b/Tests/TestObjects.cs index ff6ee87..cb6c0d0 100644 --- a/Tests/TestObjects.cs +++ b/Tests/TestObjects.cs @@ -141,6 +141,14 @@ public class TickableObject } } + public class InvalidTickableObject + { + [Tickable] + public void PostLateUpdate(int arg) + { + } + } + public class FieldInjectionWithNonPublicObject { [Inject] private InjectedObject injectedObject; diff --git a/Tests/TickableTest.cs b/Tests/TickableTest.cs index a5f50f7..6006a77 100644 --- a/Tests/TickableTest.cs +++ b/Tests/TickableTest.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Mew.Core.TaskHelpers; using NUnit.Framework; using UnityEngine; @@ -48,12 +49,23 @@ public async Task UpdateTimingTest() Assert.That(instance.UpdateCount, Is.GreaterThan(8)); Assert.That(instance.PreLateUpdateCount, Is.GreaterThan(8)); Assert.That(instance.PostLateUpdateCount, Is.GreaterThan(8)); - - Assert.That(instance.UpdateCount, Is.EqualTo(instance.EarlyUpdateCount + 1)); - Assert.That(instance.UpdateCount, Is.EqualTo(instance.PreUpdateCount + 1)); - Assert.That(instance.UpdateCount, Is.EqualTo(instance.PreLateUpdateCount + 1)); - Assert.That(instance.UpdateCount, Is.EqualTo(instance.PostLateUpdateCount + 1)); Assert.That(instance.FixedUpdateCount, Is.GreaterThan(instance.UpdateCount)); } + + [Test] + public async Task InvalidTickableTest() + { + try + { + container.Bind(); + var _ = await container.ResolveAsync(); + } + catch (Exception e) + { + if (e.Message.Contains("Tickable method should not have any parameters")) + Assert.Pass(); + } + Assert.Fail(); + } } } \ No newline at end of file