Friday 12 February 2016

Custom inspector

Creating a custom inspector allows for quicker and more agile development. In this example, I will show you how I created a simple "laser" object which can work in three different modes. I then modified the inspector for the object so I can select from it the mode I wish to use and only the variables relevant to that particular mode will show up, so as to avoid to mess around with variables that have nothing to do with that specific running mode.

To make it more clear, my "laser column" will have 3 modes: STATIC, inn which the laser beam is simply not moving. When in MOVING mode the laser beam will go up and down, and in the inspector we can tweak a float variable which determines the speed. Finally, the INTERMITTENT mode will cause the beam to go on and off, using 2 float variables to select the timing.

Before I go any further, here's a short video of the project:


I know the videos here a pretty bad quality, I will upload them on YouTube later on so I can get a better resolution. For the moment, I apologize for the "not exactly HD" quality of the content.

To achieve this, we need 2 scripts: one for the actual laser object, in which we write the behavior of the laser beam, and another one for modifying the inspector of that script.

My laser object consists of two cubes, scaled so they look like two columns. One of them has a child object, which I called "LaserPoint", which has 2 components attached: a line renderer (our beam) and the LaserPoint script. The 2 cubes are then put as child of a parent empty object, so they can be moved around as one single object. (Fig 1).

Fig 1


This is the LaserPoint script:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class LaserPoint : MonoBehaviour {

 public enum LaserTypes
 {
  STATIC,
  MOVING,
  INTERMITTENT
 }

 public LaserTypes laserType;

 private float maxHeight ;
 private float minHeight;

 LineRenderer lineRend;

 Ray ray;
 RaycastHit hit;
 Vector3 otherPoint;

 public float movingSpeed;

 public float time_off;
 public float time_on;

 float timer;
 float heightToReach;
 bool on;

 // Use this for initialization
 void Awake () {

  lineRend = GetComponent<LineRenderer> ();

  ray = new Ray (transform.position, -transform.right);

  timer = 0;

  minHeight = transform.position.y - 0.8f;
  maxHeight = transform.position.y + 0.8f;
 
 }
 
 // Update is called once per frame
 void Update () {

  switch (laserType) {
   
  case LaserPoint.LaserTypes.STATIC:   
   DrawLaserBeam ();
   break;
   
  case LaserPoint.LaserTypes.MOVING:
   MovingBehaviour();
   break;
   
  case LaserPoint.LaserTypes.INTERMITTENT:
   IntermittentBehaviour();
   break;
  }
 
 }

 void MovingBehaviour()
 {

 if (transform.position.y <= minHeight)
   heightToReach = maxHeight;
  else if(transform.position.y >= maxHeight)
   heightToReach = minHeight; 
  
  transform.position = Vector3.MoveTowards(transform.position,new Vector3(transform.position.x,heightToReach,transform.position.z),movingSpeed*Time.deltaTime);
  DrawLaserBeam ();

 }

 void IntermittentBehaviour()
 {
  timer += Time.deltaTime;

  if (timer >= time_on && on) {
   timer = 0;
   on = false;
  }

  if (timer >= time_off && !on) {
   timer = 0;
   on = true;
  }

  if (on)
   DrawLaserBeam ();
  else
   lineRend.enabled = false;
 }

 void DrawLaserBeam()
 {
  if (!lineRend.enabled)
   lineRend.enabled = true;

  ray = new Ray (transform.position, -transform.right);
  if (Physics.Raycast (ray, out hit, 50)) {
   // Debug.Log("Laser hitting: " + hit.collider.name);
   
   lineRend.SetPosition(0,transform.position);
   lineRend.SetPosition(1,hit.point);
   
   
  }
 }
}

I'm not going to go into the details of this script as this is not what this post is bout, but I will give you a general guideline. I assume that you are familiar with enumerations, raycasting, line renderer and other basic features.After declaring all the variables I need, in the Update() method I simply check the LaserType variable to check which mode has been selected and I call the relative method.

In the MovingBehavoir() I take the LaserPoint object (which has this script attached) and I just move it up and down according to the variables maxHeight and minHeight, and I draw the beam at each call.

The IntermittentBehaviour() method uses the float variable time to create a simple timer which is used to determine the seconds for which the LineRenderer will be on and off.

In the Update() we can see that for the STATIC mode I just draw the beam and nothing else happens.

When it comes to actually drawing the laser in the DrawLaserBeam() function, first thing I do is to enable the LineRenderer component. Then I cast a ray towards the right, knowing that the other column (the cube object) is going to be there, and I set the position of the 2 points of the line renderer. I do not check for any layer when casting the ray, so if any other object will go between the columns and crossing the laser beam, we will see the beam itself "hitting" that object, as the second point of the line renderer (hit.point on line 108) will be on the object obstructing the beam. (Fig 2).

Fig 2

Now, the other script. The custom inspector for the LaserPoint script will allow me to choose the mode, thanks to the enumerations, and only the variables relevant to that mode will be displayed, so it becomes "less confusing" when placing laser objects in the scene.

One important thing to remember, every time we write a custom inspector script we need to place the script inside the Editor folder.

I called the script Laser_CI, and this is it:



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[CustomEditor(typeof(LaserPoint))]
public class Laser_CI : Editor {
 
 public override void OnInspectorGUI ()
 {
  LaserPoint myTarget = (LaserPoint)target;

  myTarget.laserType = (LaserPoint.LaserTypes)EditorGUILayout.EnumPopup ("Operating mode", myTarget.laserType);

  switch (myTarget.laserType) {

  case LaserPoint.LaserTypes.STATIC:

   EditorGUILayout.HelpBox("Static type",MessageType.Info);
   break;

  case LaserPoint.LaserTypes.MOVING:

   myTarget.movingSpeed = EditorGUILayout.FloatField("Moving Speed",myTarget.movingSpeed);
   EditorGUILayout.HelpBox("Moving type",MessageType.Info);
   break;

  case LaserPoint.LaserTypes.INTERMITTENT:
   myTarget.time_on = EditorGUILayout.FloatField("Seconds on",myTarget.time_on);
   myTarget.time_off = EditorGUILayout.FloatField("Seconds off",myTarget.time_off);
   EditorGUILayout.HelpBox("Intermittent type",MessageType.Info);
   break;
  }

 }

}

First thing we notice on line 1 is the CustomEditor(...) instruction, which basically tell the engine what script we are customizing. Also, this class will need to inherit from Editor rather than from MonoBehavior. All we need to do now is to override the function OnInspectorGUI(), which is the default function used to draw all the public variables of the scripts we attach to game objects.

If, for example, we were to leave this function overridden but empty, the LaserPoint script attached to the LaserPoint object would look like this:


Notice how the script is visible in the inspector, but non of the public variables are drawn, as we simply did not tell the compiler to draw them.

On line 6, I create a LaserPoint type variable called myTarget, which is a reference to the script we are trying to customize. To get the object we use the variable target: this is a default variable used to get a reference of the script we want to create a custom inspector for. However, the target variable is of type Object, therefore we need to cast it to the type we need, which is LaserPoint. At this point we can access all the variables in the LaserPoint script through the myTarget variable.

O line 8, I use the EditorGUILayout.EnumPopup(...) method to get the enumeration drop down menu drawn in the inspector. The String parameter we pass in will be the text displayed for that variable, while the myTarget.laserType parameter is the variable itself. (Fig 3).

Fig 3
Because this is a read method, we need to re-assign the myTarget.laserType variable to it so it stays updated, and it needs to be casted to our enumerations type, which is LaserTypes, as the EditorGUILayout.EnumPopup(...) method returns a general System.enum type of object.

Next, I perform a simple switch according to the mode selected.

If STATIC mode was chosen, in the inspector I simply draw a HelpBox to remind the user what mode they are in. I will be doing this for each mode. (Fig 4).

Fig 4
For the MOVING mode, I will get the float variable to be drawn in the inspector so we can change the speed. The EditorGUILayout.FloatField (..) works like the method we use for the enumeration, passing a String variable for the variable displayed name ad the variable itself fromt the script, which is then re-assigned. (Fig 5).

Fig 5

Finally, I do the same thing for the INTERMITTENT mode, only this time I display the 2 variables used to change the timing.(Fig 6).

Fig 6
This is obviously a very small example, but it shows the potential of using a custom inspector, allow for a better workflow and, simply put, keep things nice and tidy.

No comments:

Post a Comment