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 Help with structuring, lists, objects

Discussion in 'Scripting' started by Jshh, Jan 29, 2021.

  1. Jshh

    Jshh

    Joined:
    Sep 14, 2019
    Posts:
    38
    Hi, thanks for reading.

    I'm new to scripting in Unity, and I think I could use a little conceptual help. I've created a dragging/snapping behavior and got the interactions working well enough for my purposes in a minimal test case, but I'm struggling with how to make it more flexible/generic.

    My test case has a flat plane with two cubes on it, viewed from above. The following script is attached to each cube. It handles mouse events, allowing the player to drag the cube, applying some 'inertia' by lerping to a
    desiredPosition
    , and if the desiredPostion is close enough to the sides of a manually set 'target' (the other cube), then it snaps to the appropriate side of the target. I'm using Collider.bounds to make the calculations.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class ZuiObject : MonoBehaviour
    6. {
    7.     // mouse drag
    8.     private float mouseZCoord;
    9.     private Vector3 mouseOffset;
    10.     private Vector3 desiredPosition;
    11.     [SerializeField]private float dragSpeed = 10.0f;
    12.     private bool beingDragged = false;
    13.  
    14.     // dragged object
    15.     Collider m_Collider;
    16.     Vector3 m_Size, m_Min, m_Max;
    17.  
    18.     // target object
    19.     [SerializeField] private GameObject target;
    20.     Collider target_Collider;
    21.     Vector3 target_size, target_Min, target_Max;
    22.  
    23.     // snapping
    24.     [SerializeField] private float snapZone = 0.1f;
    25.     [SerializeField] private float snapSpeed = 15f;
    26.     [SerializeField] private float unSnapThreshold = 2f; // used to divide object size, so 2 = half of object, 3 = third of object, etc
    27.  
    28.     // position shortcuts
    29.     private float leftDist, rightDist, topDist, bottomDist;
    30.     private bool verticallyAligned, horizontallyAligned = false;
    31.  
    32.     private void Start() {
    33.         // set dragged object position as wherever it starts in the scene
    34.         desiredPosition.x = transform.localPosition.x;
    35.         desiredPosition.y = transform.localPosition.y - transform.position.y; // Don't mess with depth
    36.         desiredPosition.z = transform.localPosition.z;
    37.  
    38.         // set dragged object bounds based on collider
    39.         m_Collider = GetComponent<Collider>();
    40.         m_Size = m_Collider.bounds.size;
    41.         m_Min = m_Collider.bounds.min;
    42.         m_Max = m_Collider.bounds.max;
    43.        
    44.         // set target object bounds based on collider
    45.         target_Collider = target.GetComponent<Collider>();
    46.         target_Min = target_Collider.bounds.min;
    47.         target_Max = target_Collider.bounds.max;
    48.     }
    49.    
    50.     // if this object is clicked, set beingDragged & offset click point from object origin position
    51.     void OnMouseDown() {
    52.         mouseZCoord = Camera.main.WorldToScreenPoint(gameObject.transform.localPosition).z;
    53.         mouseOffset = gameObject.transform.localPosition - GetMouseWorldPos();
    54.         beingDragged = true;
    55.     }
    56.     void OnMouseUp() {
    57.         beingDragged = false;
    58.     }
    59.  
    60.     // translate mouse from screen to world space
    61.     private Vector3 GetMouseWorldPos() {
    62.         Vector3 mousePoint = Input.mousePosition;
    63.         mousePoint.z = mouseZCoord;
    64.         return Camera.main.ScreenToWorldPoint(mousePoint);
    65.     }
    66.  
    67.     // On drag, set desiredPosition
    68.     void OnMouseDrag() {
    69.         desiredPosition = GetMouseWorldPos() + mouseOffset;
    70.     }
    71.  
    72.     // get distance from dragged object edges to opposite target edges
    73.     void GetDistances(){
    74.         rightDist = Mathf.Abs(m_Min.x - target_Max.x);
    75.         leftDist = Mathf.Abs(m_Max.x - target_Min.x);
    76.         topDist = Mathf.Abs(target_Max.z - m_Min.z);
    77.         bottomDist = Mathf.Abs(m_Max.z - target_Min.z);
    78.         //Debug.Log("leftDist: " + leftDist + " rightDist: " + rightDist + " topDist: " + topDist + " bottomDist: " + bottomDist + " mouse x: " + Input.mousePosition.x + " m_Max.x: " + m_Max.x);
    79.     }
    80.  
    81.     // set alignment bools (true if dragged object is vertically/horizontally aligned with target)
    82.     void GetAlignments() {
    83.         verticallyAligned = (desiredPosition.x < target_Max.x && desiredPosition.x + m_Size.x > target_Min.x);
    84.         horizontallyAligned = (desiredPosition.z > target_Min.z && desiredPosition.z - m_Size.z < target_Max.z);
    85.         //Debug.Log("horizontallyAligned = " + horizontallyAligned + " verticallyAligned = " + verticallyAligned);
    86.     }
    87.  
    88.     // handle snapping movement
    89.     private void SnapToEdge(){
    90.         transform.localPosition = Vector3.Lerp(transform.localPosition, desiredPosition, snapSpeed * Time.deltaTime);
    91.     }
    92.  
    93.     private void Update() {
    94.         GetDistances();
    95.         GetAlignments();
    96.  
    97.         if (beingDragged) {
    98.             if ( rightDist < snapZone && horizontallyAligned && desiredPosition.x < transform.position.x + (m_Size.x/unSnapThreshold)){ // desiredPosition within half width of dragged object (so you can drag it away easily)
    99.                 desiredPosition.x = target_Max.x;
    100.                 SnapToEdge();
    101.                 //Debug.Log("Snapping to Right!");
    102.             } else if (bottomDist < snapZone && verticallyAligned && desiredPosition.z > transform.position.z - (m_Size.z/unSnapThreshold)) {
    103.                 desiredPosition.z = target_Min.z;
    104.                 SnapToEdge();
    105.                 //Debug.Log("Snapping to Bottom!");
    106.             } else if (leftDist < snapZone && horizontallyAligned && desiredPosition.x > transform.position.x - (m_Size.x/unSnapThreshold)) {
    107.                 desiredPosition.x = target_Min.x - m_Size.x;
    108.                 SnapToEdge();
    109.                 //Debug.Log("Snapping to Left!");
    110.             } else if (topDist < snapZone && verticallyAligned && desiredPosition.z < transform.position.z + (m_Size.z/unSnapThreshold)) {
    111.                 desiredPosition.z = target_Max.z + m_Size.z;
    112.                 SnapToEdge();
    113.                 //Debug.Log("Snapping to top!");
    114.             } else {
    115.                 transform.localPosition = Vector3.Lerp(transform.localPosition, desiredPosition, dragSpeed * Time.deltaTime); // if no snapping, move to drag position
    116.             }
    117.         }
    118.  
    119.         // update collider bounds positions
    120.         m_Min = m_Collider.bounds.min;
    121.         m_Max = m_Collider.bounds.max;
    122.         target_Min = target_Collider.bounds.min;
    123.         target_Max = target_Collider.bounds.max;
    124.     }
    125.  
    126.     // Debug gizmos
    127.     private void OnDrawGizmos() {
    128.         Gizmos.color = new Color(0, 1, 0.5f, 0.5f);
    129.         Gizmos.DrawSphere(desiredPosition, 0.1f);
    130.     }
    131. }
    132.  
    So in my mind, the next step was to create a new manager script to handle everything without needing to put all that script on every cube. I was planning to create a List<> of all the draggable cubes (maybe via a tag), then somehow set the target of the currently dragged cube as the nearest object in the List, so that I would only need to worry about all the calculations for one target at a time.

    Anyway, that plan fell apart when I realized the OnMouseDown() method needed to be on the object to be dragged. That and I wondered whether I should make a Draggable.cs class to put on every draggable object anyway (to store collider/position data) and create a List<Draggable> in the manager script. But I wasn't able to describe my issues effectively enough for Google to be much use.

    So I wondered if anybody had any input about how they think I should structure this. My main concern is to keep everything as generic and reusable as I can, and avoid tying myself up in knots as things get more complicated. Obviously any other suggestions for improvements would be great too- I might be going about this completely wrong :D

    PS. please let me know if any of this is unclear, I'll try to clarify
     
  2. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    You could cast a ray through the mouse and check for the cubes colliders from the manager class (when mouse button is pressed fe). This way you find wether a cube (by tag, by layer, by name?) is under the mosue button. Then you can move it around, change color etc. .
    If you have a special class instance put on each cube depends on what information you need additionally. This class could (un)register itself with the manager for managing purposes ;). Manager would keep a list of them. But this makes only sense if you need to iterate over all cubes. I think there is no "right" way to do this. As most things every way has its (dis)advantages. When it works it is valid. When you have performance concerns/issues find out which way is faster. Personally I would also go the manager way since you probably need this anyway (target position). So especially as a beginner make the whole thing work as you need it. Thats good enough in most cases. Don't try to overengineer such "little" details from the beginning. Such isolated systems can be refactored "easily" if they need to work differently. And also experienced developers can't always predict how a certain way works out. Especially when requirements change.
     
  3. Jshh

    Jshh

    Joined:
    Sep 14, 2019
    Posts:
    38
    @exiguous Thanks for the reply! I hadn't considered using raycasts, actually. That might also be a simpler way to deal with my snap distance checks, now I think about it...

    I know what you mean about overengineering- I just have a feeling that when you've got stuff feeling right, it's good to make sure the overall architecture doesn't foster the accumulation of complexity. But I'm still learning obviously. Anyway I'll post back after I've refactored a bit.

    Thanks again!
     
    exiguous likes this.