Skip to content

Commit

Permalink
Merge pull request #43 from mewlist/tickable
Browse files Browse the repository at this point in the history
Tickable
  • Loading branch information
mewlist authored Feb 22, 2024
2 parents 265821f + c081ca0 commit 6752e87
Show file tree
Hide file tree
Showing 15 changed files with 379 additions and 17 deletions.
25 changes: 25 additions & 0 deletions Runtime/Attribute/TickableAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

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;
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Attribute/TickableAttribute.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Runtime/DIContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TargetTypeInfo, IInternalResolver> ReadOnlyBindings => Resolvers;
internal IReadOnlyDictionary<TargetTypeInfo, ConcurrentObjectBag> ReadOnlyInstanceMap => ResolvedInstanceBag.ReadOnlyInstanceMap;
Expand All @@ -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<IReadOnlyDIContainer, DIContainer>(this);
}

Expand Down Expand Up @@ -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>(T target, MethodInfo callback, ParallelScope completionSource)
Expand Down Expand Up @@ -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();
Expand Down
79 changes: 79 additions & 0 deletions Runtime/Internals/Ticker.cs
Original file line number Diff line number Diff line change
@@ -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<TickableTiming, List<TickableMethod>> invokers = new();

public Ticker()
{
MewLoop.Add<MewUnityEarlyUpdate>(InvokeEarlyUpdate);
MewLoop.Add<MewUnityFixedUpdate>(InvokeFixedUpdate);
MewLoop.Add<MewUnityPreUpdate>(InvokePreUpdate);
MewLoop.Add<MewUnityUpdate>(InvokeUpdate);
MewLoop.Add<MewUnityPreLateUpdate>(InvokePreLateUpdate);
MewLoop.Add<MewUnityPostLateUpdate>(InvokePostLateUpdate);
}

public void Dispose()
{
MewLoop.Remove<MewUnityEarlyUpdate>(InvokeEarlyUpdate);
MewLoop.Remove<MewUnityFixedUpdate>(InvokeFixedUpdate);
MewLoop.Remove<MewUnityPreUpdate>(InvokePreUpdate);
MewLoop.Remove<MewUnityUpdate>(InvokeUpdate);
MewLoop.Remove<MewUnityPreLateUpdate>(InvokePreLateUpdate);
MewLoop.Remove<MewUnityPostLateUpdate>(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<TickableTiming, List<MethodInfo>> methodsTickableMethods)
{
foreach (var (timing, methods) in methodsTickableMethods)
foreach (var methodInfo in methods)
{
if (!invokers.ContainsKey(timing))
invokers[timing] = new List<TickableMethod>();
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();
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Internals/Ticker.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 14 additions & 4 deletions Runtime/TargetMethodsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Doinject
internal class TargetMethodsInfo
{
public List<MethodInfo> InjectMethods { get; } = new();
public Dictionary<TickableTiming, List<MethodInfo>> TickableMethods { get; } = new();
public List<MethodInfo> PostInjectMethods { get; } = new();
public List<MethodInfo> OnInjectedMethods { get; } = new();

Expand All @@ -20,24 +21,33 @@ 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}()");
if (methodInfo.GetParameters().Length > 0)
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}()");
if (methodInfo.GetParameters().Length > 0)
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 (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<MethodInfo>();
TickableMethods[tickableAttr.Timing].Add(methodInfo);
}
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions Tests/TestObjects.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

namespace Doinject.Tests
{
Expand Down Expand Up @@ -99,6 +100,55 @@ 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 InvalidTickableObject
{
[Tickable]
public void PostLateUpdate(int arg)
{
}
}

public class FieldInjectionWithNonPublicObject
{
[Inject] private InjectedObject injectedObject;
Expand Down
71 changes: 71 additions & 0 deletions Tests/TickableTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
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<TickableObject>();
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.FixedUpdateCount, Is.GreaterThan(instance.UpdateCount));
}

[Test]
public async Task InvalidTickableTest()
{
try
{
container.Bind<InvalidTickableObject>();
var _ = await container.ResolveAsync<InvalidTickableObject>();
}
catch (Exception e)
{
if (e.Message.Contains("Tickable method should not have any parameters"))
Assert.Pass();
}
Assert.Fail();
}
}
}
3 changes: 3 additions & 0 deletions Tests/TickableTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Writerside~/di.tree
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<toc-element topic="injection.md"/>
<toc-element topic="bindings.md"/>
<toc-element topic="bindings_advanced.md"/>
<toc-element topic="tickable.md"/>
<toc-element topic="unitask-integration.md"/>
<toc-element topic="troubleshooting.md"/>
</instance-profile>
1 change: 1 addition & 0 deletions Writerside~/di_en.tree
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<toc-element topic="injection_en.md"/>
<toc-element topic="bindings_en.md"/>
<toc-element topic="bindings_advanced_en.md"/>
<toc-element topic="tickable_en.md"/>
<toc-element topic="unitask-integration_en.md"/>
<toc-element topic="troubleshooting_en.md"/>
</instance-profile>
Loading

0 comments on commit 6752e87

Please sign in to comment.