Skip to content

Instantly share code, notes, and snippets.

@anastasiadevana
Last active February 8, 2025 06:28
Show Gist options
  • Save anastasiadevana/2783a15edf1a969c62186e4c2ec0fa8b to your computer and use it in GitHub Desktop.
Save anastasiadevana/2783a15edf1a969c62186e4c2ec0fa8b to your computer and use it in GitHub Desktop.
Quickly hook any function in your class to a button in the Unity3d Inspector.

How to use

Let's say you have a function called MyFunction, and you want to show a button in the inspector to trigger it.

Just add a "decoy" variable with the [ButtonInvoke] attribute in your script (type of the variable doesn't really matter, but its name will be used as button text.

// "MyFunction" is the name of the function you want to trigger
[ButtonInvoke("MyFunction")] public bool testMyFunction;

// OR use "nameof" to prevent typos
[ButtonInvoke(nameof(MyFunction))] public bool testMyFunction;

This will display a button in the inspector, clicking on which will call MyFunction.

Button Example

So simple! 😊

What else can this thing do?

  • pass a parameter into your function
  • specify custom text for the button
  • choose whether to show the button in play mode, edit mode, or both

* See the ButtonInvokeTest.cs script below and the demo gif for these usage examples.

FAQ

  • How is this different from Unity's [ContextMenu]
    • A button is much faster to click, so you can "spam" it. It can also pass a parameter into your function, so you can have multiple buttons calling the same function with different parameters.

This gif shows the test code in ButtonInvokeTest.cs in action.

ButtonInvoke Demo Gif

using UnityEngine;
// Place this somewhere in your project (do NOT place in an "Editor" folder)
public class ButtonInvoke : PropertyAttribute
{
/// <summary>
/// If you choose to display in editor or both, make sure that your script has a flag to run in editor.
/// </summary>
public enum DisplayIn
{
PlayMode,
EditMode,
PlayAndEditModes
}
public readonly string customLabel;
public readonly string methodName;
public readonly object methodParameter;
public readonly DisplayIn displayIn;
/// <summary>
/// Add this attribute to any dummy field in order to show a button in inspector.
/// </summary>
/// <param name="methodName">Name of the method to call. I recommend using nameof() function to prevent typos.</param>
/// <param name="methodParameter">Optional parameter to pass into the method.</param>
/// <param name="displayIn">Should the button show in play mode, edit mode, or both.</param>
/// <param name="customLabel">Optional custom label.</param>
public ButtonInvoke(string methodName, object methodParameter = null, DisplayIn displayIn = DisplayIn.PlayMode, string customLabel = "")
{
this.methodName = methodName;
this.methodParameter = methodParameter;
this.displayIn = displayIn;
this.customLabel = customLabel;
}
}
using UnityEditor;
using UnityEngine;
// Place this in a folder called "Editor" somewhere in your project.
[CustomPropertyDrawer(typeof(ButtonInvoke))]
public class ButtonInvokeDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
ButtonInvoke settings = (ButtonInvoke) attribute;
return DisplayButton(ref settings) ? EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing : 0;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
ButtonInvoke settings = (ButtonInvoke) attribute;
if (!DisplayButton(ref settings)) return;
string buttonLabel = (!string.IsNullOrEmpty(settings.customLabel)) ? settings.customLabel : label.text;
if (property.serializedObject.targetObject is MonoBehaviour mb)
{
if (GUI.Button(position, buttonLabel))
{
mb.SendMessage(settings.methodName, settings.methodParameter);
}
}
}
private bool DisplayButton(ref ButtonInvoke settings)
{
return (settings.displayIn == ButtonInvoke.DisplayIn.PlayAndEditModes) ||
(settings.displayIn == ButtonInvoke.DisplayIn.EditMode && !Application.isPlaying) ||
(settings.displayIn == ButtonInvoke.DisplayIn.PlayMode && Application.isPlaying);
}
}
using UnityEngine;
// Test script for the [ButtonInvoke] attribute that shows various usages.
// Add this script to a cube or another 3D primitive to see it in action.
// Using [ExecuteAlways] here to show that buttons can be optionally displayed in the editor. It's not required for buttons to work.
[ExecuteAlways]
public class ButtonInvokeTest : MonoBehaviour
{
#region Test Buttons
// Just the method call - shows up in play mode only.
// You could just use "Rotate" here as a string, but using nameof() will protect from typos and renaming a method.
// The variable here is just a dummy placeholder that the field attribute is attached to.
// The name of the variable (rotate) is used to display the button label.
[ButtonInvoke(nameof(Rotate))] public bool rotate;
// Method with parameter.
// This and the next line call the same method Grow(), but with a different parameter.
// I'm using [SerializeField] to makes this private variable show up in the inspector.
// You can just use public variables without [SerializeField].
[SerializeField, ButtonInvoke(nameof(Grow), 3.0f)] private bool _growALot;
// Method with parameter that only works in editor mode.
[SerializeField, ButtonInvoke(nameof(Grow), 1.1f, ButtonInvoke.DisplayIn.EditMode)] private bool _growALittle;
// Method that works both in play and editor modes and has a custom label.
[SerializeField, ButtonInvoke(nameof(ChangeColor), null, ButtonInvoke.DisplayIn.PlayAndEditModes, "* C * O * L * O * R * I * F * I * C")] private bool _changeColor;
// Method that works both in play and editor modes.
[SerializeField, ButtonInvoke(nameof(ResetToDefault), null, ButtonInvoke.DisplayIn.PlayAndEditModes)] private bool _resetToDefault;
#endregion
#region Private Variables
private readonly Vector3 _defaultScale = Vector3.one;
private readonly Color _defaultColor = Color.gray;
#endregion
#region Methods To Test
private void Rotate()
{
transform.Rotate(Vector3.up, 45.0f);
}
private void Grow(float howMuch)
{
transform.localScale *= howMuch;
}
private void ResetToDefault()
{
transform.localScale = _defaultScale;
gameObject.GetComponent<Renderer>().sharedMaterial.color = _defaultColor;
}
private void ChangeColor()
{
gameObject.GetComponent<Renderer>().sharedMaterial.color = Color.HSVToRGB(Random.Range(0.0f, 1.0f), 1.0f, 1.0f);
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment