📜 ⬆️ ⬇️

Creating a script editor in Unity

Everyone who works with Unity comes to the creation of custom tools sooner or later. You can resist and fear for a long time, but at some point without editors and inspectors who are honed to the needs of the team, it will be impossible to move.

I participate in the project of one very talented artist, where I help in developing a quest game in retro pixel-art style. We use Unity, since we both have a long development experience in this environment. Almost immediately it became necessary to create posed events, cut-scenes and puzzles, during which the series of actions was strictly defined. At first, I tried to get rid of as little blood as possible and suggested using the standard Animator Controller and the StateMachineBehaviour class from Unity 5 to customize events, but as it turned out this approach does not work: the animator finite machine, although universal, would require an excessive amount of unnecessary actions for absolutely linear things , and we needed a similar visual solution, but allowing us to easily and easily build events like in the timeline of video editors.


A picture from the Unity documentation that inspired you to create your own editor.
')
Thus, writing your own full-fledged editor turned out to be inevitable.


Up to this point, I have only written my inspectors for MonoBehaviour classes. In my opinion, the approach used by Unity for the editor's interfaces is somewhat cumbersome, so I was very afraid of what might come out when writing the whole window with the timeline. In the end, what happened: yes, cumbersome, but no, nothing terrible, the eyes and mind get used to.

So, the task is immediately easily divided into two: the basis of the script system and the interface itself.

Scripting system



The logic of work is simple: for each scenario, a list of events should be defined that will start and end at a specific time. If we define these actions, how to store them? The built-in Unity MonoBehaviour class automatically serializes the supported fields, but in order for it to work, the script must be assigned to the active object in the scene. This is suitable for the script class, but not suitable for our actions - for each abstract entity we would have to create a real object in the hierarchy. For our purpose in Unity there is a class ScriptableObject , whose life cycle is similar to MonoBehaviour but with some limitations, and most importantly it does not require an object in the scene for the existence and execution of code.

All that the scripting class does is start a coruntine , which checks on each frame how much time has passed and who needs to be started, updated or stopped now. Here is the main method (link to full sources at the end):

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; } 



For EventAction, I identified three significant events: "The Beginning of Life", "The Moment Between" (every frame is called) and "The End". Depending on the action itself, this or that may be necessary, for example, “to orient the camera at the very beginning,” “update the position while the action is taking place,” “to return control to the player at the end.” To create your own action, it is enough to override the corresponding methods in the heir class.

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() { } } } 



With a simple over, now the most interesting.

Script Editor Interface



The old Unity interface system continues to exist for the gui editor (custom inspectors and windows) and works as follows: when certain events occur (click the mouse, update data, explicitly call Repaint ()) it calls a special method of the user class, which in turn makes calls drawing interface elements. Standard elements can be automatically located in the window, all of them are in the GUILayout and EditorGUILayout classes, I used them for simple script properties and visual settings:

Basic parameters

To create your own editor window, you must inherit from EditorWindow and define the OnGUI () method:

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"); } { 



But in the base library of elements there is no need for me, namely the draggable boxes that can exist on several tracks and change their size (there is a GUI.Window, but this is not quite that). Therefore, we had to do it manually, namely: independently draw rectangles corresponding to the actions, for example:

 //        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); 


This code will draw this box:

Object Movement Event

Unity allows you to define the pressed button (Event.current.type == EventType.MouseDown && Event.current.button == 0), find out if the cursor falls into a rectangle (Rect.Contains (Event.current.mousePosition)) or even deny processing button presses in this frame are further down the code (Event.current.Use ()). Using these standard tools, I implemented interaction: events can be dragged, selected at once, and changed in length. When the user clicks or moves the box, the parameters of the corresponding action actually change, and the interface is redrawn following them. On the right click, you can add or remove an action, and double-clicking opens the editing window:

Where does the interface come from for each action? In EventAction, I added two more virtual methods related only to the editor: OnEditorGui () and OnDrawTimelineGui () - they allow you to define the interface when editing an action, and even to display it in the timeline of the editor.

For the project, I have already written a few of my own actions that apply exclusively to it: an action that displays character dialogues, an action that sets a goal for the main character or launches its special animation, or, for example, an EventAction that allows you to control the camera's behavior: follow the player, center on the object , disable centering.

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 } } 



What happened in the end?




Known Issues


Scenario and EventAction are independent entities, so if we duplicate the script and compile its serialized properties, then the links to the existing actions will fall into the new script. I plan to fix this situation by keeping script-action links, but for now I'm thinking about it.

Conclusion


I believe that the main goal was achieved. The project is still ahead, there is still to be polished and fixed bugs, but already at this stage it is successfully performing its function. Before embarking on it, I combed the Internet for a long time in the hope of finding something ready, but I could not. Now I post it for everyone and I hope that this work may be useful to someone other than us.

The project is available on github under MIT License github.com/marcellus00/visc

Source: https://habr.com/ru/post/281571/


All Articles