Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question Moveable Points using Custom Attribute or Custom Editor

Discussion in 'Editor & General Support' started by FaffyWaffles, Dec 1, 2022.

  1. FaffyWaffles

    FaffyWaffles

    Joined:
    Apr 10, 2020
    Posts:
    45
    Preface: I'm trying to create a custom attribute that draws moveable gizmos from an array or list of points (Vector2 or Vector3)

    The dream would be to have something like:

    [MoveablePoints]
    public List<Vector2> examplePointSetList;
    [MoveablePoints]
    public Vector2[] examplePointSetArray;



    ...however, I don't know how achievable something like that is. I'm not familiar with attributes.
    ---
    BZ3zy.gif ij1ue.gif



    ---
    So far, I have this helper script that I use to move points around manually while debugging.
    (an older version can be seen here):
    Code (CSharp):
    1.     public class MovablePoints : MonoBehaviour
    2.     {
    3.  
    4.         [Range(.01f, 1f)] public float selectionRadius = .05f;
    5.  
    6.         public bool selectionActive = true;
    7.  
    8.         public class MovablePoint
    9.         {
    10.             public Vector2 point;
    11.  
    12.             public MovablePoint(float x, float y)
    13.             {
    14.                 this.point = new Vector2(x, y);
    15.             }
    16.             public MovablePoint(Vector2 point)
    17.             {
    18.                 this.point = point;
    19.             }
    20.         }
    21.  
    22.         public class Point
    23.         {
    24.             public Vector2 position;
    25.             public Vector2 offset;
    26.             public Color color;
    27.             public bool isMoving;
    28.  
    29.             public MovablePoint movablePoint;
    30.  
    31.             public Vector2 posRef;
    32.  
    33.             public Point(ref Vector2 position, MovablePoint movablePoint)
    34.             {
    35.                 this.position = position;
    36.                 this.movablePoint = movablePoint;
    37.             }
    38.  
    39.             public bool IsPointInRange(Vector2 mosPos, float selectionRadius)
    40.             {
    41.                 float dx = Mathf.Abs(position.x - mosPos.x);
    42.                 float dy = Mathf.Abs(position.y - mosPos.y);
    43.  
    44.                 if (dx > selectionRadius)
    45.                     return false;
    46.                 if (dy > selectionRadius)
    47.                     return false;
    48.                 if (dx + dy <= selectionRadius)
    49.                     return true;
    50.                 if (Mathf.Pow(dx, 2) + Mathf.Pow(dy, 2) <= Mathf.Pow(selectionRadius, 2))
    51.                     return true;
    52.                 return false;
    53.             }
    54.  
    55.         }
    56.  
    57.         public List<Point> points = new List<Point>();
    58.  
    59.         private Vector2 clickedPos;
    60.         private void Update()
    61.         {
    62.             Vector2 mosPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    63.  
    64.             if (Input.GetMouseButtonDown(0))
    65.             {
    66.                 clickedPos = mosPos;
    67.  
    68.                 foreach (Point point in points)
    69.                 {
    70.                     //if (IsPointInRange(point.position, mosPos) && !point.isMoving)
    71.                     if (point.IsPointInRange(mosPos, selectionRadius))
    72.                     {
    73.                         point.isMoving = true;
    74.                         point.offset = point.position - clickedPos;
    75.                     }
    76.                 }
    77.             }
    78.             if (Input.GetMouseButton(0))
    79.             {
    80.                 foreach (Point point in points)
    81.                 {
    82.                     if (point.isMoving)
    83.                     {
    84.                         point.position = mosPos + point.offset;
    85.                         point.movablePoint.point = mosPos + point.offset;
    86.                     }
    87.                 }
    88.             }
    89.             if (Input.GetMouseButtonUp(0))
    90.             {
    91.                 foreach (Point point in points)
    92.                 {
    93.                     point.offset = Vector2.zero;
    94.                     point.isMoving = false;
    95.                 }
    96.             }
    97.         }
    98.  
    99.         void OnDrawGizmos()
    100.         {
    101.             if (selectionActive)
    102.             {
    103.                 Color selectionColor = Color.white;
    104.                 selectionColor.a = 0.65f;
    105.                 Handles.color = selectionColor;
    106.                 if (Input.GetAxis("Mouse ScrollWheel") > 0f && selectionRadius < 1)
    107.                     selectionRadius += .01f;
    108.                 if (Input.GetAxis("Mouse ScrollWheel") < 0f && selectionRadius > 0)
    109.                     selectionRadius -= .01f;
    110.  
    111.                 Vector2 mosPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    112.                 Handles.DrawWireDisc(mosPos, new Vector3(0, 0, 1), selectionRadius);
    113.  
    114.                 foreach (Point point in points)
    115.                 {
    116.                     bool IsUnderSelectedArea = point.IsPointInRange(mosPos, selectionRadius);
    117.  
    118.                     if (point.isMoving)
    119.                         Gizmos.color = Color.green;
    120.                     else if (IsUnderSelectedArea)
    121.                         Gizmos.color = Color.red;
    122.                     else
    123.                         Gizmos.color = Color.white;
    124.  
    125.                     Gizmos.DrawSphere(point.position, 0.05f);
    126.                 }
    127.             }
    128.         }
    129.     }


    Currently, it's pretty jank as I've been experimenting with custom classes to store references to value types. This system is rather undesirable, and not very streamlined or intuitive.
     
  2. FaffyWaffles

    FaffyWaffles

    Joined:
    Apr 10, 2020
    Posts:
    45
    Basically, the moveable points work fine. I'm trying to streamline the "subscription" process with a custom attribute, and don't really know where to start.
     
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,612
    I... don't think this is actually possible out of the box with Unity's property drawer system?

    Namely the way you'd draw these points in the scene is to is to subscribe to
    SceneView.duringSceneGui
    , and unsubscribe when the object is no longer selected. However PropertyDrawer's don't have any 'Initialise' or similar message like a UnityEditor.Editor's OnEnable/OnDisable messages.

    Even then it would only work when the object is selected, as knowing what fields of certain objects have particular attributes at all time would be enormously non-performant.

    This would be trivial with tools such as Odin Inspector that have far more feature rich drawing abilities.
     
    Last edited: Dec 1, 2022
  4. FaffyWaffles

    FaffyWaffles

    Joined:
    Apr 10, 2020
    Posts:
    45
    Hmmm...

    In that case, is there some way for the MoveablePoints "Points" class to keep a reference to the value that created it, and update it remotely? Meaning, update the original "Vector2" that is stored in the list.
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,612
    I think this is another point where Unity's UI features fall short again. Far as I know there's no way to look 'upwards' to the root Unity object that a drawer is in without managing it all manually.

    If you're keen on doing this in smart ways I'd invest in Odin Inspector. Otherwise you'll be engineering all the same stuff yourself.
     
  6. FaffyWaffles

    FaffyWaffles

    Joined:
    Apr 10, 2020
    Posts:
    45
    ---

    I have Odin Inspector, (and I think I recognize you from the discord?)

    ----

    Ideally, I would like to figure out how to treat the value type items in a list as reference types, or update them from within the MoveablePoints class.

    I feel like it should be possible, as given a list of the "MoveablePoint" class updates automatically when the values are changed. (as it is it is a reference type.)

    My thinking is that you can use the "ref" keyword to pass a value as a reference into a method, so you should be able to save and reuse that reference?

    What is difference between a list of classes vs a list of structs, that you can save a reference to the objects?
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,612
    Or better asked, "What is the difference between a reference type in a collection, and a value type in a collection". Collections, be it arrays or lists, are reference types. Their elements can be either. The collections themselves behave as reference types, while each element behaves as appropriate.

    Ergo, you can get references to specific elements should they be reference types. But not if they're value types. You can get a reference to the entire collection, whereby updating one of its elements will be seen by anything else referencing the same collection.

    Should've said that earlier! Could've had this done ages ago via Odin.

    You could do something like this:

    Code (CSharp):
    1. public sealed class MoveablePointsAttributeDrawer<T> : OdinAttributeDrawer<MoveablePointsAttribute, T>, IDisposeable : where T : IEnumerable<Vector2>
    2. {
    3.     private IEnumerable<Vector2> points;
    4.  
    5.     protected override void Intialize()
    6.     {
    7.         points = (IEnumerable<Vector2>)Property.ValueEntry.WeakSmartValue;
    8.         UnityEditor.SceneView.duringSceneGui += DrawScenePoints;
    9.     }
    10.  
    11.     private void DrawScenePoints(SceneView sceneView)
    12.     {
    13.        //draw points
    14.     }
    15.  
    16.     public void Dispose()
    17.     {
    18.         UnityEditor.SceneView.duringSceneGui -= DrawScenePoints;
    19.     }
    20. }
    Written in notepad++ so could be some errors.

    But so long as you assign your points back through to Property.ValueEntry.WeakSmartValue then Odin handles dirtying the root object. You can also get a reference to the root object with
    Property.SerializationRoot.ValueEntry.WeakSmartValue
    , or dirty it with
    Property.MarkSerializationRootDirty()
    ;

    So you can just do:
    Code (CSharp):
    1. [MoveablePoints, SerializeField]
    2. private void List<Vector2> points = new List<Vector2>();
    And the scene drawing will work when the object is selected in the inspector.

    Again this only works when selecting the object with these values. If you want it to be more persistent, you could set up a static class that holds onto the last selected object.
     
    Last edited: Dec 2, 2022
    FaffyWaffles likes this.