Skip to content

Instantly share code, notes, and snippets.

@desplesda
Last active May 9, 2019 12:02
Show Gist options
  • Save desplesda/bc186ba8e6c7485604c2998afd70675b to your computer and use it in GitHub Desktop.
Save desplesda/bc186ba8e6c7485604c2998afd70675b to your computer and use it in GitHub Desktop.
PropLinePlacer - a tool for placing lines of prefabs! Video: https://twitter.com/desplesda/status/1126410852658106368
// PropLinePlacer - a tool for placing lines of prefabs
// By Jon Manning
// MIT License
// Copyright (c) 2019 Secret Lab Pty Ltd
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using UnityEngine;
public class PropLinePlacer : MonoBehaviour
{
/// <summary>
/// The end point of the line, in local space.
/// </summary>
/// <remarks>The start point is the transform of this object.</remarks>
public Vector3 endPoint = new Vector3(1, 0, 0);
/// <summary>
/// The possible prefabs that this line should contain. Prefabs will be
/// randomly selected from this list.
/// </summary>
public GameObject[] prefabs;
/// <summary>
/// The number of objects that should be created.
/// </summary>
public uint numberOfObjects = 3;
/// <summary>
/// If true, the created objects will be randomly rotated around the Y
/// axis.
/// </summary>
public bool randomizeYRotation = true;
/// <summary>
/// The amount of distance, in the X and Z directions, to randomly
/// offset our objects.
/// </summary>
public float randomXZDistance = 0.05f;
/// <summary>
/// The degree of distance along the line to shift our objects.
/// </summary>
public float maxLineDisplacement = 0f;
/// <summary>
/// A list of materials to randomly assign to the created objects.
/// </summary>
/// <remarks>If this array is empty, the created objects will not have
/// their materials changed.</remarks>
public Material[] materials;
/// <summary>
/// Produces an array of positions, in world space, for objects to be
/// created at.
/// </summary>
/// <returns>The positions, in world space, for objects to be
/// placed.</returns>
public Vector3[] GetSpawnPositions()
{
// Early out if we're producing zero objects.
if (numberOfObjects == 0)
{
return new Vector3[0];
}
var result = new Vector3[numberOfObjects];
// Save the state of the random number generator, because we're
// about to blow it away
var state = UnityEngine.Random.state;
// Reset the random number generator so that it's consistent
// between frames
Random.InitState(0);
// For each object we want to create, calculate its position
for (int i = 0; i < numberOfObjects; i++)
{
// Figure out how far along the line we are, from 0 to 1
var t = i / (float)(numberOfObjects - 1);
// Randomly displace this along the line (this is why we're
// preserving random state, otherwise the positions would
// change every frame that the object is selected in the
// editor)
t += Random.Range(-maxLineDisplacement, maxLineDisplacement) * 1f / numberOfObjects;
// Calculate this position along the line!
var position = Vector3.Lerp(
transform.position, // start point
endPoint + transform.position, // end point
t // position on the line (from 0 to 1)
);
result[i] = position;
}
// Restore the random state we saved earlier
Random.state = state;
// Return the list of positions!
return result;
}
}
// PropLinePlacer - a tool for placing lines of prefabs
// By Jon Manning
// MIT License
// Copyright (c) 2019 Secret Lab Pty Ltd
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using UnityEngine;
// If this file isn't in a folder named Editor, it will be compiled into
// the final project, and will cause build errors because the UnityEditor
// namespace isn't available in built players. Wrapping the whole thing in
// an #if-#endif avoids this problem. (You should still put this file in an
// Editor folder, because it's tidier.)
#if UNITY_EDITOR
using UnityEditor;
/// <summary>
/// Editor for the PropLinePlacer.
/// </summary>
[CustomEditor(typeof(PropLinePlacer)), CanEditMultipleObjects]
public class PropLinePlacerEditor : Editor
{
/// <summary>
/// Draws the usual inspector GUI, but also adds a button that creates
/// the objects.
/// </summary>
public override void OnInspectorGUI()
{
DrawDefaultInspector();
// When the user clicks the Place Objects button, create objects
// along the line
if (GUILayout.Button("Place Objects"))
{
// For each PropLinePlacer that is selected, create its
// objects.
foreach (PropLinePlacer t in targets)
{
PlaceObjects(t);
}
}
}
/// <summary>
/// Places objects for a single PropLinePlacer.
/// </summary>
/// <param name="placer"></param>
private void PlaceObjects(PropLinePlacer placer)
{
if (placer.prefabs.Length == 0)
{
// No prefabs to place; nothing we can do here
return;
}
foreach (var position in placer.GetSpawnPositions())
{
// Select the prefab we want to use here
var prefab = placer.prefabs[Random.Range(0, placer.prefabs.Length - 1)];
// Instantiate the prefab (note we're using
// PrefabUtility.InstantiatePrefab rather than
// GameObject.Instantiate, so what we're creating is linked to
// the prefab!
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
// Be nice and let the user undo this if they want
Undo.RegisterCreatedObjectUndo(instance, "Create object");
// Place this instance under ourselves, and position and rotate
// it correctly
instance.transform.SetParent(placer.transform, false);
instance.transform.position = position;
instance.transform.rotation = placer.transform.rotation;
// Rotate around Y if we need to
if (placer.randomizeYRotation)
{
var rotation = Random.Range(0, 359);
instance.transform.Rotate(0, rotation, 0);
}
// Offset our position in the X-Z plane by a random amount
float distance = placer.randomXZDistance;
var xDisplacement = Random.Range(-distance / 2f, distance / 2f);
var zDisplacement = Random.Range(-distance / 2f, distance / 2f);
instance.transform.Translate(xDisplacement, 0, zDisplacement);
// If we have materials to use, randomly select one, and apply
// it to the mesh renderer
var renderer = instance.GetComponent<MeshRenderer>();
if (placer.materials.Length > 0 && renderer != null)
{
var material = placer.materials[Random.Range(0, placer.materials.Length)];
renderer.sharedMaterial = material;
}
}
}
/// <summary>
/// Draws the Scene GUI: the line itself, the positions we will spawn
/// objects, and allow for changing the line's end point interactively.
/// </summary>
private void OnSceneGUI()
{
PropLinePlacer placer = (PropLinePlacer)target;
EditorGUI.BeginChangeCheck();
var startPoint = placer.transform.position;
var endPoint = placer.endPoint + startPoint;
// Draw the handles that let us modify the end point
endPoint = Handles.PositionHandle(endPoint, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(placer, "Change placer end point");
placer.endPoint = endPoint - startPoint;
}
// Draw the line itself
Handles.DrawDottedLine(startPoint, endPoint, 5);
// Don't draw the markers on top of everything else
Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
foreach (var position in placer.GetSpawnPositions()) {
Handles.color = Color.yellow;
// Draw a marker for this location
Handles.SphereHandleCap(0, position, Quaternion.identity, 0.1f, EventType.Repaint);
// Draw a disc representing the possible area that this object
// can be placed
Handles.color = new Color(0, 0, 1, 0.25f);
Handles.DrawSolidDisc(position, Vector3.up, placer.randomXZDistance);
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment