Skip to content

Instantly share code, notes, and snippets.

@TsubameUnity
Last active March 4, 2019 03:51
Show Gist options
  • Save TsubameUnity/4fb8d97628f2cd03290284c71aa546d9 to your computer and use it in GitHub Desktop.
Save TsubameUnity/4fb8d97628f2cd03290284c71aa546d9 to your computer and use it in GitHub Desktop.
Transformのインスペクタ表示に、一括入力ボタンを表示させるエディタ拡張。一括入力ウィンドウを開いているときにEnterで入力を確定、ESCで入力をキャンセルが可能です。
using UnityEngine;
using UnityEditor;
using System;
namespace TransformEx
{
/// <summary>
/// 一括表示を行う場合の設定項目
/// </summary>
internal interface ISetAllMode
{
/// <summary>
/// パラメータのプロパティ一括変更方法を定義する
/// </summary>
Action<FieldProperty, float> SetAllProp { get; set; }
/// <summary>
/// 一括設定用ボタンの表示文
/// </summary>
string AllInputLabel { get; set; }
/// <summary>
/// パラメータのプロパティ一括変更方法が実行されているか
/// </summary>
bool Inputting { get; set; }
}
/// <summary>
/// TransFormパラメータのエディタ描画設定
/// </summary>
internal class FieldProperty : ISetAllMode
{
/// <summary>
/// パラメータのSerializedProperty
/// </summary>
internal SerializedProperty Prop { get; private set; }
/// <summary>
/// パラメータのエディタ表示スタイル
/// </summary>
internal GUIContent Content { get; private set; }
/// <summary>
/// 一括設定用ボタンの表示文
/// </summary>
public string AllInputLabel { get; set; }
/// <summary>
/// パラメータのプロパティ描写方法を定義する
/// </summary>
internal Action<FieldProperty> ViewProp { get; set; }
/// <summary>
/// パラメータのプロパティ一括変更方法を定義する
/// </summary>
public Action<FieldProperty, float> SetAllProp { get; set; }
/// <summary>
/// パラメータのプロパティ一括変更方法が実行されているか
/// </summary>
public bool Inputting { get; set; }
internal FieldProperty(SerializedProperty prop, GUIContent content, string allInputLabel)
{
Prop = prop;
Content = content;
AllInputLabel = allInputLabel;
Inputting = false;
}
}
/// <summary>
/// Transform表示拡張の実体
/// </summary>
[CustomEditor(typeof(Transform)), CanEditMultipleObjects]
internal class TransformInspectorEx : Editor
{
static readonly GUIContent positionGUIContent = new GUIContent(
"Position", "The local position of this Game Object relative to the parent.");
static readonly GUIContent rotationGUIContent = new GUIContent(
"Rotation", "The local rotation of this Game Object relative to the parent.");
static readonly GUIContent scaleGUIContent = new GUIContent(
"Scale", "The local scaling of this Game Object relative to the parent.");
FieldProperty positionField;
FieldProperty scaleField;
TransformRotationGUI rotationGUI;
void OnEnable()
{
// コンパイル時に走るのでエラーが出ないように排除
if (!targets[0])
{
return;
}
// Positionの表示設定
positionField = new FieldProperty(serializedObject.FindProperty("m_LocalPosition"), positionGUIContent, "A");
positionField.ViewProp = (field) => EditorGUILayout.PropertyField(field.Prop, field.Content, GUILayout.Height(16));
positionField.SetAllProp = (field, allSetValue) => field.Prop.vector3Value = Vector3.one * allSetValue;
// Scaleの表示設定
scaleField = new FieldProperty(serializedObject.FindProperty("m_LocalScale"), scaleGUIContent, "A");
scaleField.ViewProp = (field) => EditorGUILayout.PropertyField(field.Prop, field.Content, GUILayout.Height(16));
scaleField.SetAllProp = (field, allSetValue) => field.Prop.vector3Value = Vector3.one * allSetValue;
// Rotationの表示設定
var rotationField = new FieldProperty(serializedObject.FindProperty("m_LocalRotation"), rotationGUIContent, "A");
rotationGUI = new TransformRotationGUI(rotationField);
rotationGUI.FieldProp.ViewProp = (field) => rotationGUI.RotationField(field);
rotationGUI.FieldProp.SetAllProp = (field, allSetValue) =>
{
// Rotationのみ表示と実体が異なるため、手動でUndoの情報を設定する
Undo.RecordObjects(targets, "SetAllInspector");
foreach (Transform trans in targets)
{
TransformUtils.SetInspectorRotation(trans, Vector3.one * allSetValue);
}
// 選択が複数の場合でも正常に適用されるように設定する
field.Prop.serializedObject.SetIsDifferentCacheDirty();
};
}
public override void OnInspectorGUI()
{
if (!EditorGUIUtility.wideMode)
{
// ウィンドウ幅が狭い場合に、二行になるのを防ぐ
EditorGUIUtility.wideMode = true;
EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth - 212;
}
serializedObject.Update();
// Transformの各パラメータ描写の開始
DrawPropField(positionField);
DrawPropField(rotationGUI.FieldProp);
DrawPropField(scaleField);
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// パラメータの描写実行
/// </summary>
/// <param name="allInputLabel">一括設定用ボタンの表示文</param>
/// <param name="fieldProp">表示するパラメータ</param>
void DrawPropField(FieldProperty fieldProp)
{
// 一括入力設定を行うか
var allSetMode = fieldProp as ISetAllMode;
if (allSetMode != null)
{
using (new EditorGUILayout.HorizontalScope())
using (new EditorGUI.DisabledGroupScope(allSetMode.Inputting))
{
// 一括設定フォームのウィンドウ作成
if (GUILayout.Button(allSetMode.AllInputLabel, GUILayout.Width(20), GUILayout.Height(16)))
{
allSetMode.Inputting = true;
OpenInputConfirmWindow(fieldProp, (allSetValue) =>
{
allSetMode.Inputting = false;
serializedObject.Update();
allSetMode.SetAllProp(fieldProp, allSetValue);
serializedObject.ApplyModifiedProperties();
},
() =>
{
allSetMode.Inputting = false;
});
}
if (fieldProp.ViewProp != null)
{
fieldProp.ViewProp(fieldProp);
}
}
}
else
{
if (fieldProp.ViewProp != null)
{
fieldProp.ViewProp(fieldProp);
}
}
}
InputConfirmWindow inputConfirmWindow;
/// <summary>
/// 一括入力用のウィンドウを開く
/// </summary>
/// <param name="fieldProp">一括入力するプロパティ</param>
/// <param name="onComplete">入力完了時に呼び出される</param>
/// <param name="onCancel">入力未完了で終了された場合に呼び出される</param>
void OpenInputConfirmWindow(FieldProperty fieldProp, Action<float> onComplete, Action onCancel)
{
if (inputConfirmWindow)
{
inputConfirmWindow.Close();
}
Vector2 inputConfirmWindowSize = new Vector2(170.0f, 90.0f);
inputConfirmWindow = EditorWindow.GetWindow<InputConfirmWindow>(true, "一括設定");
// サイズ変更不可に設定
inputConfirmWindow.minSize = inputConfirmWindowSize;
inputConfirmWindow.maxSize = inputConfirmWindowSize;
// 入力のコールバックを設定
inputConfirmWindow.Setup(fieldProp, onComplete, onCancel);
// 一括入力ウィンドウの表示位置をいい感じに調整
Vector2 mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
mousePos.x += 30;
mousePos.y += 50;
inputConfirmWindow.position = new Rect(mousePos, inputConfirmWindowSize);
// フォーカスされなくなると即時閉じるウィンドウを作成
inputConfirmWindow.ShowAuxWindow();
}
}
/// <summary>
/// 一括入力の設定を行うウィンドウ
/// </summary>
internal class InputConfirmWindow : EditorWindow
{
FieldProperty fieldProp;
Action<float> onComplete;
Action onCancel;
/// <summary>
/// 入力のコールバックを設定
/// </summary>
/// <param name="fieldProp">一括入力したいパラメータ</param>
/// <param name="onComplete">入力完了時に呼び出される</param>
/// <param name="onCancel">入力未完了で終了された場合に呼び出される</param>
internal void Setup(FieldProperty fieldProp, Action<float> onComplete, Action onCancel)
{
this.fieldProp = fieldProp;
this.onComplete = onComplete;
this.onCancel = onCancel;
}
// GUIにフォーカスを設定するための文字列
const string ForcusField = "Focus";
bool initFocus;
/// <summary>
/// 一括入力の値として引き渡されるフィールド
/// </summary>
float setAllValue = 0.0f;
void OnGUI()
{
// Escキーが押される、コンパイル中、編集データが失われている場合は入力を終了
if (Event.current.keyCode == KeyCode.Escape || EditorApplication.isCompiling || fieldProp == null)
{
if (onCancel != null)
{
onCancel();
}
Close();
return;
}
// 入力確定ショートカットキーの指定
if (Event.current.keyCode == KeyCode.Return)
{
onComplete(setAllValue);
Close();
return;
}
GUILayout.Space(5);
// m_localから始まるので、不要な先頭の7文字を削る
string propName = fieldProp.Prop.name.Substring(7);
GUILayout.Label(string.Format("[{0}] を入力してください", propName));
GUILayout.Space(5);
// 一括入力欄の描写
using (new EditorGUILayout.HorizontalScope())
{
EditorGUIUtility.labelWidth = 60;
GUI.SetNextControlName(ForcusField);
setAllValue = EditorGUILayout.FloatField(propName, setAllValue, GUILayout.Width(160));
GUILayout.FlexibleSpace();
}
GUILayout.Space(5);
using (new GUILayout.HorizontalScope())
{
if (GUILayout.Button("OK", GUILayout.Width(80.0f), GUILayout.Height(30.0f)))
{
onComplete(setAllValue);
Close();
}
if (GUILayout.Button("CANCEL", GUILayout.Width(80.0f), GUILayout.Height(30.0f)))
{
onCancel();
Close();
}
}
// 最初のフォーカスを一括入力欄に設定しておく
if (!initFocus)
{
EditorGUI.FocusTextInControl(ForcusField);
initFocus = true;
}
}
/// <summary>
/// ゲームの実行状態が変化した際に呼ばれる
/// </summary>
/// <param name="state">現在のゲームの実行状態</param>
void OnChangedPlayMode(PlayModeStateChange state)
{
// 実行した瞬間、停止した瞬間に入力を終了させる
if (state == PlayModeStateChange.ExitingEditMode || state == PlayModeStateChange.ExitingPlayMode)
{
if (onCancel != null)
{
onCancel();
}
Close();
}
}
void OnEnable()
{
initFocus = false;
// ゲームの実行状態が変化した際に呼ばれる
EditorApplication.playModeStateChanged += OnChangedPlayMode;
}
void OnDisable()
{
if (onCancel != null)
{
onCancel();
}
EditorApplication.playModeStateChanged -= OnChangedPlayMode;
}
void OnSelectionChange()
{
if (onCancel != null)
{
onCancel();
}
Close();
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
namespace TransformEx
{
/// <summary>
/// Rotationのエディタ表示用処理
/// </summary>
[System.Serializable]
internal class TransformRotationGUI
{
internal FieldProperty FieldProp { get; private set; }
Object[] selectObjects;
internal TransformRotationGUI(FieldProperty fieldProp)
{
FieldProp = fieldProp;
selectObjects = fieldProp.Prop.serializedObject.targetObjects;
}
/// <summary>
/// Rotationの表示処理
/// </summary>
/// <param name="fieldProp">表示するパラメータ</param>
internal void RotationField(FieldProperty fieldProp)
{
var baseTransform = (Transform)selectObjects[0];
var localEulerAngles = TransformUtils.GetInspectorRotation(baseTransform);
foreach (Transform transform in selectObjects)
{
// インスペクタ側の値で比較を行う
if (localEulerAngles != TransformUtils.GetInspectorRotation(transform))
{
// 不一致扱いとして、入力欄にハイフンを表示させる
EditorGUI.showMixedValue = true;
break;
}
}
using (var changeCheck = new EditorGUI.ChangeCheckScope())
{
Vector3 inputEulerAngles = EditorGUILayout.Vector3Field(fieldProp.Content, localEulerAngles);
if (changeCheck.changed)
{
// ローテーションのみ表示と実体が異なるため、手動でUndoの情報を設定する
Undo.RecordObjects(selectObjects, "ChangeInspector");
foreach (Transform transform in selectObjects)
{
TransformUtils.SetInspectorRotation(transform, inputEulerAngles);
}
// 選択が複数の場合でも正常に更新適用がされるように設定する
fieldProp.Prop.serializedObject.SetIsDifferentCacheDirty();
}
}
// 入力欄にハイフンを表示させないように解除
EditorGUI.showMixedValue = false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment