LessonEX エディタ拡張
EX-1 エディタ拡張とは
Unityは便利なエディタですが、作業の効率化のために機能をプログラマー側で拡張したいということもあります。エディタ拡張を行うことでプログラマー以外の職種の人もデータを編集しやすいようにしたり、ケアレスミスを防げるようにしたりすることができます。
Unityにはオリジナルのウィンドウを作成するなどエディタ拡張向けの機能があるため、ここではいくつかの例を通してエディタ拡張を行ってみましょう。今までのスクリプトとは書き方が異なるため、C#とUnityに慣れてきた人向けの内容です。
ちなみに小ネタで解説しているAttributeも分類的にはエディタ拡張の一種になります。こちらは初心者でも使いやすいため、積極的に使っていきましょう。
EX-2 エディタ拡張の準備
エディタ拡張を行う際は事前にEditorフォルダを作っておきましょう。エディタ拡張をするためのファイルはこのフォルダに入れていくことになります。
名前を間違えると認識されないので注意しましょう。

EX-3 インスペクターの拡張
まずはインスペクターの表示内容を拡張してみましょう。Attributeを用いることである程度同じことができますが、こちらのメリットはインスペクター内で関数を実行したりif文の判定を取ったりすることができる点にあります。
拡張元になるスクリプトを作成します。新しいスクリプトPlayerを作成して、以下のように入力してください。このスクリプトはEditorフォルダに入れなくても構いません。
サンプルなのでお好みでパラメータを追加してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// 適当なパラメータ
public string Name; // 名前
public int Level = 0; // レベル
public int DefaultHP = 0; // 初期体力
public int LevelUpHP = 0; // レベルアップで上昇するHP量
public int DefaultATK = 0; // 初期攻撃力
public int LevelUpATK = 0; // レベルアップで上昇する攻撃力
public int DefaultDEF = 0; // 初期防御力
public int LevelUpDEF = 0; // レベルアップで上昇する防御力
}
今回は扱いがわかりやすいように変数をpublicにしていますが、実際の実装ではprivateにしておいた方がよいでしょう。
Playerスクリプトを適当なオブジェクトにアタッチしてみてください。例のごとく変数の中身がインスペクターに表示されています。
ここからエディタ拡張を行っていきましょう。

Editorフォルダ内にPlayerEditorスクリプトを作成してください。

まずはレベルの値が0以下の場合、不正な値として警告を表示するようにしてみましょう。実際の実装でも値をうっかり入れ忘れるということは起こり得るため、入れ忘れていることがわかりやすいようにします。
PlayerEditorスクリプトを開いて、以下のように入力してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; // エディタ拡張をする時に必要
[CustomEditor(typeof(Player))] // 対象となるクラスを指定
public class PlayerEditor : Editor // Editorを継承する必要がある
{
// インスペクターを表示しているときに実行される
public override void OnInspectorGUI()
{
// 開いているスクリプトを引っ張ってくる
Player player = (Player)target;
// 最初にメッセージを表示してみる
GUILayout.Label("【プレイヤー】");
// 通常表示される内容を表示する
base.OnInspectorGUI();
// 値が異常な場合は警告を表示してみる
if (player.Level <= 0)
{
EditorGUILayout.HelpBox("レベルの値が0以下です!", MessageType.Warning);
}
}
}
新しい要素が多いですが、実装自体はシンプルなので順番に見ていきましょう。
【プログラムの解説】
・エディタ拡張を行う場合は最初に using UnityEditor; と記述する必要があるので注意しましょう。
・[CustomEditor(typeof(Player))] はこのスクリプトを適用する対象を指定することができるアトリビュートです。今回は当然Playerクラスを指定しています。
・エディタ拡張を行うクラスはEditorを継承する必要があります。using UnityEditor; と同じく忘れないように注意しましょう。
・OnInspectorGUI 関数はインスペクターの表示内容をカスタマイズしたい時に使用します。
・Editor側で用意されているTargetというパラメータには拡張される側のクラスが入っています。
Player player = (Player)target; のようにキャストすることで、拡張される側のデータを引っ張ってくることが可能です。
※ Player player = target as Player; のように記述することもできます
・GUILayout.Label("表示したい文章"); を使用することでインスペクターに文章を表示することができます。アトリビュートでもHeaderを用いることで似たようなことはできますが、こちらは変数に紐付けられている訳ではなく、単体で動作します。
・base.OnInspectorGUI(); と記述することで、通常のインスペクターで表示される内容をまとめて表示できます。

・EditorGUILayout.HelpBox("文章"); を使うことでヘルプや警告を表示できます。第二引数で種類を指定できます。

この状態でPlayerの項目を確認すると、Levelが0の時に警告が表示されるようになります。Levelの値を1にすると警告が消えることを確認してください。


もしインスペクターに「Multi-object editing not supported.」と表示された場合、別のインスペクターを開いてから再度確認すると反映されます。

Unity Error!

次は項目のリセットボタンを実装してみましょう。
インスペクター横のリセットボタンを押すことでもリセットできますが、インスペクターにボタンが表示されている方がわかりやすいので、今回はリセットボタンを実装してみます。

PlayerEditorスクリプトを開いて、赤い部分のコードを追加してください。
(前略)
// 通常表示される内容を表示する
base.OnInspectorGUI();
// 値が異常な場合は警告を表示してみる
if (player.Level <= 0)
{
EditorGUILayout.HelpBox("レベルの値が0以下です!", MessageType.Warning);
}
// リセットボタンを表示
if (GUILayout.Button("リセット"))
{
// 押された時にパラメータをリセットする
player.Name = "";
player.Level = 1;
player.DefaultHP = 0;
player.LevelUpHP = 0;
player.DefaultATK = 0;
player.LevelUpATK = 0;
player.DefaultDEF = 0;
player.LevelUpDEF = 0;
}
}
}
【プログラムの解説】
・GUILayout.Button("表示内容") を使うことでボタンを表示することができます。押された瞬間にtrueを返すため、if文の中に書くことで「ボタンが押されたときの処理」を記述できます。
ここまで書けたら保存して、適当に項目を入力した後にリセットボタンを押してみましょう。項目がリセットされます。

エディタ拡張を使うことで入力欄を表示することもできます。レベルの値を入力することで、そのレベル時のステータスを計算してみましょう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; // エディタ拡張をする時に必要
[CustomEditor(typeof(Player))] // 対象となるクラスを指定
public class PlayerEditor : Editor // Editorを継承する必要がある
{
int m_sampleLevel = 0; // ステータス計算用レベル
// インスペクターを表示しているときに実行される
public override void OnInspectorGUI()
{
// 開いているスクリプトを引っ張ってくる
Player player = (Player)target;
// 最初にメッセージを表示してみる
GUILayout.Label("【プレイヤー】");
// 通常表示される内容を表示する
base.OnInspectorGUI();
// 値が異常な場合は警告を表示してみる
if (player.Level <= 0)
{
EditorGUILayout.HelpBox("レベルの値が0以下です!", MessageType.Warning);
}
// リセットボタンを表示
if (GUILayout.Button("リセット"))
{
// 押された時にパラメータをリセットする
player.Name = "";
player.Level = 1;
player.DefaultHP = 0;
player.LevelUpHP = 0;
player.DefaultATK = 0;
player.LevelUpATK = 0;
player.DefaultDEF = 0;
player.LevelUpDEF = 0;
}
// 空白
EditorGUILayout.Space();
// 入力欄を表示
m_sampleLevel = EditorGUILayout.IntField("計算用レベル", m_sampleLevel);
// ステータスを計算
int hp = player.DefaultHP + (player.LevelUpHP * m_sampleLevel);
int atk = player.DefaultATK + (player.LevelUpATK * m_sampleLevel);
int def = player.DefaultDEF + (player.LevelUpDEF * m_sampleLevel);
// 計算結果を表示
GUILayout.Label("【推定ステータス】¥nHP:" + hp + "¥nATK:" + atk + "¥nDEF:" + def);
}
}
※ 半角¥を表示できないので全角で表示していますが、実際は半角で入力してください。
【プログラムの解説】
・EditorGUILayout.Space(); と記入することで項目の間に行間を追加することができます。インスペクターを見やすいようにお好みで調整しましょう。
・EditorGUILayout.IntField 関数を使うことでインスペクターにint型の入力欄を表示できます。他にもFloatFieldやColorFieldなど変数の型ごとに様々な関数があります。
第一引数に表示名、第二引数に表示する値を指定して使用します。
これで入力したレベルでの推定ステータスを計算できます。計算用レベルの項目に値を入力して確認してみてください。
このようにエディタ拡張をすることで開発を補助する機能を実装することができます。開発中に「こういう機能があったら効率が上がるのにな」と思ったときはエディタ拡張を行うことを考えてみてください。

EX-4 ウィンドウの作成
先ほどはインスペクターの内容を拡張しましたが、オリジナルのウィンドウを作ることもできます。
まずは文章を表示するだけのウィンドウを表示してみましょう。
Editorフォルダ内に新しいスクリプトTestWindowEditorを作成して、以下のように入力してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; // エディタ拡張をする時に必要
public class TestWindowEditor : EditorWindow // 自作のウィンドウを扱うときに継承
{
// ウィンドウを作成
[MenuItem("Window/TestWindow")]
static void Open()
{
// ウィンドウの名前を変更
GetWindow<TestWindowEditor>("開発補助ツール");
}
// ここにウィンドウのGUI処理を記述
void OnGUI()
{
// 適当にメッセージを表示してみる
EditorGUILayout.LabelField("作業を自動化するウィンドウです");
}
}
【プログラムの解説】
・オリジナルのウィンドウを作成する際は EditorWindowクラスを継承する必要があります。
・[MenuItem("Window/TestWindow")] で、どこにどういった名前でウィンドウを開くメニューを表示するか設定しています。
・OnGUI() 関数内にウィンドウを開いている時の処理を記述します。
後はインスペクターを拡張したときとほぼ同じです。
「Window」→「TestWindow」を開くと、設定した文章が表示されます。このようにUnityでは簡単に自作ウィンドウを作ることができます。


あとはOnGUI関数内で実行したい処理をお好みで記述するだけです。
今回は「指定したオブジェクトを一定間隔で大量に配置する」機能と「指定したタグのオブジェクトを一括削除する」機能を実装してみましょう。
TestWindowEditorスクリプトを開いて、赤い部分のコードを追加してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; // エディタ拡張をする時に必要
public class TestWindowEditor : EditorWindow // 自作のウィンドウを扱うときに継承
{
GameObject m_createObject; // 生成するオブジェクト
Vector3 m_startPos; // 生成地点
Vector3 m_offset; // ずらす座標
int m_createNum = 0; // 生成数
string m_tagName = ""; // 検索対象のタグ
// ウィンドウを作成
[MenuItem("Window/TestWindow")]
static void Open()
{
// ウィンドウの名前を変更
GetWindow<TestWindowEditor>("開発補助ツール");
}
// ここにウィンドウのGUI処理を記述
void OnGUI()
{
// 適当にメッセージを表示してみる
EditorGUILayout.LabelField("作業を自動化するウィンドウです");
// 行間を開ける
EditorGUILayout.Space();
// オブジェクト一括生成
EditorGUILayout.LabelField("【オブジェクト一括生成】");
// パラメータの設定
m_createObject = EditorGUILayout.ObjectField("生成対象", m_createObject,
typeof(GameObject), true) as GameObject;
m_startPos = EditorGUILayout.Vector3Field("生成開始地点", m_startPos);
m_offset = EditorGUILayout.Vector3Field("ずらす座標", m_offset);
m_createNum = EditorGUILayout.IntField("生成数", m_createNum);
// 生成ボタン
if (GUILayout.Button("生成"))
{
// for文で一気に生成
Vector3 pos = m_startPos;
for (int i = 0; i < m_createNum; i++)
{
Instantiate(m_createObject, pos, Quaternion.identity);
pos += m_offset;
}
}
// 行間を開ける
EditorGUILayout.Space();
// タグで検索して削除
EditorGUILayout.LabelField("【タグで検索して削除】");
// タグ指定
m_tagName = EditorGUILayout.TagField("対象のタグ", m_tagName);
// 削除ボタン
if (GUILayout.Button("削除"))
{
// 削除処理
GameObject[] objects = GameObject.FindGameObjectsWithTag(m_tagName);
foreach(GameObject obj in objects){
DestroyImmediate(obj);
}
}
}
}
【プログラムの解説】
・ObjectField関数を使うことで○○Field関数が存在しない型でも入力欄を表示できます。
・エディタ内でオブジェクトを削除する際はDestroy関数ではなくDestroyImmediateを使用してください。
ここまで書けたらウィンドウを開いて、各機能が動作するか確認してみてください。
このようにエディタ拡張をすることで、開発効率を上げる機能を実装することもできます。

紹介しきれなかった関数の中でよく使うものを一部ピックアップして紹介します。
・GUILayout.Toggle
トグル(チェックボックス)を表示します。bool型の値を管理することができます。
・GUILayout.HorizontalSlider
横方向のスライダーを表示します。第二引数に下限、第三引数に上限を指定します。
(縦方向のGUILayout.VerticalSliderもあります)
・EditorGUILayout.Foldout
折り畳みを表示します。クリック時の挙動は上記のGUILayout.Toggleとほぼ同じですが、フラグを使って折り畳みを実装する際に便利です。


Unity Tips!

EX-5 データベースの作成
最後にここまでのまとめとして、大量のモンスターデータを管理するためのデータベース用ウィンドウを作ってみましょう。
ScriptableObjectを用いることで大量のモンスターのパラメータを管理できますが、デフォルトの表示では目的のモンスターデータを見つけたい時に不便な配置になっています。特にデータ数が多い場合は、項目を1つ探すだけでも時間がかかってしまいます(ScriptableObjectについては3D脱出ゲーム編2-2参照)

今回は某RPGのモンスターデータ作成を想定して、モンスターのパラメータを見やすく設定するためのエディタを作ってみましょう。
まずはモンスターデータの定義とデータを格納する場所を作ります。
MonsterDataスクリプトを作成して、以下のように入力してください。スクリプトを作るのはEditorフォルダ内でなくても構いません。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 属性の定義
public enum ElementType
{
enFire, // 火属性
enWater, // 水属性
enWind, // 風属性
enEarth, // 土属性
enLight, // 光属性
enDark // 闇属性
}
[System.Serializable]
public class Monster
{
public string Name; // 名前
public ElementType Element; // 属性
public Sprite Image; // モンスター画像
public int HP; // 体力
public int ATK; // 攻撃力
public int DEF; // 防御力
[Multiline(4)]
public string Explanation; // 説明文
}
[CreateAssetMenu(fileName = "MonsterDataBase", menuName = "CreateMonsterDataBase"),
System.Serializable]
public class MonsterData : ScriptableObject
{
// モンスターリストの可変長配列
public List<Monster> Monsters = new List<Monster>();
}
コードの内容は3D脱出ゲーム編2-2とほとんど変わらないため、解説は省略します。
ただしモンスターデータの定義は構造体ではなくクラスで行ってください。
また、データを保存するためには[System.Serializable] のアトリビュートをつける必要があります。
Createメニュー内に「CreateMonsterDataBase」の項目が追加されているので、クリックして追加してください。


Editorフォルダ内にMonsterDBEditorスクリプトを作成して、以下のように入力してください。
こちらもあくまで一例なので、中身はお好みで変更してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
public class MonsterDBEditor : EditorWindow
{
// 対象データベース
static MonsterData m_monsterData;
// 名前一覧
static List<string> m_nameList = new List<string>();
// スクロール位置
Vector2 m_leftScrollPos = Vector2.zero;
// 選択中No
int m_selectNo = -1;
// 検索欄
SearchField m_searchField;
string m_searchText = "";
// ウィンドウを作成
[MenuItem("Window/MonsterDataBase")]
static void Open()
{
// 読み込み
m_monsterData = AssetDatabase.LoadAssetAtPath<MonsterData>("Assets/MonsterDataBase.asset");
// 名前を変更
GetWindow<MonsterDBEditor>("モンスターデータベース");
// 変更を通知
EditorUtility.SetDirty(m_monsterData);
}
// ここにウィンドウのGUI処理を記述
void OnGUI()
{
// まずは名前一覧を作成
ResetNameList();
// 左右に配置
EditorGUILayout.BeginHorizontal(GUI.skin.box);
{
// 左側
LeftUpdate();
// 右側
RightUpdate();
}
EditorGUILayout.EndHorizontal();
}
void LeftUpdate()
{
// 名前リストのサイズを指定
EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(160), GUILayout.Height(400));
{
// 検索欄
m_searchField ??= new SearchField();
GUILayout.Label("名前検索");
m_searchText = m_searchField.OnToolbarGUI(m_searchText);
// 検索処理
Search();
// 左側のスクロールビュー
m_leftScrollPos = EditorGUILayout.BeginScrollView(m_leftScrollPos, GUI.skin.box);
{
// データリスト
for (int i = 0; i < m_nameList.Count; i++)
{
// 色変更
if (m_selectNo == i)
{
GUI.backgroundColor = Color.cyan;
}
else
{
GUI.backgroundColor = Color.white;
}
// ボタンが押されたときの処理
if (GUILayout.Button(i + ":" + m_nameList[i]))
{
// 対象変更
m_selectNo = i;
GUI.FocusControl("");
Repaint();
}
}
// 色を戻す
GUI.backgroundColor = Color.white;
}
EditorGUILayout.EndScrollView();
// 項目操作ボタン
EditorGUILayout.BeginHorizontal();
{
if (GUILayout.Button("追加", EditorStyles.miniButtonLeft))
{
AddData();
}
if (GUILayout.Button("削除", EditorStyles.miniButtonRight))
{
DeleteData();
}
}
EditorGUILayout.EndHorizontal();
// 項目数
GUILayout.Label("項目数:" + m_nameList.Count);
}
EditorGUILayout.EndVertical();
}
void RightUpdate()
{
// 何も選んでいないなら非表示
if (m_selectNo < 0)
{
return;
}
// 右側を更新
EditorGUILayout.BeginVertical(GUI.skin.box);
{
// 基礎情報を表示
GUILayout.Label("ID:" + m_selectNo + " Name:" + m_nameList[m_selectNo]);
// 空白
EditorGUILayout.Space();
// 設定欄を表示
m_monsterData.Monsters[m_selectNo].Name =
EditorGUILayout.TextField("名前", m_monsterData.Monsters[m_selectNo].Name);
// 列挙型を文字列に置き換えられる
m_monsterData.Monsters[m_selectNo].Element = (ElementType)EditorGUILayout.Popup(
"属性",
(int)m_monsterData.Monsters[m_selectNo].Element,
new string[] { "火属性", "水属性", "風属性", "土属性", "光属性", "闇属性" }
);
m_monsterData.Monsters[m_selectNo].Image =
EditorGUILayout.ObjectField("画像", m_monsterData.Monsters[m_selectNo].Image,
typeof(Sprite), true) as Sprite;
// 空白
EditorGUILayout.Space();
// ステータス欄
m_monsterData.Monsters[m_selectNo].HP =
EditorGUILayout.IntField("体力", m_monsterData.Monsters[m_selectNo].HP);
m_monsterData.Monsters[m_selectNo].ATK =
EditorGUILayout.IntField("攻撃力", m_monsterData.Monsters[m_selectNo].ATK);
m_monsterData.Monsters[m_selectNo].DEF =
EditorGUILayout.IntField("防御力", m_monsterData.Monsters[m_selectNo].DEF);
// 空白
EditorGUILayout.Space();
GUILayout.Label("図鑑説明");
m_monsterData.Monsters[m_selectNo].Explanation =
EditorGUILayout.TextArea(m_monsterData.Monsters[m_selectNo].Explanation);
// 値が異常な場合は警告を表示してみる
if (m_monsterData.Monsters[m_selectNo].HP <= 0)
{
EditorGUILayout.HelpBox("初期体力が0以下です!", MessageType.Warning);
}
}
EditorGUILayout.EndVertical();
// 保存
Undo.RegisterCompleteObjectUndo(m_monsterData, "monsterData");
}
// 名前一覧の作成
static void ResetNameList()
{
// 初期化
m_nameList.Clear();
// 名前を入れていく
foreach (Monster monster in m_monsterData.Monsters)
{
m_nameList.Add(monster.Name);
}
}
// 検索
void Search()
{
if (m_searchText == "")
{
return;
}
// 初期化
int startNum = m_selectNo;
startNum = Mathf.Max(startNum, 0);
for (int i = startNum; i < m_nameList.Count; i++)
{
// 文字列が含まれるかチェック
if (m_nameList[i].Contains(m_searchText))
{
// 終了
m_selectNo = i;
GUI.FocusControl("");
Repaint();
return;
}
}
// ヒットしない場合は-1にしておく
m_selectNo = -1;
}
// データの追加
void AddData()
{
Monster newMonster = new Monster();
// 追加
m_monsterData.Monsters.Add(newMonster);
}
// データの削除
void DeleteData()
{
if (m_selectNo == -1)
{
// 削除できない
return;
}
// 選択位置のデータを削除
m_monsterData.Monsters.Remove(m_monsterData.Monsters[m_selectNo]);
// 調整
m_selectNo -= 1;
m_selectNo = Mathf.Max(m_selectNo, 0);
}
}
要素が多いため、一部の解説は省略します。
【プログラムの解説】
・AssetDatabase.LoadAssetAtPath<MonsterData>("Assets/MonsterDataBase.asset");
では、ファイルパスからモンスターデータを読み込んでいます。
この機能はUnityEditorの機能なので、エディタ上でしか動作しません。
・EditorGUILayout.BeginHorizontal(); と
EditorGUILayout.EndHorizontal(); で囲んだ部分は要素が横に並ぶようになります。
必ずしも間を中かっこで囲う必要はありませんが、サンプルでは範囲がわかりやすいように中かっこで囲んでいます。


引数に GUI.skin.box を追加することで範囲を枠で囲んだり、GUILayout.Width(160) 等で範囲を指定することもできます。
BeginVertical とEndVertical は左右ではなく上下に要素を並べることができる関数で、同じように使うことができます。
また、EditorGUILayout.BeginScrollView(); と EditorGUILayout.EndScrollView();でスクロールできる領域を表示することもできます。スクロール位置を管理するためのVector2型の変数が必要です(サンプルではm_leftScrollPos)
・m_searchField ??= new SearchField(); の??= はNull合体代入演算子と呼ばれるもので、左辺がnullの時のみ右辺の値を代入するというものです(参照)

・SearchField は検索欄になります。using UnityEditor.IMGUI.Controls; が必要なので注意しましょう。また、検索欄が表示されるだけなので実際の検索処理は別で書く必要があります。
・Repaint関数はウィンドウの描画を全て更新する関数です。
変更がウィンドウに反映されないときに実行すると良いでしょう。
新しい要素は多いですが、実際に使ってみると挙動がわかりやすいと思います。
実際に「Window」→「MonsterDataBase」を開いて確認してみましょう。左側のデフォルトの配置より、右側のオリジナルのウィンドウの方が一目でデータを確認しやすいと思います。


おまけとして、エディタ拡張でレーダーチャートを描画してみましょう。
Handlesクラスを使うことで、エディタ拡張内で線や円を描画することができます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; // エディタ拡張をする時に必要
public class TestWindowEditor : EditorWindow // 自作のウィンドウを扱うときに継承
{
// 適当なステータス
int HP = 100;
int MP = 70;
int ATK = 95;
int DEF = 60;
int SPD = 85;
int LUK = 50;
// ウィンドウを作成
[MenuItem("Window/Graph")]
static void Open()
{
// 名前を変更
GetWindow<Test>("グラフテスト");
}
private void OnGUI()
{
// ステータスの配列を作成
List<int> status = new();
status.Add(HP);
status.Add(MP);
status.Add(ATK);
status.Add(DEF);
status.Add(SPD);
status.Add(LUK);
// 中心座標を指定
Vector2 center = new(200.0f, 200.0f);
// 半径を指定
float radius = 1.0f;
// 土台を描画する
Handles.DrawWireDisc(center, Vector3.forward, 100.0f);
Handles.DrawSolidDisc(center, Vector3.forward, 4.0f);
// グラフの角度を計算
float angleIncrement = 360.0f / status.Count;
// 頂点を格納する配列を作成
Vector3[] points = new Vector3[status.Count + 1];
// 頂点座標の計算
for (int i = 0; i < status.Count; i++)
{
float angle = i * angleIncrement;
float value = status[i];
// 各頂点の位置を計算
float x = center.x + Mathf.Cos(Mathf.Deg2Rad * angle) * radius * value;
float y = center.y + Mathf.Sin(Mathf.Deg2Rad * angle) * radius * value;
// 座標を設定
points[i] = new Vector3(x, y, 0);
// ラインを引く
float x2 = center.x + Mathf.Cos(Mathf.Deg2Rad * angle) * radius * 100;
float y2 = center.y + Mathf.Sin(Mathf.Deg2Rad * angle) * radius * 100;
Handles.DrawLine(center, new Vector3(x2, y2));
}
// レーダーチャートの色を設定
Handles.color = Color.blue;
// グラフが一周するように末尾に先頭の値を代入
points[status.Count] = points[0];
// レーダーチャートを描画する
Handles.DrawPolyLine(points);
// 装飾する
GUI.contentColor = Color.red;
for(int i = 0; i < points.Length -1 ; i++)
{
// 頂点ごとの丸を描画
Handles.DrawSolidDisc(points[i], Vector3.forward, 4.0f);
// 値を表示
GUI.Label(new Rect(points[i].x, points[i].y, 40, 20), "" + status[i]);
}
}
}
【スクリプトの解説】
・Handles.DrawWireDisc 関数で、任意の円を描画することができます。
今回はレーダーチャートの外枠に使用しています。
・Handles.DrawSolidDisc 関数で、任意の●を描画することができます。
今回はレーダーチャートの中央と各頂点に使用しています。
・Handles.DrawLine 関数で、第一引数と第二引数の座標を繋ぐ線を描画できます。
・頂点座標の計算には極座標系を用いています。
極座標系は今まで使ってきたデカルト座標系(x軸とy軸)とは異なり、半径と角度で位置を指定します。
極座標系からデカルト座標系へ変換を行うことでグラフの頂点を求めています。
X座標 = 基点のX座標 + Mathf.Cos(Mathf.Deg2Rad * 角度) * 半径 * 対象の値;
Y座標 = 基点のY座標 + Mathf.Sin(Mathf.Deg2Rad * 角度) * 半径 * 対象の値;
※ 極座標系の話は無理に理解しなくても問題ありません。
・Handles.DrawPolyLine 関数で、引数に渡された座標のリストを繋ぐ多角形を描画できます。
これによってレーダーチャートを作成することができます。

このTIPSで知ってほしいことはレーダーチャートの作り方というより「エディタ拡張では色々な表現ができる」ということです。ぜひ色々な機能を試して便利なエディタを作ってみてください。
Unity Tips!

大量のデータを扱うゲームを作る際にはぜひエディタ拡張を行ってみてください。エディタ拡張には他にも色々な関数があるため、各自で調べて色々な機能を増やしてみましょう。
今回は省略しましたが自作ウィンドウだけでなくヒエラルキーを拡張して視認性を上げたり、シーンビューにボタンを追加して作業効率を上げたりと、様々なことができます。
エディタ拡張は開発を効率化できますが、メインの開発が進むわけではないため筆者のようにエディタ拡張に夢中になってメインの開発を疎かにしないようにしましょう。
【エディタ拡張まとめ①】https://baba-s.hatenablog.com/entry/2016/06/28/100000
【エディタ拡張まとめ②】https://caitsithware.com/wordpress/archives/1377
【実際の現場での活用①】https://gamemakers.jp/article/2023_09_06_48727/
【実際の現場での活用②】https://www.youtube.com/watch?v=P4AIgRtFM4A