Unityでスクリプトエディターを作成する

遅かれ早かれ、Unityで働くすべての人がカスタムツールを作成するようになります。 あなたは長い間抵抗し、恐れることができますが、チームのニーズに合わせて編集者や検査官がいなくなると、前進することは不可能になります。



私は非常に才能のあるアーティストのプロジェクトに参加し、レトロなピクセルアートスタイルのクエストゲームの開発を支援しています。 この環境での開発には長い歴史があるため、Unityを使用しています。 ほとんどすぐに、一連のイベントを作成し、シーンとパズルをカットする必要が生じました。その間、一連のアクションが厳密に定義されています。 最初は可能な限り少ない血を取り除き、Unity 5の標準のAnimator ControllerとStateMachineBehaviourクラスを使用してイベントをカスタマイズすることを提案しましたが、このアプローチは機能しないことが判明しました:アニメーターの有限状態マシンは、普遍的ですが、絶対的に線形なもののために過度の不必要なアクションを必要とします、同様の視覚的ソリューションが必要でしたが、ビデオエディタのタイムラインのようにイベントを簡単かつ簡単に配置できるようにしました。





独自のエディターの作成に影響を与えたUnityドキュメントの画像



したがって、独自の本格的なエディターを作成することは避けられないことが判明しました。





その瞬間まで、私はMonoBehaviourクラスのインスペクターのみを作成しました。 私の意見では、Unityがエディターインターフェイスに使用するアプローチはやや面倒なので、タイムラインを使用してウィンドウ全体を記述するときに何が起こるかを非常に恐れていました。 最後に、何が起こったのか:はい、面倒ですが、大丈夫です、目と意識がそれに慣れます。



そのため、タスクはすぐに2つに簡単に分割できます。スクリプトシステムの基礎とインターフェイスそのものです。



スクリプトシステム





作業のロジックは単純です。各シナリオについて、厳密に定義された時間に開始および終了するイベントのリストを決定する必要があります。 これらのアクションを定義する場合、それらを保存する方法は? 組み込みのUnity MonoBehaviourクラスは、サポートされているフィールドを自動的にシリアル化しますが、それを機能させるには、スクリプトをシーン内のアクティブオブジェクトに割り当てる必要があります。 これはスクリプトクラスには適していますが、アクションには適していません。抽象エンティティごとに、階層内に実際のオブジェクトを作成する必要があります。 私たちの目的のために、UnityにはScriptableObjectクラスがあり、そのライフサイクルはMonoBehaviourに似ていますが、いくつかの制限があります。最も重要なことは、シーンにオブジェクトが存在してコードを実行する必要がないことです。



スクリプトクラスが行うことは、 コルーチンを起動することだけです。 コルーチンは 、各フレームで、経過した時間と、誰が今開始、更新、または停止する必要があるかをチェックします。 メインメソッドを次に示します(最後の完全なソースコードへのリンク)。



Scenario.cs
private IEnumerator ExecuteScenario() { Debug.Log("[EventSystem] Started execution of " + gameObject.name); _time = 0f; var totalDuration = _actions.Any () ? _actions.Max (action => action.EndTime) : 0f; var isPlaying = true; while (isPlaying) { for (var i = 0; i < _actions.Count; i++) { var action = _actions.ElementAt(i); if (_time >= action.StartTime && _time < action.EndTime) { if (action.NowPlaying) action.ActionUpdate(ref _time); //       // ,      ""  else action.ActionStart(_time); } else if (_time >= action.EndTime) { if (!action.NowPlaying) continue; action.Stop(); } } if(_time >= totalDuration) isPlaying = false; _time += Time.deltaTime; yield return null; } foreach (var eventAction in _actions.Where(eventAction => eventAction.NowPlaying)) eventAction.Stop(); //       -      _coroutine = null; if(_callback != null) //    -      _callback(); Debug.Log("[EventSystem] Finished executing " + gameObject.name); _canPlay = !PlayOnce; }
      
      









EventActionについて、3つの重要なイベントを定義しました:「人生の始まり」、「間の瞬間」(各フレームが呼び出されます)、および「終わり」。 アクション自体に応じて、たとえば、「最初にカメラの向きを合わせる」、「アクションが行われている間に位置を更新する」、「最後にプレーヤーに制御を戻す」など、これまたは必要な場合があります。 独自のアクションを作成するには、後継クラスの対応するメソッドをオーバーライドするだけで十分です。



EventAction.cs
 using System; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace Visc { public abstract class EventAction : ScriptableObject { //      [SerializeField] protected string _description; [SerializeField] protected GameObject _actor; [SerializeField] protected float _startTime; [SerializeField] protected float _duration = 1f; public GameObject Actor { get { return _actor; } } public string Description { get { return _description; } } public float StartTime { get { return _startTime; } set { _startTime = value >= 0f ? value : 0f; } } public float Duration { get { return _duration; } set { _duration = value >= 0.1f ? value : 0.1f; } } public float EndTime { get { return _startTime + _duration; } } public bool NowPlaying { get; protected set; } public void ActionStart(float starTime) { Debug.Log("[EventSystem] Started event " + _description); NowPlaying = true; OnStart(starTime); } public void ActionUpdate(ref float timeSinceActionStart) { OnUpdate(ref timeSinceActionStart); } public void Stop() { Debug.Log("[EventSystem] Finished event " + _description); NowPlaying = false; OnStop(); } //       //         protected virtual void OnEditorGui() { } protected virtual void OnStart(float startTime) { } protected virtual void OnUpdate(ref float currentTime) { } protected virtual void OnStop() { } } }
      
      









シンプルなオーバーで、今では楽しい部分です。



スクリプトエディターインターフェイス





古いUnityインターフェイスシステムは、GUIエディター(カスタムインスペクターとウィンドウ)に引き続き存在し、次のように動作します:特定のイベント(マウスクリック、データ更新、明示的な呼び出しRepaint())が発生すると、ユーザークラスの特別なメソッドが呼び出され、呼び出しが行われますインターフェース要素の描画。 標準要素はウィンドウに自動的に配置できます。これらはすべてGUILayoutクラスとEditorGUILayoutクラスにあり、単純なスクリプトプロパティと視覚設定に使用しました。



基本的なパラメーター



独自のエディターウィンドウを作成するには、EditorWindowを継承し、OnGUI()メソッドを定義する必要があります。



ScenarioEditorWindow.cs
 private void OnGUI() { if (CurrentScenario != null) { //    -     GUILayout.BeginHorizontal(); if(Application.isPlaying) if(GUILayout.Button("PLAY")) _currentScenario.Execute(); GUILayout.BeginHorizontal(); //       CurrentScenario.VisibleScale = EditorGUILayout.Slider("Scale", CurrentScenario.VisibleScale, 0.1f, 100f); CurrentScenario.MaximumDuration = EditorGUILayout.FloatField("Max duration (seconds)", CurrentScenario.MaximumDuration); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); CurrentScenario.MaximumTracks = EditorGUILayout.IntField("Max tracks", CurrentScenario.MaximumTracks); BoxHeight = EditorGUILayout.IntSlider("Track height", BoxHeight, 20, 50); if (_draggedAction == null) { var newVisibleDuration = CurrentScenario.MaximumDuration/CurrentScenario.VisibleScale; var newScale = newVisibleDuration*CurrentScenario.VisibleScale/_visibleDuration; _visibleDuration = newVisibleDuration; CurrentScenario.VisibleScale = newScale; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); CurrentScenario.PlayOnce = EditorGUILayout.Toggle("Play once", CurrentScenario.PlayOnce); GUILayout.EndHorizontal(); if (GUILayout.Button("Save")) EditorSceneManager.MarkAllScenesDirty(); GUILayout.EndHorizontal(); } else { _eventActionTypes = null; GUILayout.Label("Select scenario"); } {
      
      









しかし、要素の基本ライブラリには、複数のトラックに存在してサイズを変更できるドラッグ可能なボックス(GUI.Windowがありますが、これは正しくありません)は必要ありません。 したがって、手動で行う必要がありました。つまり、アクションに対応する長方形を自分で描画します。次に例を示します。



 //        if(action.EditingTrack < _trackOffset || action.EditingTrack >= _trackOffset + maxVisibleTracks) continue; var horizontalPosStart = position.width * (action.StartTime / duration) - hOffset; var horizontalPosEnd = position.width * (action.EndTime / duration) - hOffset; var width = horizontalPosEnd - horizontalPosStart; //   var boxRect = new Rect (horizontalPosStart + HandleWidth, offset + BoxHeight * (action.EditingTrack - _trackOffset), width - HandleWidth * 2, BoxHeight); EditorGUIUtility.AddCursorRect (boxRect, MouseCursor.Pan); //   ,    "" var boxStartHandleRect = new Rect (horizontalPosStart, offset + BoxHeight * (action.EditingTrack - _trackOffset), HandleWidth, BoxHeight); EditorGUIUtility.AddCursorRect (boxStartHandleRect, MouseCursor.ResizeHorizontal); GUI.Box (boxStartHandleRect, "<"); //   var boxEndHandleRect = new Rect (horizontalPosEnd - HandleWidth, offset + BoxHeight * (action.EditingTrack - _trackOffset), HandleWidth, BoxHeight); EditorGUIUtility.AddCursorRect (boxEndHandleRect, MouseCursor.ResizeHorizontal); GUI.Box (boxEndHandleRect, ">"); //  , ,  ,      action.DrawTimelineGui (boxRect);
      
      







このコードは、次のようなボックスを描画します。



オブジェクト移動イベント



Unityでは、クリックしたボタン(Event.current.type == EventType.MouseDown && Event.current.button == 0)を判別したり、カーソルが長方形内にあるかどうかを調べたり(Rect.Contains(Event.current.mousePosition))、処理を禁止することもできます。ボタンはコードに沿ってこのフレームでさらにクリックします(Event.current.Use())。 これらの標準ツールを使用して、相互作用を実装しました。イベントをドラッグアンドドロップし、複数を一度に選択して、長さを変更できます。 ユーザーがボックスをクリックまたは移動すると、対応するアクションのパラメーターが実際に変更され、それらの結果としてインターフェースが再描画されます。 アクションを右クリックすると、追加または削除できます。ダブルクリックすると、編集ウィンドウが開きます。



各アクションのインターフェースはどこから来ますか? EventActionに、エディターのみに関連する2つの仮想メソッドを追加しました。OnEditorGui()およびOnDrawTimelineGui()-アクションの編集時にインターフェースを定義し、エディターのタイムラインに表示することさえできます。



このプロジェクトでは、キャラクターダイアログを表示するアクション、メインキャラクターの目標を設定するアクション、または特別なアニメーションをトリガーするアクション、またはカメラの動作を制御できるEventActionを作成しました。プレーヤーの追跡、オブジェクトの中心、センタリングをオフにします。



CameraTargetControl.cs
 #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; namespace Platformer { public class CameraTargetControl : EventAction { [SerializeField] private bool _turnOffTargetingAtStart; [SerializeField] private bool _turnOnTargetingAtEnd; [SerializeField] private bool _targetActorInstedOfPlayerAtStart; [SerializeField] private bool _targetPlayerInTheEnd; protected override void OnStart(float startTime) { if(_turnOffTargetingAtStart) GameManager.CameraController.SetTarget(null); else if (_targetActorInstedOfPlayerAtStart) GameManager.CameraController.SetTarget(_actor.transform); } protected override void OnStop() { if(_turnOnTargetingAtEnd || _targetPlayerInTheEnd) GameManager.CameraController.SetTarget(GameManager.PlayerController.transform); } #if UNITY_EDITOR protected override void OnEditorGui() { _turnOffTargetingAtStart = EditorGUILayout.Toggle("Camera targeting off", _turnOffTargetingAtStart); if (_turnOffTargetingAtStart) _turnOnTargetingAtEnd = EditorGUILayout.Toggle("Targeting on in the end", _turnOnTargetingAtEnd); else { _turnOnTargetingAtEnd = false; _targetActorInstedOfPlayerAtStart = EditorGUILayout.Toggle("Target actor", _targetActorInstedOfPlayerAtStart); if (_targetActorInstedOfPlayerAtStart) _targetPlayerInTheEnd = EditorGUILayout.Toggle("Target player in the end", _targetPlayerInTheEnd); } } #endif } }
      
      









最後に何が起こったのですか?









既知の問題



シナリオとEventActionは独立したエンティティであるため、スクリプトを複製してそのシリアル化されたプロパティをコンパイルすると、既存のアクションへのリンクは新しいスクリプトに分類されます。 スクリプトとアクションの関係を保存することでこの状況を修正する予定ですが、今のところは考えています。



おわりに



主な目標は達成されたと思います。 このプロジェクトはまだ進んでおり、バグの修正と修正は進んでいますが、すでにこの段階で機能を正常に実行しています。 始める前に、準備ができているものを見つけることを期待して、インターネットを長時間検索しましたが、できませんでした。 現在、私はそれをすべての人に公開していますが、この作品が私たち以外の誰かに役立つことを願っています。



このプロジェクトはgithubのMIT Licenseで入手できますgithub.com/marcellus00/visc



All Articles