Search Unity

Feedback transform.position change callback

Discussion in 'Scripting' started by StarManta, Sep 17, 2019.

  1. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    I can't count the number of posts I've seen here (or the number of issues I've had in my own code) that would be resolved by adding a callback that happens when transform.position is changed. It'd be an insanely easy to add on the callback.

    Concerned about too many callbacks impacting performance? Fine, make it Editor-only. 90% of the time it's just needed for debugging, but holy cow is it needed.
     
    a436t4ataf and heiko-achilles like this.
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    This is what Transform.hasChanged was built for.

    You need to build a system around that - generally something that iterates the objects you care about and check the flag.
     
  3. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    If they'd make it editor only, then people will start complain why not in player even more, than now. Also while there are a lot of cases wich may be solved with this, this is not the only solution possible and this will significanlty slowdown transform in much more cases, where it is completely not needed. So please don't do it.
     
  4. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    That is absolutely worthless for this purpose. You can't check the stacktrace and find out what has caused the change.
     
  5. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    What other solution would you suggest?
     
  6. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    Use this instead of tranform so you will always have that event you want.

    Code (CSharp):
    1. class VerboseTransform : MonoBehaviour {
    2.   public UnityEvent OnPositionChanged;
    3.   public Vector3 position {
    4.      get => transform.position;
    5.      set {
    6.          transform.position = value;
    7.          OnPositionChanged.Invoke();
    8.       }
    9.    }
    10. }
    Working with transform hierarchies is already super slow in Unity, there's no need making it even slower.
     
    Wilhelm_LAS likes this.
  7. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    This solution doesn't help you find position changes in any code you didn't write.

    And if you find and replace in your code (which still wouldn't catch stuff like DLLs), then you have the exact same performance problems you're complaining about for a builtin solution.
     
    a436t4ataf and heiko-achilles like this.
  8. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    Yes. But if there's no built in solution, then there's no performance overhead from them. Bingo!
     
  9. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    What? That doesn't make any sense. It easily could (and, I assume, would) be written in such a way that if nothing has subscribed to the event, there would be no performance cost.
     
  10. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    No, there would be a cost.

    You need to do some kind of :

    Code (csharp):
    1. if (shouldCallback)
    2.     DoCallback();
    Where shouldCallback would be a null-check of some sort. That's expensive at scales like "setting transform's position". The editor overhead is bad enough as-is, so enabling this in the editor sounds bad.

    If you implement something like this, it has to be hidden behind an #if, which is problematic when talking about built-in things, as those are in precompiled dll's. You'd literally have to ship two copies of UnityEngine.dll, or incur a bad performance cost.

    So it's for sure not easy to implement.
     
    angrypenguin likes this.
  11. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    At worst, it needs to do nothing more than checking against a bool (which only needs to be changed when the "on changed" callback is altered), and you'd need to change transform.position literally millions of times in a frame to see any performance impact from a bool check... and if you're setting transform.position millions of times in a frame, the performance problem ain't the bool check.
     
  12. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Also:
    Setting a bool (which the transform.position setter method obviously must do in order for Transform.hasChanged to work) has a miniscule performance overhead just like checking a bool would, and for drastically less usefulness.
     
  13. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    At least it must check if anything subscribed. Also this will affect transform h
    No, this is wrong assumption. At worst it will need to marshall a call to c++ part to check if there are any non-null subscribers present.
     
  14. kru

    kru

    Joined:
    Jan 19, 2013
    Posts:
    452
    No, at worst it will immediately lead to the heat death of the universe.

    ..this is a good feature request and should be considered for inclusion.
     
  15. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    By "at worst", I mean "this is the best solution I can think of that will definitely 100% work, and the worst case scenario is that no one can think of a better one", not, "this is the worst possible solution".

    Imagine an implementation that looks something like this:
    Code (csharp):
    1. Vector3 _position = Vector3.zero;
    2. public Vector3 position {
    3. set {
    4. if (doesHavePositionChangeSubscribers) positionChangeCallback(_position, value);
    5. _position = value; //and insert other math etc
    6. }
    7. get {
    8. return _position; //and other math etc
    9. }
    10. }
    11. private bool doesHavePositionChangeSubscribers = false;
    12. private delegate PositionChangeCallback(Vector3 old, Vector3 new);
    13. private event PositionChangeCallback positionChangeCallback = null;
    14. public void SubscribeToPositionChange(PositionChangeCallback pcc) {
    15. positionChangeCallback += pcc;
    16. doesHavePositionChangeSubscribers = true;
    17. }
    If implemented this way, it would be nothing more than an added bool comparison. There is no need or reason to make the performance overhead any worse than that.
     
  16. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    What if object was destroyed inbetween subscription and invocation, but c# part have not unsubscribed from event (wich is not neccessary in c# and even may be considered bad practice in terms of gc graph fragmentation) ?
     
  17. ChrisHandzlik

    ChrisHandzlik

    Joined:
    Dec 2, 2016
    Posts:
    205
    Last edited: Nov 15, 2021
    Baste likes this.
  18. ChrisHandzlik

    ChrisHandzlik

    Joined:
    Dec 2, 2016
    Posts:
    205
  19. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    (Needed this yet again today, Google brought me here, to make me sad at the fact this is still missing).

    The probable real reason Unity never implemented it is that it requires altering some legacy code buried deep inside the Unity Engine/Editor ('Transform' is a special class, for performance reasons 20 years ago), that no-one wants to be responsible for rewriting/editing/breaking/fixing. That code was probably badly written in the first place (was a tiny company) but over the decades has become very badly maintained/written, so I'm not surprised no-one has the courage/professional confidence to edit it.

    Transform.hasChanged is astonishingly bad code that should never have been published (it's guaranteed to fail on non-trivial projects and there's nothing you can do to fix that: the design of it sticks the data in the wrong place, as a global).

    For the subset of cases that are RectTransform ... I've managed to reliably simulate this by implementing the SetLayoutHorizontal() callback, and all the OnRectTransformChange callbacks -- and then deducing 'if it's a SetLayoutHorizontal but it's not an OnRectTransformXXXChange, then ... it's the 'never implemented' OnRectTransformPositionChange event'. An annoying workaround - but in testing for multiple years has worked reliably - for a core API call Unity never bothered to implement.

    (and, of course: it's not slow at all. That's not why Unity has failed to add it).
     
    FrankLee_legou likes this.
  20. wpetillo

    wpetillo

    Joined:
    May 23, 2017
    Posts:
    24
    I had a similar issue where I wanted to store transform positional data on a component automatically when moving the transform in the editor. Here's what I have, it works well for my purposes (I'm not concerned with performance here because it's editor only, only applies where I attach it, and can be turned off).


    using System;
    using System.Collections;
    using UnityEngine;

    public class TransformWatchTest : MonoBehaviour
    {
    [SerializeField] WatchTransformInEditor example;
    [SerializeField] Vector3 value;

    void OnValidate() => example.OnValidate(this, Value);
    void Value(Vector3 value) => this.value = value;
    }

    [Serializable]
    public class WatchTransformInEditor
    {
    public bool watch = true;
    public float refreshRate = 0.1f;
    public Action<Vector3> onSetPosition;

    IEnumerator tick;

    public void OnValidate(MonoBehaviour mono, Action<Vector3> onSetPosition)
    {
    if (!watch || !mono.gameObject.activeInHierarchy)
    return;

    this.onSetPosition = onSetPosition;

    if (tick == null)
    {
    tick = Tick(mono.transform);
    mono.StartCoroutine(Tick(mono.transform));
    }
    }

    IEnumerator Tick(Transform transform)
    {
    var delay = new WaitForSecondsRealtime(refreshRate);
    while (watch && transform.gameObject.activeInHierarchy)
    {
    onSetPosition?.Invoke(transform.position);
    yield return delay;
    }
    tick = null;
    }
    }
     
  21. ddfletch

    ddfletch

    Joined:
    Nov 15, 2023
    Posts:
    1
    Very useful request. The reasons posted against it have just gotten silly. The cost of a subscriber check is absolutely meaningless.
     
  22. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    I wouldn't say it's meaningless. Something like this would also have to be implemented on the C++ side, as
    Transform
    is mostly just a wrapper class for the C++ object on the managed side.

    So that would mean a marshalled call to the C++ side when assigning a position, and then a call back to the C# side just to check if there are any subscribers to the delegate. That's pretty much doubled the overhead for every time we assign to
    Transform.position
    .

    Maybe a magic method could be implemented, as Unity checks for their existance and only calls them if they know they exist.

    Nonetheless this is pretty trivial to implement with a custom component.
     
    Last edited: Dec 20, 2023
  23. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    It's about 300 lines of code (*)

    Trivial? LOL, no.

    I guess if you're happy with a fundamentally broken hack that will only work for small / toy projects, and will break in very strange ways that waste literal days or weeks in debugging ... then, sure! It's "trivial to implement with a custom component".

    (*) - after all the edge cases I've had to add over the years, dealing with all the different contexts in which transforms can get changed, and ensuring re-entrant calls work etc, along with handling multiple cycles of Unity fixing (or adding) bugs in UnityEditor specifically around changing values of Transform.*
     
  24. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    I mean it's either that or doubling the overhead of every assignment to
    Transform.position
    . I would rather just implement what I need on an as-need basis than suddenly having a core part of Unity become half as performant.

    Also depends what context we're talking, whether that be editing in the editor, or at runtime. There's various solutions to both, all of which depend on your particular use case.

    At the end of the day there's no perfect solution here that doesn't make an integral part of Unity slower, even if it's only by a small amount. And transform hierarchies are already slow to begin with.
     
  25. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    One of my posts above gave a very straightforward way to approach this wherein it would literally just be a bool check. No marshalled call back to C# side unless there's actually a subscriber to the event. An added bool check is the definition of negligible, far from double the overhead.
     
  26. Dawdlebird

    Dawdlebird

    Joined:
    Apr 22, 2013
    Posts:
    88
    somewhat old thread, but has any of you looked at UnityEditor.ObjectChangeEvents.changesPublished? It lets you pick up events such as creation, destruction and transform updates among other things.
    You can do something like this:

    Code (CSharp):
    1. void Initialize()
    2. {
    3.     UnityEditor.ObjectChangeEvents.changesPublished += OnChangesPublished;    // detect scene object changes
    4. }
    5.  
    6. void OnChangesPublished(ref ObjectChangeEventStream stream)
    7. {
    8.     for (int i = 0; i < stream.length; ++i)
    9.     {
    10.         // component has changed
    11.         if (stream.GetEventType(i) == ObjectChangeKind.ChangeGameObjectOrComponentProperties)
    12.         {
    13.             stream.GetChangeGameObjectOrComponentPropertiesEvent(i, out var data);
    14.  
    15.             if (EditorUtility.InstanceIDToObject(data.instanceId) is Transform transform)
    16.             {
    17.                 // do stuff with transform
    18.             }
    19.         }
    20.     }
    21. }
    This code would only exist in some editor tool that needs to keep track of certain transforms. That way you can keep it nice and central to where you need it, without having to inject any code into the relevant gameobjects or monobehaviors themselves (they need not to know they're being watched).
     
    Last edited: Jan 29, 2024
  27. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    That API is useful. But it's Editor-only. Although the first post said "90% of the time only needed for debugging" my experience in the last 5 years is that debugging is actually the least common use-case -- but it's the first one many people run into, and it's the one that's easiest to hack a solution for.