Search Unity

Discussion Ideas for easier control over transform vectors ( eg. position.x += 1; )

Discussion in 'Scripting' started by Zephni, Jan 10, 2023.

  1. Zephni

    Zephni

    Joined:
    Apr 23, 2015
    Posts:
    100
    This is an age old "issue" that for some reason keeps on coming back and bugging me, and I know others have wondered about this aswell, and that is: "is there any way we can manipulate transform vectors like 'position', 'rotation', and 'scale' without having to manually get the original vector, set it to a new value, and then re-set the original transform vector? Which feels like a real pain every time".

    TLDR:
    Code (CSharp):
    1. // Can we turn
    2. Vector3 newPosition = transform.position;
    3. newPosition.x += 1;
    4. transform.position = newPosition;
    5.  
    6. // Into something like this?
    7. position.x += 1;
    The answer is a definite yes! But I'm intrigued to know what your thoughts are on this as a concept / solution. Here is an example class named "TransformVector", in it's constructor it takes the transform of a game object, and the transform type (either position, rotation, or scale), and then you can access this object's x, y and z values just like a stand alone vector, and it will automagically update the appropriate transform values for you behind the scenes:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TransformVector
    4. {
    5.     // Enum for the different transform types
    6.     public enum TransformTypes { Position, Rotation, Scale }
    7.  
    8.     // The transform type
    9.     public TransformTypes transformType;
    10.  
    11.     // Reference to the objects transform
    12.     public Transform transform;
    13.  
    14.     // Floats for the transform vector
    15.     public float x { get { return get().x; } set { set(value); } }
    16.     public float y { get { return get().y; } set { set(null, value); } }
    17.     public float z { get { return get().z; } set { set(null, null, value); } }
    18.     public float xy { set { set(value, value); } }
    19.     public float xz { set { set(value, null, value); } }
    20.     public float yz { set { set(null, value, value); } }
    21.     public float xyz { set { set(new Vector3(value, value, value)); } }
    22.  
    23.     /// <summary>
    24.     /// Constructor
    25.     /// </summary>
    26.     /// <param name="transform"></param>
    27.     /// <param name="transformType"></param>
    28.     public TransformVector(Transform transform, TransformTypes transformType)
    29.     {
    30.         this.transform = transform;
    31.         this.transformType = transformType;
    32.     }
    33.  
    34.     /// <summary>
    35.     /// Get the transform vector
    36.     /// </summary>
    37.     /// <returns>Vector3 of this transform type</returns>
    38.     public Vector3 get()
    39.     {
    40.         switch (transformType) {
    41.             case TransformTypes.Position: return transform.position;
    42.             case TransformTypes.Rotation: return transform.eulerAngles;
    43.             case TransformTypes.Scale: return transform.localScale;
    44.             default: return Vector3.zero;
    45.         }
    46.     }
    47.  
    48.     /// <summary>
    49.     /// Set the transform vector
    50.     /// </summary>
    51.     /// <param name="value"></param>
    52.     public void set(Vector3 value)
    53.     {
    54.         switch (transformType) {
    55.             case TransformTypes.Position: transform.position = value; break;
    56.             case TransformTypes.Rotation: transform.eulerAngles = value; break;
    57.             case TransformTypes.Scale: transform.localScale = value; break;
    58.         }
    59.     }
    60.  
    61.     /// <summary>
    62.     /// Set the transform vector with nullable floats
    63.     /// </summary>
    64.     /// <param name="_x"></param>
    65.     /// <param name="_y"></param>
    66.     /// <param name="_z"></param>
    67.     public void set(float? _x = null, float? _y = null, float? _z = null)
    68.     {
    69.         Vector3 v = get();
    70.         if (_x != null) v.x = (float)_x;
    71.         if (_y != null) v.y = (float)_y;
    72.         if (_z != null) v.z = (float)_z;
    73.         set(v);
    74.     }
    75.  
    76.     /// <summary>
    77.     /// Translate the transform vector
    78.     /// </summary>
    79.     /// <param name="value"></param>
    80.     public void translate(Vector3 value)
    81.     {
    82.         switch (transformType) {
    83.             case TransformTypes.Position: transform.Translate(value); break;
    84.             case TransformTypes.Rotation: transform.Rotate(value); break;
    85.             case TransformTypes.Scale: transform.localScale += value; break;
    86.         }
    87.     }
    88.  
    89.     /// <summary>
    90.     /// Translate the transform vector
    91.     /// </summary>
    92.     /// <param name="_x"></param>
    93.     /// <param name="_y"></param>
    94.     /// <param name="_z"></param>
    95.     public void translate(float _x, float _y = 0, float _z = 0)
    96.     {
    97.         translate(new Vector3(_x, _y, _z));
    98.     }
    99.  
    100.     /// <summary>
    101.     /// Implicit conversion to Vector3
    102.     /// </summary>
    103.     /// <param name="transformVector"></param>
    104.     public static implicit operator Vector3(TransformVector transformVector)
    105.     {
    106.         return transformVector.get();
    107.     }
    108.  
    109.     /* Operator overloads
    110.     -------------------------------*/
    111.  
    112.     public static TransformVector operator +(TransformVector a, Vector3 b)
    113.     {
    114.         a.set(a + b);
    115.         return a;
    116.     }
    117.  
    118.     public static TransformVector operator -(TransformVector a, Vector3 b)
    119.     {
    120.         a.set(a - b);
    121.         return a;
    122.     }
    123.  
    124.     public static TransformVector operator *(TransformVector a, Vector3 b)
    125.     {
    126.         a.set(a * b);
    127.         return a;
    128.     }
    129.  
    130.     public static TransformVector operator /(TransformVector a, Vector3 b)
    131.     {
    132.         a.set(a / b);
    133.         return a;
    134.     }
    135. }
    136.  
    Now I can already hear a few questions some may have about this, and one is that now we have to add our TransformVector to our game object which takes up extra lines anyway, eg:

    Code (CSharp):
    1. // TransformVector fields
    2. TransformVector position;
    3. TransformVector rotation;
    4. TransformVector scale;
    5.  
    6. // GameObject's Awake method
    7. void Awake()
    8. {
    9.     // Set transform vectors
    10.     position = new TransformVector(transform, TransformVector.TransformTypes.Position);
    11.     rotation = new TransformVector(transform, TransformVector.TransformTypes.Rotation);
    12.     scale = new TransformVector(transform, TransformVector.TransformTypes.Scale);
    13. }
    But now we can access any of the properties and manipulate them on a nice single line, like so:

    Code (CSharp):
    1. // GameObject's Update method
    2. void Update()
    3. {
    4.     // Update transform value with TransformVector
    5.     position.x += Time.deltaTime;
    6.     scale.xy = Mathf.Sin(Time.time);
    7.     rotation.xyz += Time.deltaTime;
    8. }
    The other way could be creating something like a "TransformVectors" Component MonoBehaviour that has position, rotation, and scale TransformVector's built in, and then can be accessed through the gameObject that way, like:

    Code (CSharp):
    1. // Somewhere in a game object's Update method
    2. transformVectors.position.x += Time.deltaTime;
    Just out of interest though, what do you think of this approach, and have you got any thoughts on this?

    One final thing is that of course you could add extension methods to all GameObject's with something like:

    Code (CSharp):
    1. // Extension method for changing x position of a game object
    2. public static void SetXPosition(this GameObject gameObject, float x)
    3. {
    4.     Vector3 position = gameObject.transform.position;
    5.     position.x = x;
    6.     gameObject.transform.position = position;
    7. }
    But you can see how quickly this gets messy, as you may want to add, multiply, divide, or set multiple parts of a vector in one go etc, and then you have to add all of those for rotation and scale also, so I personally don't really feel comfortable with this solution, but maybe you guys have some thoughts or better ways of doing this.

    I really do understand that this isn't the "world's worst problem" but was just messing about in Unity and trying some things to see what feels the most comfortable solution! Would be interesting to hear your thoughts
     
    Last edited: Jan 10, 2023
    mgear likes this.
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,689
    I have never been bugged by this issue.

    Extension methods are cute and can tidy things up a little bit, but as soon as you use them, now your code is now completely useless to everybody who does not have ALL the clever little Zephni-authored extension methods.

    But hey, it's your codebase... if it bugs you then by all means, do what you must.
     
    Zephni and SF_FrankvHoof like this.
  3. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    This is caused by structs in Csharp which are scalar types. By moving them to classes you trade ease of use for efficiency. And it means that you are going to lose a lot of FPS by doing so.
     
    Zephni likes this.
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,992
    Not only that. The actual position is stored on the native side. So when you want to change a component of a vector, you have to communicate that to the native side through a method anyways. Unity only has a method for setting the whole position and not individual components. So currently there's no way around a read - modify - write pattern. Though if you want to have a one-liner, you can always do

    Code (CSharp):
    1. transform.position += Vector3.right * Time.deltaTime;
    The
    A += B
    operator in C# can not be overloaded individually as you can in C++ and is always just a short hand for
    A = A + B
    .

    Though for setting individual components to a certain value, there's no way around a temporary variable. Either explicitly or implicitly through an extension method.

    You may want that, however how would you actually express that in the syntax? Do you only think about things that are possible in HLSL? like component swizzling? Unity's new mathematics library does support swizzling as they were aiming for a high correlation with shader code. Though there will always be things that are not directly supported and supporting all those swizzle variants occupies pages after pages of code. Just as an example, look at the float4 struct and the 5000 lines of auto generated code. Though in the end you still have the issue that you need an actual method call into the native engine core to change the actual position.

    The problem is not the struct per say, but the combination with a property. Because reading a property of a struct type will give you a copy of the struct since the getter of the property is a method that returns that struct and therefore is always a copy. You need to invoke the setter of the property to actually cause a change of the position.

    Of course you could simply create your own wrapper struct for "transform.position" that would allow all the operations you may want to support

    Here's a crazy approach that is quite extensible and should not allocate any garbage.
    This is an example how you can modularize the approach and seperate concerns. There are seperate accessor objects (using the strategy pattern) that allows you to choose a certain property. The "Vector3PropertyWrapper" on the other hand only provides methods that work on a wrapped property, not knowing on what kind of object or which property exactly as this is done / choosen by the accessor and the specific extension method:

    Code (CSharp):
    1.  
    2. public interface IVector3Accessor<T>
    3. {
    4.     Vector3 Get(T aObj);
    5.     void Set(T aObj, Vector3 aVector);
    6. }
    7.  
    8. public class TransformPositionAccessor : IVector3Accessor<Transform>
    9. {
    10.     public static readonly IVector3Accessor<Transform> Default = new TransformPositionAccessor();
    11.     public Vector3 Get(Transform aTrans) => aTrans.position;
    12.     public void Set(Transform aTrans, Vector3 aVector) => aTrans.position = aVector;
    13. }
    14. public class TransformLocalPositionAccessor : IVector3Accessor<Transform>
    15. {
    16.     public static readonly IVector3Accessor<Transform> Default = new TransformLocalPositionAccessor();
    17.     public Vector3 Get(Transform aTrans) => aTrans.localPosition;
    18.     public void Set(Transform aTrans, Vector3 aVector) => aTrans.localPosition = aVector;
    19. }
    20.  
    21. public struct Vector3PropertyWrapper<T>
    22. {
    23.     private T m_Obj;
    24.     private IVector3Accessor<T> m_Accessor;
    25.     public Vector3PropertyWrapper(T aObj, IVector3Accessor<T> aAccessor)
    26.     {
    27.         m_Obj = aObj;
    28.         m_Accessor = aAccessor;
    29.     }
    30.     public float X
    31.     {
    32.         get => m_Accessor.Get(m_Obj).x;
    33.         set
    34.         {
    35.             var v = m_Accessor.Get(m_Obj);
    36.             v.x = value;
    37.             m_Accessor.Set(m_Obj, v);
    38.         }
    39.     }
    40.     public float Y
    41.     {
    42.         get => m_Accessor.Get(m_Obj).y;
    43.         set
    44.         {
    45.             var v = m_Accessor.Get(m_Obj);
    46.             v.y = value;
    47.             m_Accessor.Set(m_Obj, v);
    48.         }
    49.     }
    50.     public float Z
    51.     {
    52.         get => m_Accessor.Get(m_Obj).z;
    53.         set
    54.         {
    55.             var v = m_Accessor.Get(m_Obj);
    56.             v.z = value;
    57.             m_Accessor.Set(m_Obj, v);
    58.         }
    59.     }
    60.     public Vector3PropertyWrapper<T> SetX(float aNewX)
    61.     {
    62.         X = aNewX;
    63.         return this;
    64.     }
    65.     public Vector3PropertyWrapper<T> SetY(float aNewY)
    66.     {
    67.         Y = aNewY;
    68.         return this;
    69.     }
    70.     public Vector3PropertyWrapper<T> SetZ(float aNewZ)
    71.     {
    72.         Z = aNewZ;
    73.         return this;
    74.     }
    75. }
    76. public static class TransformPositionWrapperExt
    77. {
    78.     public static Vector3PropertyWrapper<Transform> WPos(this Transform aTransform) => new Vector3PropertyWrapper<Transform>(aTransform, TransformPositionAccessor.Default);
    79.     public static Vector3PropertyWrapper<Transform> LPos(this Transform aTransform) => new Vector3PropertyWrapper<Transform>(aTransform, TransformLocalPositionAccessor.Default);
    80. }
    81.  
    So with this madness you can do

    transform.WPos().X += 1;


    or

    transform.WPos().SetX(5).SetZ(7);


    Of course those are just some examples. You could add whatever additional properties or methods you think may be useful for a Vector3 property.

    I would not recommend using something like that because it makes the code harder to read, especially for people not knowing this hack and also makes it a lot slower because every seperate "set" would cause a read - modify - write cycle on the property. I just posted this to show what is possible to avoid a lot code duplication. Because once you have this setup, you can add support for the localScale by just adding an accessor for the localScale property and adding an extension method to access and create the wrapper.

    Since the accessors are actually shared and cached in a static readonly variable, it does not produce garbage when you use it. The wrapper itself is a struct and does not allocate any heap memory. You can actually write accessors for any type you like.
     
    Zephni and Kurt-Dekker like this.
  5. Zephni

    Zephni

    Joined:
    Apr 23, 2015
    Posts:
    100
    @Bunny83 Wow that's a very interesting approach with the Vector3PropertyWrapper and exactly the kind of thing I was wondering about. In the grand scheme of things like Kurt-Dekker mentioned it is not very helpful when working on projects with others or sharing code or using this inside a "package" of some kind because it's simply not vanilla Unity, and feels very hacky also.

    But I guess this is more me trying to understand some of the deeper aspects of C#, with overloading and extension methods etc etc, and was more of a curiosity to see if there were ways around it. But the Vector3PropertyWrapper mentioned above is pretty much exactly what I was dreaming up, and I wondered if that was possible so it's very handy to see, so thanks for that!

    Also it's a good point that's been made regarding using classes instead of structs, and especially in my case where I had 3 extra objects attached to the original game object. I often forget about the overhead of these things but I guess it can build up. It would be interesting exactly how much difference this makes when it's scaled up.