Unityでインスペクタに表示する変数のヘルプをTooltipを使って書くことがあると思います。
これあまり見ないし、そもそも設定されているかどうかわかりづらすぎませんか?
という不満が前々からあったので独自のTooltipを軽く作ってみました。
作ったもの
こんな感じのものを作りました。
辞書的なTooltip作ってみてる。
— Haruma:K (@harumak_11) 2018年8月9日
どうせドキュメントなんて書いてもみんな読まないからクリックしたらわかるようにしたいのだ! pic.twitter.com/hoMat1Ca61
その変数のヘルプのほか、関連するヘルプがあればリンクが表示されます。
シャドウバースのゲーム中に出るヘルプが非常に好きなのでそんなイメージで作りました。
使い方
1.上部メニューのBalloon Help > Create SourceからデータソースのScriptableObjectを生成します
2.データを入力します。説明文中に[他のHelpのID]と表記すると他のHelpにリンクされます
3.BalloonHelpアトリビュートをつけたシリアライズフィールドを定義します
[BalloonHelp("actor_prefab_id"), SerializeField] private int _actorPrefabId;
これだけで、あとはいい感じに表示してくれます。
ソースコード(Window)
まずデータソースのScriptableObjectから。
#if UNITY_EDITOR using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; public class BalloonHelpSource : ScriptableObject, ISerializationCallbackReceiver { /// <summary> /// ヘルプ内容 /// </summary> [System.Serializable] public class Help { public string id; public string title; [TextArea(5, 50)] public string description; [System.NonSerialized] public List<string> relationIds = new List<string>(); } /// <summary> /// シングルトン /// </summary> public static BalloonHelpSource Instance { get { if (_instance == null) { _instance = AssetDatabase.FindAssets("t:" + typeof(BalloonHelpSource).Name) .Select(guid => AssetDatabase.LoadAssetAtPath<BalloonHelpSource>(AssetDatabase.GUIDToAssetPath(guid))) .FirstOrDefault(); } return _instance; } } private static BalloonHelpSource _instance = null; [SerializeField] private List<Help> _helps = new List<Help>(); private Dictionary<string, Help> _helpMap = new Dictionary<string, Help>(); /// <summary> /// Helpを取得する /// </summary> public Help GetHelp(string id, bool createRelations = true) { // Helpを取得 / 存在しなかったらnullを返す if (!_helpMap.ContainsKey(id)){ return null; } var ret = new Help(); var help = _helpMap[id]; ret.id = help.id; ret.title = help.title; ret.description = help.description; // Relationsの構築 if (createRelations) { var matches = Regex.Matches(help.description, @"\[[a-z0-9A-Z._]*\]"); foreach (Match matche in matches) { var replaceTarget = matche.Value; var replaceId = replaceTarget.Replace("[", "").Replace("]", ""); var replaceHelp = GetHelp(replaceId, false); if (replaceHelp == null) { continue; } ret.description = ret.description.Replace(replaceTarget, replaceHelp.title); ret.relationIds.Add(replaceHelp.id); } } return ret; } /// <summary> /// シリアライズ前の処理 /// </summary> public void OnBeforeSerialize() { } /// <summary> /// デシリアライズ後の処理 /// </summary> public void OnAfterDeserialize() { // _helpMapを初期化する for (int i = 0; i < _helps.Count; i++){ var help = _helps[i]; if (_helpMap.ContainsKey(help.id)){ _helpMap[help.id] = help; } else { _helpMap.Add(help.id, help); } } } /// <summary> /// インスタンスを生成する /// </summary> [MenuItem("Balloon Help/Create Source")] public static void CreateInstance() { var instance = AssetDatabase.FindAssets("t:" + typeof(BalloonHelpSource).Name) .Select(guid => AssetDatabase.LoadAssetAtPath<BalloonHelpSource>(AssetDatabase.GUIDToAssetPath(guid))) .FirstOrDefault(); if (instance != null){ // すでに存在する場合は作らない Selection.activeObject = instance; EditorUtility.DisplayDialog("確認", "既に作成されています\n" + AssetDatabase.GetAssetPath(instance), "OK"); return; } // ファイル保存パネルを表示して保存 var path = "Assets"; string ext = "asset"; var fileName = "name." + ext; fileName = System.IO.Path.GetFileNameWithoutExtension(AssetDatabase.GenerateUniqueAssetPath(System.IO.Path.Combine(path, fileName))); path = EditorUtility.SaveFilePanelInProject("Save Balloon Help Source", fileName, ext, "", path); if (!string.IsNullOrEmpty(path)) { instance = CreateInstance<BalloonHelpSource>(); AssetDatabase.CreateAsset(instance, path); AssetDatabase.Refresh(); } } /// <summary> /// インスタンスを選択する /// </summary> [MenuItem("Balloon Help/Select Source")] public static void SelectInstance() { var instance = AssetDatabase.FindAssets("t:" + typeof(BalloonHelpSource).Name) .Select(guid => AssetDatabase.LoadAssetAtPath<BalloonHelpSource>(AssetDatabase.GUIDToAssetPath(guid))) .FirstOrDefault(); if (instance != null){ Selection.activeObject = instance; return; } else { EditorUtility.DisplayDialog("確認", "生成済みのSourceがありません。" + AssetDatabase.GetAssetPath(instance), "OK"); return; } } } #endif
説明はコメントに書いてある通りです。
またViewにはPopupWindowContentを使っています。
この部分のソースコードを示します。
#if UNITY_EDITOR using UnityEngine; using UnityEditor; public class BalloonHelp : PopupWindowContent { private const float WINDOW_WIDTH = 300.0f; private const float WINDOW_PADDING = 8.0f; private const string RELATION_TEXT = "Relations"; private static GUIStyle _contentLabelStyle; private static GUIStyle _titleLabelStyle; private static GUIStyle _relationLabelStyle; private BalloonHelpSource.Help _help; private Vector2 _windowSize; public static void Show(string id, Vector2 position) { var rect = new Rect(position, Vector2.zero); var content = new BalloonHelp(id); PopupWindow.Show(rect, content); } /// <summary> /// コンストラクタ /// </summary> private BalloonHelp(string id) { // Sourceが生成されていなかったらサイズだけ決める if (BalloonHelpSource.Instance == null) { _windowSize = new Vector2(WINDOW_WIDTH, 100); return; } // データソースを取得する _help = BalloonHelpSource.Instance.GetHelp(id); // Sourceが生成されていなかったらサイズだけ決める if (_help == null) { _windowSize = new Vector2(WINDOW_WIDTH, 100); return; } // Styleを定義する _contentLabelStyle = new GUIStyle(EditorStyles.label); _contentLabelStyle.wordWrap = true; _titleLabelStyle = new GUIStyle(EditorStyles.boldLabel); _titleLabelStyle.wordWrap = true; _relationLabelStyle = new GUIStyle(EditorStyles.label); _relationLabelStyle.wordWrap = true; _relationLabelStyle.normal.textColor = new Color(0.4f, 0.4f, 1.0f); // ウィンドウサイズを計算する var labelWidth = WINDOW_WIDTH - (WINDOW_PADDING * 2); _windowSize = Vector2.zero; _windowSize.x = WINDOW_WIDTH; _windowSize.y += _titleLabelStyle.CalcHeight(new GUIContent(_help.title), labelWidth); _windowSize.y += 4; _windowSize.y += _contentLabelStyle.CalcHeight(new GUIContent(_help.description), labelWidth); if (_help.relationIds.Count >= 1) { _windowSize.y += 4; _windowSize.y += 8; // EditorGUILayout.Space(); _windowSize.y += 4; _windowSize.y += _contentLabelStyle.CalcHeight(new GUIContent(RELATION_TEXT), labelWidth); _windowSize.y += 4; for (int i = 0; i < _help.relationIds.Count; i++) { _windowSize.y += _relationLabelStyle.CalcHeight(new GUIContent(_help.relationIds[i]), labelWidth); } } _windowSize.y += WINDOW_PADDING * 2; } public override Vector2 GetWindowSize() { return _windowSize; } public override void OnGUI(Rect rect) { // 余白を定義する using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(WINDOW_PADDING); using (new EditorGUILayout.VerticalScope()) { GUILayout.Space(WINDOW_PADDING); if (BalloonHelpSource.Instance == null) { // BalloonHelpSourceが無いエラーを表示する DrawSourceNotFoundError(); } else if (_help == null) { // Helpが無いエラーを表示する DrawHelpNotFoundError(); } else { // 内容を表示する DrawContent(); } } } } /// <summary> /// ウィンドウの中身を描画する /// </summary> private void DrawContent() { // 説明文を描画する EditorGUILayout.LabelField(_help.title, _titleLabelStyle); EditorGUILayout.LabelField(_help.description, _contentLabelStyle); // 関連リンクを描画する if (_help.relationIds.Count >= 1) { EditorGUILayout.Space(); EditorGUILayout.LabelField(RELATION_TEXT, _titleLabelStyle); for (int i = 0; i < _help.relationIds.Count; i++) { var relationId = _help.relationIds[i]; var relationSource = BalloonHelpSource.Instance.GetHelp(relationId); var relationTitle = relationSource == null ? relationId : relationSource.title; if (GUILayout.Button(relationTitle, _relationLabelStyle)) { var mouseRect = new Rect(Event.current.mousePosition, Vector2.one); var content = new BalloonHelp(relationId); PopupWindow.Show(mouseRect, content); } } } } /// <summary> /// Sourceが見つからないエラーを表示 /// </summary> private void DrawSourceNotFoundError() { EditorGUILayout.LabelField("BalloonHelpSourceが作成されていません。"); if (GUILayout.Button("作成する")) { BalloonHelpSource.CreateInstance(); } } /// <summary> /// Helpが見つからないエラーを表示 /// </summary> private void DrawHelpNotFoundError() { EditorGUILayout.LabelField("Helpが定義されていません。"); if (GUILayout.Button("編集する")) { BalloonHelpSource.SelectInstance(); } } } #endif
ここまででBalloonHelp.Show()でウィンドウが開かれるようになりました。
ソースコード(アトリビュート)
直接BalloonHelp.Show()で呼び出してもいいですが、アトリビュートで簡単に呼び出せるようにして見ます。
using UnityEngine; public class BalloonHelpAttribute : PropertyAttribute { public string helpId; public BalloonHelpAttribute(string helpId) { this.helpId = helpId; } }
PropertyAttributeにはデータソースのIDを保持しておきます。
次にこのアトリビュートのPropertyDrawerを書きます。
#if UNITY_EDITOR using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(BalloonHelpAttribute))] public class BalloonHelpAttributeDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var attr = attribute as BalloonHelpAttribute; var style = new GUIStyle(EditorStyles.label); style.normal.textColor = new Color(0.4f, 0.4f, 1.0f); // ラベルを描画する var propertyRect = EditorGUI.PrefixLabel(position, label, style); position.xMax = propertyRect.xMin; if (position.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseUp && Event.current.button == 0) { BalloonHelp.Show(attr.helpId, Event.current.mousePosition); } // プロパティフィールドを描画する var preIndent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; EditorGUI.PropertyField(propertyRect, property, GUIContent.none, true); EditorGUI.indentLevel = preIndent; } public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { return EditorGUI.GetPropertyHeight(property); } } #endif
基本的にそのまま描画していますが、ラベル部分の色を変えて、
押下したらBalloonHelp.Show()を呼ぶ処理を加えています。
結果