AnnulusGames / LitMotion

Lightning-fast and Zero Allocation Tween Library for Unity.

Home Page:https://annulusgames.github.io/LitMotion/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Proposal] Allow OnCancel and struct state.

Akeit0 opened this issue · comments

Related to #12
以下のクラスとOnCancelがあれば、Unitaskの特別対応もなくなり、StructのState、MultiStateも可能になります。
OnCancelがないと、Cancelされた場合にDisposeができなくなります。
また、FixedString系のメソッドよりもパフォーマンスが落ちても、UnsafeTextを用いて、Dispose処理した方が使いやすいのではないかと思います。

public abstract class Box
{
    internal abstract void Release();
}
public  class Box<T> : Box 
{
    static Stack<Box<T>> pool = new Stack<Box<T>>();
    public T Value;
    public static Box<T> CreateOrGet(T value)
    {
        if (pool.TryPop(out var result))
        {
            result.Value = value;
            return result;
        }
        return new Box<T>(){Value = value};
    }
    internal override void Release()
    {
        Value = default;
        pool.Push(this);
    }
}
public struct UnsafeActionList : IDisposable
{
    ActionList head;

    public void Append([NotNull] Action action)
    {
        ActionList.Append(ref head, action);
    }

    public void Add<TTarget>([NotNull] TTarget target, [NotNull] Action<TTarget> action) where TTarget : class
    {
        ActionList.Append(ref head, target, action);
    }
    public void RemoveTarget(object target)
    {
        ActionList.RemoveTarget(ref head, target);
    }
    public void Remove(object target,object action)
    {
        ActionList.Remove(ref head, target,action);
    }
    public void Invoke()
    {
        if (head == null) return;
        head.Invoke();
        head = null;
    }

    public void InvokeAndDispose()
    {
        if (head == null) return;
        head.InvokeAndDispose();
        head = null;
    }

    public void Dispose()
    {
        if (head == null) return;
        head.Dispose();
        head = null;
    }
}

public sealed class ActionList : IDisposable
{
    static ActionList pool;
    object target;
    object action;
    ActionList nextNode;
    public ActionList LastNode
    {
        get
        {
            var next = this;
            while (next.nextNode != null)
            {
                next = next.nextNode;
            }
            return next;
        }
    }
    ActionList()
    {
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static ActionList CreateOrGet()
    {
        if (pool == null)
        {
            return new ActionList();
        }
        else
        {
            ref var poolRef = ref pool;
            var result = poolRef;
            poolRef = result.nextNode;
            result.nextNode = null;
            return result;
        }
    }

    public static void Append(ref ActionList head, [NotNull] Action action)
    {
        if (action == null) return;
        if (head == null)
        {
            head = CreateOrGet(null, action);
        }
        else
        {
            head.LastNode.nextNode = CreateOrGet(null, action);
        }
    }

    public static void Append<TTarget>(ref ActionList head, [NotNull] TTarget target, [NotNull] Action<TTarget> action)
        where TTarget : class
    {
        if (target == null)
        {
            Debug.LogError("target is null");
            return;
        }

        if (action == null) return;
        if (head == null)
        {
            head = CreateOrGet(target, action);
        }
        else
        {
            head.LastNode.nextNode = CreateOrGet(target, action);
        }
    }
    public static void RemoveTarget(ref ActionList head, object target)
    {
        if (head == null) return;
        if (head.target == target)
        {
            head = head.nextNode;
            return;
        }
        var next = head;
        while (next.nextNode != null)
        {
            if (next.nextNode.target == target)
            {
                next.nextNode = next.nextNode.nextNode;
                return;
            }
            next = next.nextNode;
        }
    }
    public static void Remove(ref ActionList head, object target,object action)
    {
        if (head == null) return;
        if (head.target == target&&head.action==action)
        {
            head = head.nextNode;
            return;
        }
        var next = head;
        while (next.nextNode != null)
        {
            if (next.nextNode.target == target&&next.nextNode.action==action)
            {
                next.nextNode = next.nextNode.nextNode;
                return;
            }
            next = next.nextNode;
        }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ActionList CreateOrGet(object target, object action)
    {
        var result = CreateOrGet();
        result.target = target;
        result.action = action;
        return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Invoke()
    {
        var next = this;
        InvokeLabel:
        try
        {
            if (next.target != null)
                UnsafeUtility.As<object, Action<object>>(ref next.action)(next.target);
            else UnsafeUtility.As<object, Action>(ref next.action)();
        }
        catch (Exception ex)
        {
            Debug.LogException(ex);
        }

        var nextNext = next.nextNode;
        if (nextNext != null)
        {
            next = nextNext;
            goto InvokeLabel;
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void InvokeAndDispose()
    {
        var next = this;
        InvokeLabel:
        try
        {
            if (next.target != null)
                UnsafeUtility.As<object, Action<object>>(ref next.action)(next.target);
            else UnsafeUtility.As<object, Action>(ref next.action)();
        }
        catch (Exception ex)
        {
            Debug.LogException(ex);
        }

        next.action = null;
        next.target = null;
        var nextNext = next.nextNode;
        if (nextNext != null)
        {
            next = nextNext;
            goto InvokeLabel;
        }

        next.nextNode = pool;
        pool = this;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Dispose()
    {
        var next = this;
        InvokeLabel:
        next.action = null;
        next.target = null;
        var nextNext = next.nextNode;
        if (nextNext != null)
        {
            next = nextNext;
            goto InvokeLabel;
        }

        next.nextNode = pool;
        pool = next;
    }
}

OnCancelについては #34 で追加しました。複数Stateのサポートについては現在検討中です。

またstringのトゥイーンについてですが、UnsafeTextは採用すべきでないと考えます。実際にMagic TweenではUnsafeTextを採用していますが、それによってDispose処理をパイプラインに組み込む必要が生まれた結果、余計な設計の複雑化を招きました。(故にUnsafeTextの採用は失敗だったと考えています。)

UnsafeTextは実質的にポインタのラッパーに過ぎないため安全性の確保が難しく、値の更新時に新たに確保したUnsafeTextを返す必要が生まれるなど考慮すべき点が多数あります。その点FixedStringは単なるunmanaged構造体であるため値の引き渡しも安全であり、利用側にとっても使用するサイズを選択することによる最適化の余地が生まれます。

提案にあったようなラッパークラスを用いて値型に対応したStateを実装した結果、モーション作成時のパフォーマンスの低下やコードの複雑化に見合うだけのメリットはないと判断しました。そのためこの機能は実装されません。