Gauntlet mode in development: Still level building

This week I’ve been mostly placing content in Gauntlet mode levels. I did make an SMG drone as a less deadly version of the the existing MMG drone:

smg_drone

I also made a little Unity editor prefab placer script which I’ll share for any Unity devs out there.

In Unity, if you want to add things to a scene you usually drag prefabs from the Project window, which is really just a folder view. It works pretty well but it means that you need to switch around in folders to find things, and you also can’t see the object while you’re positioning it. Unity provides pretty good tools to customise the editor so I made a little custom window instead:

thing_placer

It lets you spawn arbitrary prefabs with the buttons, and it automatically raycasts to the ground and places things there. You can show a visual indicator for placing invisible prefabs, and there’s an option to give things a random starting rotation. You could also rotate the selected object with the keyboard like in the gif above, but for some reason it often ran at like 1fps and was horrible to use, so I’ve removed that feature.

The code isn’t anything special because it’s just meant for my personal use in the editor, but I’ll share it here as I think it could be useful to other unity devs. If you’re using it, read the code comments as there are some changeable things within, but it should work as-is as long as you replace the paths in¬†LoadPrefabList with your own:

using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
 
// GUI editor interface for easily placing a set of hard-coded prefabs on a map
public class SimpleThingPlacer : EditorWindow {
	// Set in inspector. Have this enabled to give prefabs a random starting rotation
	[SerializeField]
	bool randomYRotation = true;
 
	// Got prefabs that don't have a visual model? Put their GameObject names here to see a visual indicator when holding them
	readonly string[] prefabsThatNeedVisualIcons = {"VehicleSpawner"};
 
	// Your prefabs can be assigned to categories you define here, and will show up in these groups in the editor window
	enum Categories { NonEnemy, Enemy, Group }
 
	Dictionary<Categories, List> prefabs;
	GameObject curHeldPrefab;
 
	// #### UNITY INTERNAL METHODS ####
 
	protected void OnGUI() {
		if (Application.isPlaying) return;
		if (prefabs.IsNullOrEmpty()) LoadPrefabList();
 
		EditorGUILayout.BeginVertical("Box");
		randomYRotation = EditorGUILayout.Toggle("Random Y rotation: ", randomYRotation);
		EditorGUILayout.EndVertical();
 
		// This is pretty basic - could be modified to auto-set NUM_PER_ROW based on window width
		const int NUM_PER_ROW = 3;
		const int ENTRY_HEIGHT = 64;
		const int ENTRY_WIDTH = 80;
		foreach (KeyValuePair<Categories, List> category in prefabs) {
			bool isInRow = false;
			EditorGUILayout.BeginVertical("Box");
			EditorGUILayout.LabelField(category.Key.ToString(), EditorStyles.boldLabel);
			List p = category.Value;
			for (int i = 0; i < p.Count; i++) {
				GameObject prefab = p[i];
				if (i % NUM_PER_ROW == 0) {
					GUILayout.BeginHorizontal();
					isInRow = true;
				}
 
				GUILayout.BeginVertical();
				if (GUILayout.Button(AssetPreview.GetAssetPreview(prefab), GUILayout.Height(ENTRY_HEIGHT), GUILayout.Width(ENTRY_WIDTH))) {
					CreatePrefab(prefab);
				}
				GUILayout.Label(prefab.name, GUILayout.Width(ENTRY_WIDTH));
				GUILayout.EndVertical();
 
 
				if ((i + 1) % NUM_PER_ROW == 0) {
					GUILayout.EndHorizontal();
					isInRow = false;
				}
			}
			if (isInRow) GUILayout.EndHorizontal();
			EditorGUILayout.EndVertical();
		}
	}
	protected void OnEnable() {
		LoadPrefabList();
		SceneView.onSceneGUIDelegate += OnSceneGUI;
	}
 
	protected void OnDestroy() {
		// ReSharper disable once DelegateSubtraction
		SceneView.onSceneGUIDelegate -= OnSceneGUI;
	}
 
	protected void OnSceneGUI(SceneView sceneView) {
		if (curHeldPrefab != null) {
			// PLACE HELD OBJECT ON GROUND
			if (Event.current.type == EventType.repaint) {
				Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
 
				RaycastHit hit;
				// Could mask this if you want to exclude some things. The actual held prebab is already excluded, 
				const float CAST_DISTANCE = 1000;
 
				// METHOD 1
				// Less reliable, but no layer mask needed (you can still mask if you want to) -
				// the prefab that's being held is automatically excluded
				bool rayHitSomething = Physics.Raycast(ray, out hit, CAST_DISTANCE);
				if (rayHitSomething) {
					// In metres. How far to move ahead before trying again if we hit our own prebab
					const float MOVE_AHEAD_DIST = 0.5f;
					// Avoid hitting our own prefab, regardless of what layer it's on
					while (rayHitSomething && HasTransformInAncestors(hit.transform, curHeldPrefab.transform)) {
						Vector3 newStartPos = hit.point + (MOVE_AHEAD_DIST * ray.direction);
						rayHitSomething = Physics.Raycast(newStartPos, ray.direction, out hit, CAST_DISTANCE);
					}
					curHeldPrefab.transform.position = hit.point;
				}
 
				// METHOD 2
				// Simpler and more reliable option, but the mask needs to NOT include whatever layers
				// the held prefabs might be on, or they'll try to position themselves on themselves
				/*
				int hitMask = 1 << LayerMask.NameToLayer("Static");
				bool rayHitSomething = Physics.Raycast(ray, out hit, CAST_DISTANCE, hitMask);
				if (rayHitSomething) {
					curHeldPrefab.transform.position = hit.point;
				}
				 */
			}
 
			// VISUAL HANDLE FOR INVISIBLE PREFABS
			if (prefabsThatNeedVisualIcons.Contains(curHeldPrefab.name)) {
				Handles.SphereCap(0, curHeldPrefab.transform.position, curHeldPrefab.transform.rotation, 1.0f);
			}
 
			// RELEASE ON MOUSE 1 PRESS
			Event curEvent = Event.current;
			if (curEvent.type == EventType.MouseDown && curEvent.button == 0) {
				ReleaseHeldPrefab();
			}
		}
	}
 
	// #### PUBLIC METHODS ####
 
	[MenuItem("Window/Simple Thing Placer")]
	public static void ShowWindow() {
		GetWindow(typeof(SimpleThingPlacer));
	}
 
	// #### PROTECTED/PRIVATE METHODS ####
 
	void LoadPrefabList() {
		prefabs = new Dictionary<Categories, List>();
 
		// Replace these with the paths to the assets you want to show up
		// Or these hard-coded paths could be replaced by automatically loading all assets in a folder etc
		// Or maybe create an interface in the editor window itself to add/remove prefabs
		AddPrefab(prefabs, Categories.NonEnemy,  "Assets/Prefabs/Environment/SinglePlayerPhysicsDamageable/Walls/WoodenWall.prefab");
		AddPrefab(prefabs, Categories.NonEnemy, "Assets/Prefabs/Environment/SinglePlayerPhysicsDamageable/Walls/ConcreteBarrier.prefab");
		AddPrefab(prefabs, Categories.Enemy, "Assets/Prefabs/EnemyNonVehicles/Drones/MMGDrone.prefab");
		AddPrefab(prefabs, Categories.Enemy, "Assets/Prefabs/EnemyNonVehicles/Drones/SMGDrone.prefab");
		AddPrefab(prefabs, Categories.Group, "Assets/Prefabs/GauntletSets/GauntletEarthRoadblock1.prefab");
	}
 
	// Path should start with the Assets forlder, e.g. "Assets/Textures/texture.jpg"
	void AddPrefab(Dictionary<Categories, List> prefabDict, Categories category, string path) {
		GameObject prefab = AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)) as GameObject;
		if (prefab != null) {
			ISpawnedThing ist = prefab.GetComponent(typeof(ISpawnedThing)) as ISpawnedThing;
			if (ist != null) {
				if (!prefabDict.ContainsKey(category)) prefabDict[category] = new List(1);
				prefabDict[category].Add(prefab);
			}
		}
		else {
			Debug.LogWarning("Couldn't find prefab at: " + path);
		}
	}
 
	void CreatePrefab(GameObject prefab) {
		curHeldPrefab = (GameObject)Instantiate(prefab);
		// Remove any automatic "(clone)" designation
		curHeldPrefab.name = prefab.name;
		if (randomYRotation) {
			Vector3 eulerRot = curHeldPrefab.transform.eulerAngles;
			eulerRot.y = Random.Range(0, 359);
			curHeldPrefab.transform.eulerAngles = eulerRot;
		}
		Selection.activeGameObject = curHeldPrefab;
	}
 
	void ReleaseHeldPrefab() {
		curHeldPrefab = null;
	}
 
	// Returns true if the input transform or any of its parents, grandparents etc have the specified tag.
	static bool HasTransformInAncestors(Transform trans, Transform match) {
		if (trans == match) return true;
 
		trans = trans.parent;
		while (trans != null) {
			if (trans == match) return true;
			trans = trans.parent;
		}
		return false;
	}
}
Bookmark the permalink. Both comments and trackbacks are currently closed.

One Comment

  1. Josh
    Posted August 22, 2016 at 2:59 am | Permalink

    Thanks for the script! I always wanted something like that but was too swamped with other tasks to make one.