Search Unity

Here is an Editor Script to Help Place Objects on Ground

Discussion in 'Scripting' started by bibbinator, Jan 4, 2010.

  1. bibbinator

    bibbinator

    Joined:
    Nov 20, 2009
    Posts:
    507
    I was looking for a simple way to place objects on the ground or surface below the object and couldn't seem to find any way in Unity to do it easily.

    Here is an editor script that you can use to place the selected object on the surface below it. Just put this code into a file called DropObjectEditor.js in your Editor folder.

    When you run it a small window opens that lets you place it by bottom, origin or center and you can simply dock the window in your editor and it's always available then.

    Please let me know of any bugs.

    Cheers

    EDIT: Realized multi-select dropping was trivial and still worked for single selections, so changed the code to drop multiple items. Easier to populate a large area or put items on a table...

    Code (csharp):
    1.  
    2. class DropObjectEditor extends EditorWindow
    3. {
    4.     // add menu item
    5.     @MenuItem ("Window/Drop Object")
    6.    
    7.     static function Init ()
    8.     {
    9.         // Get existing open window or if none, make a new one:
    10.         var window : DropObjectEditor = EditorWindow.GetWindow(DropObjectEditor);
    11.         window.Show ();
    12.     }
    13.    
    14.     function OnGUI ()
    15.     {
    16.         GUILayout.Label ("Drop Using:", EditorStyles.boldLabel);
    17.         GUILayout.BeginHorizontal();
    18.        
    19.         if(GUILayout.Button("Bottom"))
    20.         {
    21.             DropObjects("Bottom");
    22.         }
    23.        
    24.         if(GUILayout.Button("Origin"))
    25.         {
    26.             DropObjects("Origin");
    27.         }
    28.        
    29.         if(GUILayout.Button("Center"))
    30.         {
    31.             DropObjects("Center");
    32.         }
    33.        
    34.         GUILayout.EndHorizontal();
    35.     }
    36.    
    37.     function DropObjects(method : String)
    38.     {
    39.         // drop multi-selected objects using the right method
    40.         for(var i : int = 0; i < Selection.transforms.length; i++)
    41.         {
    42.             // get the game object
    43.             var go : GameObject = Selection.transforms[i].gameObject;
    44.            
    45.             // don't think I need to check, but just to be sure...
    46.             if(!go)
    47.             {
    48.                 continue;
    49.             }
    50.            
    51.             // get the bounds
    52.             var bounds : Bounds = go.renderer.bounds;
    53.             var hit : RaycastHit;
    54.             var yOffset : float;
    55.        
    56.             // override layer so it doesn't hit itself
    57.             var savedLayer : int = go.layer;
    58.             go.layer = 2; // ignore raycast
    59.             // see if this ray hit something
    60.             if(Physics.Raycast(go.transform.position, -Vector3.up, hit))
    61.             {
    62.                 // determine how the y will need to be adjusted
    63.                 switch(method)
    64.                 {
    65.                     case "Bottom":
    66.                         yOffset = go.transform.position.y - bounds.min.y;
    67.                         break;
    68.                     case "Origin":
    69.                         yOffset = 0.0;
    70.                         break;
    71.                     case "Center":
    72.                         yOffset = bounds.center.y - go.transform.position.y;
    73.                         break;
    74.                 }
    75.                 go.transform.position = hit.point;
    76.                 go.transform.position.y += yOffset;
    77.             }
    78.             // restore layer
    79.             go.layer = savedLayer;
    80.         }
    81.     }
    82. }
    83.  
     
    akeemovic and Rodolfo-Rubens like this.
  2. SirLancelot

    SirLancelot

    Joined:
    Dec 30, 2009
    Posts:
    144
    Wow, that's really helpful. Thank you so much! Unity should support something like this out of the box. But one question..

    Is it possible to place it in the Editor as Button right there where all the other positioning "gizmos" like move/rotate/scale are? Haven't done some Editor GUI scripting before so I have no idea if it's possible or not.
     
  3. lazalong

    lazalong

    Joined:
    Oct 13, 2009
    Posts:
    139
    In fact it seems there is a trick (but I know it only for Mac)

    Place-on-surface function if you hold shift and apple, click the center of the GO's gizmo and move it around (pivot point must be set to center in the top of Unity)
    http://forum.unity3d.com/viewtopic.php?p=239837#239837
     
    unity_Y1nE58wfAPIsCQ likes this.
  4. llde_chris

    llde_chris

    Joined:
    Aug 13, 2009
    Posts:
    205
    It's ctrl+shift for windows.
     
  5. SirLancelot

    SirLancelot

    Joined:
    Dec 30, 2009
    Posts:
    144
    Yeah that does work - thank you :).
     
  6. rw3iss

    rw3iss

    Joined:
    Dec 17, 2012
    Posts:
    5
    Awesome. Thanks!
     
  7. BPPHarv

    BPPHarv

    Joined:
    Jun 9, 2012
    Posts:
    318
    I've customized your code to deal with a mixture of game objects who's top object doesn't have a renderer component on it. I just set the bounds, as in my case the location of that object is the bottom in game terms. Some other intrepid user might want to do a look up through the hierarchy to calculate a bounding volume based on child objects.

    Thanks for posting the original!.

    Code (csharp):
    1.  
    2.                 // get the bounds
    3.                 var bounds : Bounds;
    4.                
    5.                 if(go.renderer)
    6.                 {
    7.                     bounds = go.renderer.bounds;
    8.                 }
    9.                 else
    10.                 {
    11.                     bounds.center=go.transform.position;
    12.                     bounds.extents=Vector3(0,0,0);                 
    13.                 }
    14.  
     
  8. ocarinamat

    ocarinamat

    Joined:
    Feb 3, 2013
    Posts:
    1
    Thank you!!
     
  9. florianveltman

    florianveltman

    Joined:
    Sep 7, 2012
    Posts:
    27
    hey thanks bibbinator,
    I converted the script to C#, and added the possibility to have the object's rotation to be updated to match the surface's normal direction. I hope it's useful to someone!
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. public class DropObjectsEditor : EditorWindow
    4. {
    5.     RaycastHit hit;
    6.     float yOffset;
    7.     int savedLayer;
    8.     bool AlignNormals;
    9.     Vector3 UpVector = new Vector3(0, 90, 0);
    10.     [MenuItem("Window/Drop Object")]                                                // add menu item
    11.     static void Awake()
    12.     {
    13.         EditorWindow.GetWindow<DropObjectsEditor>().Show();                         // Get existing open window or if none, make a new one
    14.     }
    15.  
    16.     void OnGUI()
    17.     {
    18.         GUILayout.Label("Drop using: ", EditorStyles.boldLabel);
    19.         EditorGUILayout.BeginHorizontal();
    20.  
    21.         if (GUILayout.Button("Bottom"))
    22.         {
    23.             DropObjects("Bottom");
    24.         }
    25.  
    26.         if (GUILayout.Button("Origin"))
    27.         {
    28.             DropObjects("Origin");
    29.         }
    30.  
    31.         if (GUILayout.Button("Center"))
    32.         {
    33.             DropObjects("Center");
    34.         }
    35.         EditorGUILayout.EndHorizontal();
    36.         AlignNormals = EditorGUILayout.ToggleLeft("Align Normals", AlignNormals);  // toggle to align the object with the normal direction of the surface
    37.         if (AlignNormals)
    38.         {
    39.             EditorGUILayout.BeginHorizontal();
    40.             UpVector = EditorGUILayout.Vector3Field("Up Vector", UpVector);          // Vector3 helping to specify the Up vector of the object
    41.                                                                                      // default has 90° on the Y axis, this is because by default
    42.                                                                                      // the objects I import have a rotation.
    43.                                                                                      // If anyone has a better way to do this I'd be happy
    44.                                                                                      // to see a better solution!
    45.             GUILayout.EndHorizontal();
    46.         }
    47.     }
    48.  
    49.     void DropObjects(string Method)
    50.     {
    51.         for (int i = 0; i < Selection.transforms.Length; i++)                       // drop multi-selected objects using the selected method
    52.         {
    53.             GameObject go = Selection.transforms[i].gameObject;                     // get the gameobject
    54.             if (!go) { continue; }                                                  // if there's no gameobject, skip the step — probably unnecessary but hey…
    55.  
    56.             Bounds bounds = go.GetComponent<Renderer>().bounds;                     // get the renderer's bounds
    57.             savedLayer = go.layer;                                                  // save the gameobject's initial layer
    58.             go.layer = 2;                                                           // set the gameobject's layer to ignore raycast
    59.  
    60.             if (Physics.Raycast(go.transform.position, -Vector3.up, out hit))       // check if raycast hits something
    61.             {
    62.                 switch (Method)                                                     // determine how the y position will need to be adjusted
    63.                 {
    64.                     case "Bottom":
    65.                         yOffset = go.transform.position.y - bounds.min.y;
    66.                         break;
    67.                     case "Origin":
    68.                         yOffset = 0f;
    69.                         break;
    70.                     case "Center":
    71.                         yOffset = bounds.center.y - go.transform.position.y;
    72.                         break;
    73.                 }
    74.                 if (AlignNormals)                                                   // if "Align Normals" is checked, set the gameobject's rotation
    75.                                                                                     // to match the raycast's hit position's normal, and add the specified offset.
    76.                 {
    77.                     go.transform.up = hit.normal + UpVector;
    78.                 }
    79.                 go.transform.position = new Vector3(hit.point.x, hit.point.y + yOffset, hit.point.z);
    80.             }
    81.             go.layer = savedLayer;                                                  // restore the gameobject's layer to it's initial layer
    82.         }
    83.     }
    84. }
    If anyone has an idea on how to make it so you can "undo" the script's actions, that'd also be super awesome.
     
  10. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    Some necro! But it's a usefull script.

    To enable Undo, just add
    Code (CSharp):
    1. Undo.RecordObjects(Selection.Transforms, "Drop Objects")
    to the top of the DropObjects script.

    I would also add a null-check to the GetComponent<Renderer> - if the user has selected some objects that doesn't have renderers, the script will fail.
     
    Rodolfo-Rubens likes this.
  11. wolfen231

    wolfen231

    Joined:
    Apr 22, 2014
    Posts:
    402
    Saved me so much time. Thanks for these scripts.
     
  12. Rodolfo-Rubens

    Rodolfo-Rubens

    Joined:
    Nov 17, 2012
    Posts:
    1,197
    Thanks guys! This should be put on some type of very useful scripts repository, very very useful!
     
  13. 4Xrn7oIe

    4Xrn7oIe

    Joined:
    Jan 29, 2016
    Posts:
    10
    Hey guys,

    I believe I noticed a few bugs, and have a slightly modified implementation which has worked well for my purposes so far.

    One is that the yOffset is not really what you want if your up vector is anything but perfectly vertical. Otherwise, you'll be translating in y only, when really what you want is to translate some distance along the object's y/up axis.

    The other bug is that you need to use GetComponentsInChildren< Renderer >() to get the given GameObject's full hierarchy of Renderers and compute a full bounding box from that, rather than assuming that if the given GameObject itself does not have a renderer, then neither will any of its descendents. For my purposes, I ended up not needing to compute any bounding box, but I believe something like this will work if you need it:

    Code (CSharp):
    1.     Bounds GetBoundsForGameObjectHierarchy( GameObject go )
    2.     {
    3.         Bounds bounds = new Bounds();
    4.         bounds.center = go.transform.position;
    5.         bounds.extents = Vector3.zero;
    6.  
    7.         // Deal with parent and all descendents (GetComponentsInChildren() gets everything)
    8.         Renderer[] rgDescendentRenderers = go.GetComponentsInChildren< Renderer >();
    9.         foreach( Renderer renderer in rgDescendentRenderers  )
    10.         {
    11.             if ( !renderer )
    12.                 continue;
    13.  
    14.             bounds.Encapsulate( renderer.bounds );
    15.         }
    16.  
    17.         return bounds;
    18.     }
    The original solution also does some stuff where it manipulates the rotation/up vector of the given game objects that that I didn't need and I'm not sure makes sense. My solution will search down along the given set of game objects' up vectors and place the objects on the closest objects it finds, and if you have "align rotations" checked, it will correctly align the given objects to the normal at the hit point(s).

    The original solution also sets the GameObject's layer property to '2' temporarily, which seemed a bit hacky to me and potentially unreliable (from Googling a bit, it seemed like 2 was arbitrary and not representative of a constant somewhere and is not guaranteed to work). I changed the casting to a RaycastAll() and grabbed the closest object. From what I understand, it is acceptable to assume that the raycast will not hit the GameObject in question, assuming the origin of the cast starts within any collider, if the GameObject has one.

    For my purposes, I'm dropping objects onto planets and what I've got here has been working well so far.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. public class DropObjectsEditorWindow : EditorWindow
    5. {
    6.     private bool m_bAlignRotations = true;
    7.  
    8.     // Add a menu item
    9.     [ MenuItem("Window/Drop Object(s)") ]
    10.  
    11.     static void Awake()
    12.     {
    13.         // Get or create an editor window
    14.         EditorWindow.GetWindow< DropObjectsEditorWindow >().Show();
    15.     }
    16.  
    17.     void OnGUI()
    18.     {
    19.         EditorGUILayout.BeginHorizontal();
    20.  
    21.         if ( GUILayout.Button( "Drop/align selected object(s)" ) )
    22.         {
    23.             DropObjects();
    24.         }
    25.  
    26.         EditorGUILayout.EndHorizontal();
    27.  
    28.         // Add checkbox for rotation alignment
    29.         m_bAlignRotations = EditorGUILayout.ToggleLeft( "Align Rotations", m_bAlignRotations );
    30.     }
    31.  
    32.     void DropObjects()
    33.     {
    34.         Undo.RecordObjects( Selection.transforms, "Drop Objects" );
    35.  
    36.         for ( int i = 0; i < Selection.transforms.Length; i++ )
    37.         {
    38.             GameObject go = Selection.transforms[i].gameObject;
    39.             if ( !go )
    40.                 continue;
    41.  
    42.             // Cast a ray and get all hits
    43.             RaycastHit[] rgHits = Physics.RaycastAll( go.transform.position, -go.transform.up, Mathf.Infinity );
    44.  
    45.             // We can assume we did not hit the current game object, since a ray cast from within the collider will implicitly ignore that collision
    46.             int iBestHit = -1;
    47.             float flDistanceToClosestCollider = Mathf.Infinity;
    48.             for( int iHit = 0; iHit < rgHits.Length; ++iHit )
    49.             {
    50.                 RaycastHit CurHit = rgHits[ iHit ];
    51.  
    52.                 // Assume we want the closest hit
    53.                 if ( CurHit.distance > flDistanceToClosestCollider )
    54.                     continue;
    55.  
    56.                 // Cache off the best hit
    57.                 iBestHit = iHit;
    58.                 flDistanceToClosestCollider = CurHit.distance;
    59.             }
    60.  
    61.             // Did we find something?
    62.             if ( iBestHit < 0 )
    63.             {
    64.                 Debug.LogWarning( "Failed to find an object on which to place the game object " + go.name + "." );
    65.                 continue;
    66.             }
    67.  
    68.             // Grab the best hit
    69.             RaycastHit BestHit = rgHits[ iBestHit ];
    70.  
    71.             // Set position
    72.             go.transform.position = new Vector3( BestHit.point.x, BestHit.point.y, BestHit.point.z );
    73.  
    74.             // Set rotation
    75.             if ( m_bAlignRotations )
    76.             {
    77.                 go.transform.rotation *= Quaternion.FromToRotation( go.transform.up, BestHit.normal );
    78.             }
    79.         }
    80.     }
    81. }
    I'm new to C#, so please do let me know if you notice anything weird in what I'm doing, or see any bugs/problems/etc. Thanks a lot.
     
    Last edited: Feb 10, 2016
  14. rmh16

    rmh16

    Joined:
    Feb 19, 2016
    Posts:
    3
    @4Xrn7oIe: thanks a million -- this has saved me countless hours of arranging fiddly little things across a terrain :)
     
  15. Crossway

    Crossway

    Joined:
    May 24, 2016
    Posts:
    507
    Hi. It doesn't work correctly for some objects.