Skip to content

Instantly share code, notes, and snippets.

@Danik
Last active August 6, 2021 14:03
Show Gist options
  • Save Danik/85d4a7b6d344e69c7579 to your computer and use it in GitHub Desktop.
Save Danik/85d4a7b6d344e69c7579 to your computer and use it in GitHub Desktop.
A simple in-game parameter tweaking script for Unity. It finds all fields and properties marked with [TweakableMember] in MonoBehaviours in a scene, and enables tweaking in a GUI from inside the game, which can be useful on tablets etc, where there is no access to the inspector.
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
///<summary>
/// A simple in-game GUI for Unity that allows tweaking of script fields
/// and properties marked with [TweakableMember]
///</summary>
public class UnityTweakGUI : MonoBehaviour {
public Transform[] targetObjects;
public int winWidth = 350;
public int winHeight = 500;
public bool expanded = false;
private Dictionary<string,List<TweakableParam>> groupParamsMap;
private string[] sortedGroups;
private Vector2 scrollPosition;
private Rect windowRect;
private int headerHeight = 20;
private int expandToggleWidth = 60;
private GUIStyle groupTextStyle;
void Start()
{
groupTextStyle = new GUIStyle();
groupTextStyle.normal.textColor = Color.white;
groupTextStyle.fontStyle = FontStyle.Bold;
groupTextStyle.fontSize = 18;
windowRect = new Rect(0, 0, winWidth, winHeight);
scrollPosition = new Vector2();
InitTweakableParams();
}
/// Init or re-init the tweakable params collection
private void InitTweakableParams()
{
if (groupParamsMap == null)
{
groupParamsMap = new Dictionary<string, List<TweakableParam>>();
}
else
{
groupParamsMap.Clear();
}
// Search all objects in scene by default if none are specified
if (targetObjects == null || targetObjects.Length == 0)
{
Debug.Log("No target transform set for TweakGUI, using all in scene.");
targetObjects = FindAllRootTransformsInScene().ToArray();
}
// Traverse target transforms
foreach (Transform t in targetObjects)
{
AddTweakableParamsForTransform(t);
}
// Sort groups alphabetically
sortedGroups = new string[groupParamsMap.Count];
groupParamsMap.Keys.CopyTo(sortedGroups, 0);
Array.Sort<string>(sortedGroups);
}
/// Walks through the transform heirarchy and finds all [Tweakable] members of MonoBehaviours.
private void AddTweakableParamsForTransform(Transform targetObj)
{
if (targetObj != null)
{
foreach (MonoBehaviour monoBehaviour in targetObj.GetComponents<MonoBehaviour>())
{
foreach (MemberInfo memberInfo in monoBehaviour.GetType().GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
{
if (memberInfo is FieldInfo || memberInfo is PropertyInfo)
{
foreach (object attributeObj in memberInfo.GetCustomAttributes(true))
{
if (attributeObj is TweakableMemberAttribute)
{
var attribute = (TweakableMemberAttribute)attributeObj;
var tweakableParam = new TweakableParam(attribute, memberInfo, monoBehaviour);
List<TweakableParam> paramList;
if (groupParamsMap.TryGetValue(attribute.group, out paramList))
{
paramList.Add(tweakableParam);
}
else
{
paramList = new List<TweakableParam>();
paramList.Add(tweakableParam);
groupParamsMap.Add(attribute.group, paramList);
}
}
}
}
}
}
// Add tweakable fields in child components recursively
foreach (Transform child in targetObj)
{
AddTweakableParamsForTransform(child);
}
}
}
/// Draw the tweak GUI.
void OnGUI()
{
// Scale GUI to make it useable on high-res displays (retina etc)
var scale = 1f;
if (Screen.dpi > 200) {
scale = 3f;
}
else if (Screen.dpi > 100)
{
scale = 2f;
}
var scaleVec = new Vector2(scale, scale);
GUIUtility.ScaleAroundPivot(scaleVec, Vector2.zero);
if (expanded)
{
windowRect.width = winWidth;
windowRect.height = winHeight;
}
else
{
windowRect.width = expandToggleWidth + 20;
windowRect.height = headerHeight + 10;
}
// Draw window
windowRect = GUILayout.Window(0, windowRect, delegate(int id)
{
GUI.DragWindow(new Rect(expandToggleWidth, 0, winWidth - expandToggleWidth, headerHeight));
GUILayout.BeginVertical();
expanded = GUI.Toggle(new Rect(0, 0, expandToggleWidth, headerHeight), expanded, "Expand");
if (expanded)
{
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
GUILayout.BeginVertical();
// Iterate over sorted param groups and lay out the right kind of GUI
// controls for each tweakable parameter.
foreach (var group in sortedGroups) {
GUILayout.Label(group, groupTextStyle);
List<TweakableParam> tweakableParams;
if (groupParamsMap.TryGetValue(group, out tweakableParams))
{
for (int i = 0; i < tweakableParams.Count; i++)
{
TweakableParam tweakableParam = tweakableParams[i];
TweakableMemberAttribute attr = tweakableParam.attribute;
MemberInfo memberInfo = tweakableParam.memberInfo;
object value = tweakableParam.GetMemberValue();
Type type = value.GetType();
object newValue = null;
string paramName = attr.displayName != "" ? attr.displayName : memberInfo.Name;
// Output the right GUI control
bool showHeaderLabel = true;
bool showValueLabel = true;
if (type == typeof(bool))
{
showHeaderLabel = false;
}
if (showHeaderLabel)
{
GUILayout.Label(paramName);
}
GUILayout.BeginHorizontal();
// Check the type of the member and draw the right control
if (type == typeof(float))
{
newValue = GUILayout.HorizontalSlider((float)value, attr.minValue, attr.maxValue);
}
else if (type == typeof(int))
{
newValue = (int)GUILayout.HorizontalSlider((int)value, attr.minValue, attr.maxValue);
}
else if (type == typeof(bool))
{
newValue = GUILayout.Toggle((bool)value, paramName);
showValueLabel = false;
}
if (showValueLabel)
{
GUILayout.Label(type == typeof(float) ? ((float)value).ToString("F3") : value.ToString(), GUILayout.Width(40));
}
GUILayout.EndHorizontal();
if (newValue != null)
{
tweakableParam.SetMemberValue(newValue);
}
}
}
}
GUILayout.EndVertical();
GUILayout.EndScrollView();
}
GUILayout.EndVertical();
}, "Tweaks");
}
private List<Transform> FindAllRootTransformsInScene()
{
List<Transform> roots = new List<Transform>();
foreach (GameObject obj in UnityEngine.Object.FindObjectsOfType(typeof(GameObject)))
{
if (obj.transform.parent == null)
{
roots.Add(obj.transform);
}
}
return roots;
}
/// Private class that wraps a tweakable member
private class TweakableParam
{
public TweakableMemberAttribute attribute;
public MemberInfo memberInfo;
private object ownerObject;
private bool isField;
public TweakableParam(TweakableMemberAttribute attr, MemberInfo memberInfo, object ownerObject)
{
this.attribute = attr;
this.memberInfo = memberInfo;
this.ownerObject = ownerObject;
this.isField = memberInfo is FieldInfo;
if (!(memberInfo is FieldInfo || memberInfo is PropertyInfo))
{
throw new ArgumentException("Member " + memberInfo.ToString() + " not supported.");
}
}
public object GetMemberValue()
{
if (isField)
{
return ((FieldInfo)memberInfo).GetValue(ownerObject);
}
else
{
return ((PropertyInfo)memberInfo).GetValue(ownerObject, null);
}
}
public void SetMemberValue(object value)
{
if (isField)
{
((FieldInfo)memberInfo).SetValue(ownerObject, value);
}
else
{
((PropertyInfo)memberInfo).SetValue(ownerObject, value, null);
}
}
}
}
/// Attribute that can be used to mark fields or properties on MonoBehaivours as Tweakable,
/// letting them be tweaked in an in-game GUI.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
class TweakableMemberAttribute : Attribute
{
public float maxValue;
public float minValue;
public string displayName;
public string group;
private const string DEFAULT_GROUP = "Default";
public TweakableMemberAttribute()
: this(0, 100)
{
}
public TweakableMemberAttribute(string displayname, string group = DEFAULT_GROUP)
: this(0, 100, displayname, group)
{
}
public TweakableMemberAttribute(float minValue, float maxValue, string displayName = "", string group = DEFAULT_GROUP)
{
this.minValue = minValue;
this.maxValue = maxValue;
this.displayName = displayName;
this.group = group;
}
}
@petejonze
Copy link

Just tried this code, and seems to work beautifully! Thanks for sharing!

Are you still developing this and/or working with Unity in general?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment