An editor extension to allow pickup type (and parameters) to be changed at design time via a custom Inspector UI

The use of enums and corresponding drop-down menus in the Inspector panel to restrict changes to one of a limited set often works fine (for example, pickup types for a pickup object). However, the trouble with this approach is, when two or more properties are related and need to be changed together, there is a danger of changing one property, for example, pickup type from Heart to Key, but forgetting to change corresponding properties; for example, leaving the Sprite Renderer component still showing a Heart sprite. Such mismatches cause problems both in terms of messing up intended level design and, of course, the frustration for the player when they collide with something showing one pickup image, but a different kind of pickup type is added to the inventory!

If a class of GameObject has several related properties or components, which all need to be changed together, then a good strategy is to use Unity Editor extensions to do all the associated changes each time a different choice is made from a drop-down menu showing the defined set of enumerated choices.

In this recipe, we introduce an Editor extension for PickUp components of GameObjects.

An editor extension to allow pickup type (and parameters) to be changed at design time via a custom Inspector UI

Getting ready

This recipe assumes you are starting with project Simple2Dgame_SpaceGirl setup from the first recipe in Chapter 2, Inventory GUIs. A copy of this Unity project is provided in a folder named unityProject_spaceGirlMiniGame in the 1362_12_01 folder.

How to do it...

To create an editor extension to allow pickup type (and parameters) to be changed at design-time via a custom Inspector UI, follow these steps:

  1. Start with a new copy of mini-game Simple2Dgame_SpaceGirl.
  2. In the Project panel, create a new folder named EditorSprites. Move the following images from folder Sprites into this new folder: star, healthheart, icon_key_green_100, icon_key_green_32, icon_star_32, and icon_heart_32.
    How to do it...
  3. In the Hierarchy panel, rename GameObject star to be named pickup.
  4. Edit the tags, changing tag Star to Pickup. Ensure the pickup GameObject now has the tag Pickup.
  5. Add the following C# script PickUp to GameObject pickup in the Hierarchy:
    using UnityEngine;
    using System;
    using System.Collections;
    
    public class PickUp : MonoBehaviour {
      public enum PickUpType {
        Star, Health, Key
      }
    
      [SerializeField]
      public PickUpType type;
    
      public void SetSprite(Sprite newSprite){
        SpriteRenderer spriteRenderer = GetComponent<SpriteRenderer>();
        spriteRenderer.sprite = newSprite;
      }
    }
  6. In the Project panel, create a new folder named Editor. Inside this new folder, create a new C# script class named PickUpEditor, with the following code:
    using UnityEngine;
    using System.Collections;
    using System;
    using UnityEditor;
    using System.Collections.Generic;
    
    [CanEditMultipleObjects]
    [CustomEditor(typeof(PickUp))]
    public class PickUpEditor : Editor
    {
      public Texture iconHealth;
      public Texture iconKey;
      public Texture iconStar;
    
      public Sprite spriteHealth100;
      public Sprite spriteKey100;
      public Sprite spriteStar100;
    
      UnityEditor.SerializedProperty pickUpType;
    
      private Sprite sprite;
      private PickUp pickupObject;
    
      void OnEnable () {
        iconHealth = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_heart_32.png", typeof(Texture)) as Texture;
        iconKey = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_key_32.png", typeof(Texture)) as Texture;
        iconStar = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_star_32.png", typeof(Texture)) as Texture;
    
        spriteHealth100 = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/healthheart.png", typeof(Sprite)) as Sprite;
        spriteKey100 = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_key_100.png", typeof(Sprite)) as Sprite;
        spriteStar100 = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/star.png", typeof(Sprite)) as Sprite;
    
        pickupObject = (PickUp)target;
        pickUpType = serializedObject.FindProperty ("type");
      }
    
    
      public override void OnInspectorGUI()
      {
        serializedObject.Update ();
    
        string[] pickUpCategories = TypesToStringArray();
        pickUpType.enumValueIndex = EditorGUILayout.Popup("PickUp TYPE: ", pickUpType.enumValueIndex, pickUpCategories);
    
        PickUp.PickUpType type = (PickUp.PickUpType)pickUpType.enumValueIndex;
        switch(type)
        {
        case PickUp.PickUpType.Health:
          InspectorGUI_HEALTH();
          break;
    
        case PickUp.PickUpType.Key:
          InspectorGUI_KEY();
          break;
    
        case PickUp.PickUpType.Star:
        default:
          InspectorGUI_STAR();
          break;
        }
    
        serializedObject.ApplyModifiedProperties ();
      }
    
      private void InspectorGUI_HEALTH()
      {
        GUILayout.BeginHorizontal();
        GUILayout.FlexibleSpace();
        GUILayout.Label(iconHealth);
        GUILayout.Label("HEALTH");
        GUILayout.Label(iconHealth);
        GUILayout.Label("HEALTH");
        GUILayout.Label(iconHealth);
        GUILayout.FlexibleSpace();
        GUILayout.EndHorizontal();
    
        pickupObject.SetSprite(spriteHealth100);
      }
    
      private void InspectorGUI_KEY()
      {
        GUILayout.BeginHorizontal();
        GUILayout.FlexibleSpace();
        GUILayout.Label(iconKey);
        GUILayout.Label("KEY");
        GUILayout.Label(iconKey);
        GUILayout.Label("KEY");
        GUILayout.Label(iconKey);
        GUILayout.FlexibleSpace();
        GUILayout.EndHorizontal();
    
        pickupObject.SetSprite(spriteKey100);
      }
    
      private void InspectorGUI_STAR()
      {
        GUILayout.BeginHorizontal();
        GUILayout.FlexibleSpace();
        GUILayout.Label(iconStar);
        GUILayout.Label("STAR");
        GUILayout.Label(iconStar);
        GUILayout.Label("STAR");
        GUILayout.Label(iconStar);
        GUILayout.FlexibleSpace();
        GUILayout.EndHorizontal();
    
        pickupObject.SetSprite(spriteStar100);
      }
      private string[] TypesToStringArray(){
        var pickupValues = (PickUp.PickUpType[])Enum.GetValues(typeof(PickUp.PickUpType));
    
        List<string> stringList = new List<string>();
    
        foreach(PickUp.PickUpType pickupValue in pickupValues){
          string stringName = pickupValue.ToString();
          stringList.Add(stringName);
        }
    
        return stringList.ToArray();
      }
    }
  7. In the Inspector panel, select GameObject pickup and choose different values of the drop-down menu PickUp Type. You should see corresponding changes in the image and icons in the Inspector for the Pick Up (Script) component (three icons with the name of the type in between). The Sprite property of the Sprite Renderer component for this GameObject should change. Also, in the Scene panel, you'll see the image in the scene change to the appropriate image for the pickup type you have chosen.
    How to do it...

How it works...

Our script class PickUp has the enum PickUpType with the three values: Star, Health, and Key. Also, there is the variable type, storing the type of the parent GameObject. Finally, there is a SetSprite(…) method that sets the Sprite Renderer component of the parent GameObject to be set to the provided Sprite parameter. It is this method that is called from the editor script each time the pickup type is changed from the drop-down menu (with the corresponding sprite for the new type being passed).

The vast majority of the work for this recipe is the responsibility of the script class PickUpEditor. While there is a lot in this script, its work is relatively straightforward: for each frame, via method OnInspectorGUI(), a dropdown list of PickUpType values is presented to the user. Based on the value selected from this drop-down list, one of three methods is executed: InspectorGUI_HEALTH(), InspectorGUI_KEY(), InspectorGUI_STAR(). Each of these methods displays three icons and the name of the type in the Inspector beneath the drop-down menu and ends by calling the SetSprite(…) method of the GameObject being edited in the Inspector to update the Sprite Renderer component of the parent GameObject with the appropriate sprite.

The C# attribute [CustomEditor(typeof(PickUp))] appearing before our class is declared, tells Unity to use this special editor script to display component properties in the Inspector panel for Pick Up (Script) components of GameObjects, rather than Unity's default Inspector which displays public variables of such scripted components.

Before and after its main work, the OnInspectorGUI() method first ensures that any variables relating to the object being edited in the Inspector have been updated —serializedObject.Update(). The last statement of this method correspondingly ensures that any changes to variables in the editor script have been copied back to the GameObject being edited—serializedObject.ApplyModifiedProperties().

The OnEnable() method of script class PickUpEditor loads the three small icons (for display in the Inspector) and the three larger sprite images (to update the Sprite Renderer for display in the Scene/Game panels). The pickupObject variable is set to be a reference to the PickUp scripted component, allowing us to call the SetSprite(…) method. The pickUpType variable is set to be linked to the type variable of the PickUp scripted component whose special Inspector editor view makes this script possible—serializedObject.FindProperty ("type").

There's more...

Here are some details you don't want to miss.

Offer the custom editing of pickup parameters via Inspector

Many pickups have additional properties, rather than simply being an item being carried. For example, a health pickup may add health "points" to the player's character, a coin pickup may add money "points" to the characters bank balance, and so on. So, let's add an integer points variable to our PickUp class and offer the user the ability to easily edit this points value via a GUI slider in our customer Inspector editor.

Offer the custom editing of pickup parameters via Inspector

To add an editable points property to our PickUp objects, follow these steps:

  1. Add the following extra line into C# script PickUp to create our new integer points variable:
    public int points;
  2. Add the following extra line into C# script PickUpEditor to work with our new integer points variable:
    UnityEditor.SerializedProperty points;
  3. Add the following extra line into the OnEnable() method in C# script PickUpEditor to associate our new points variable with its corresponding value in the PickUp scripted component of the GameObject being edited:
    void OnEnable () {
      points = serializedObject.FindProperty ("points");
      pickUpType = serializedObject.FindProperty ("type");
      // rest of method as before…
  4. Now we can add an extra line into each GUI method for the different PickUp types. For example, we can add a statement to display an IntSlider to the user to be able to see and modify the points value for a Health PickUp object. We add a new statement at the end of the InspectorGUI_HEALTH()method in C# script PickUpEditor to display a modifiable IntSlider representing our new points variable as follows:
    private void InspectorGUI_HEALTH(){
      // beginning of method just as before…
    
      pickupObject.SetSprite(spriteHealth100);
    
    // now display Int Slider for points
      points.intValue = EditorGUILayout.IntSlider ("Health points", points.intValue, 0, 100);
    }

We provide four parameters to the IntSlider(…) method. The first is the text label the user will see next to the slider. The second is the initial value the slider displays. The last two are the maximum and minimum values. In our example, we are permitting values from 0 to 100, but if health pickups only offer one, two, or three health points, then we'd just call with EditorGUILayout.IntSlider ("Health points", points.intValue, 1, 5). This method returns a new integer value based on where the slider has been positioned, and this new value is stored back into the integer value part of our SerializedProperty variable points.

Note that the loading and saving of values from the scripted component in the GameObject and our editor script is all part of the work undertaken by our calls to the Update() method and the ApplyModifiedProperties() method on the serialized object in the OnInspectorGUI() method.

Note that since points may not have any meaning for some pickups, for example, keys, then we simply would not display any slider for the GUI Inspector editor when the user is editing PickUp objects of that type.

Offer a drop-down list of tags for key-pickup to fit via Inspector

While the concept of "points" may have no meaning for a key pickup, the concept of the type of lock that a given key fits is certainly something we may wish to implement in a game. Since Unity offers us a defined (and editable) list of string tags for any GameObject, often it is sufficient, and straightforward, to represent the type of lock or door corresponding to a key via its tag. For example, a green key might fit all objects tagged LockGreen and so on.

Offer a drop-down list of tags for key-pickup to fit via Inspector

Therefore, it is very useful to be able to offer a custom Inspector editor for a string property of key pickups that stores the tag of the lock(s) the key can open. This task combines several actions, including using C# to retrieve an array of tags from the Unity editor, then the building and offering of a drop-down list of these tags to the user, with the current value already selected in this list.

To add a selectable list of strings for the tag for lock(s) that a key fits, follow these steps:

  1. Add the following extra line into C# Script PickUp to create our new integer fitsLockTag variable:
    public string fitsLockTag;
  2. Add the following extra line into C# script PickUpEditor to work with our new integer fitsLockTag variable:
    UnityEditor.SerializedProperty fitsLockTag;
  3. Add the following extra line into the OnEnable()method in C# script PickUpEditor to associate our new fitsLockTag variable with its corresponding value in the PickUp scripted component of the GameObject being edited:
    void OnEnable () {
      fitsLockTag = serializedObject.FindProperty ("fitsLockTag");
      points = serializedObject.FindProperty ("points");
      pickUpType = serializedObject.FindProperty ("type");
      // rest of method as before…
  4. Now we need to add some extra lines of code into the GUI method for key PickUps. We need to add several statements to the end of method InspectorGUI_KEY() in C# script PickUpEditor to set up and display a selectable popup drop-down list representing our new fitsLockTag variable as follows. Replace the InspectorGUI_KEY() method with the following code:
    private void InspectorGUI_KEY() {
      GUILayout.BeginHorizontal();
      GUILayout.FlexibleSpace();
      GUILayout.Label(iconKey);
      GUILayout.Label("KEY");
      GUILayout.Label(iconKey);
      GUILayout.Label("KEY");
      GUILayout.Label(iconKey);
      GUILayout.FlexibleSpace();
      GUILayout.EndHorizontal();
    
      pickupObject.SetSprite(spriteKey100);
    
      string[] tags = UnityEditorInternal.InternalEditorUtility.tags;
      Array.Sort(tags);
      int selectedTagIndex = Array.BinarySearch(tags, fitsLockTag.stringValue);
      if(selectedTagIndex < 0)  selectedTagIndex = 0;
      selectedTagIndex = EditorGUILayout.Popup("Tag of door key fits: ", selectedTagIndex, tags);
    
      fitsLockTag.stringValue = tags[selectedTagIndex];
    }

We've added several statements to the end of this method. First tags, an array of strings, is created (and sorted), containing the list of tags currently available in the Unity editor for the current game. We then attempt to find the location in this array of the current value of fitsLockTag — we can use the BinarySearch(…) method of built-in script class Array because we have alphabetically sorted our array (which also makes it easier for the user to navigate). If the string in fitsLockTag cannot be found in array tags, then the first item will be selected by default (index 0).

The user is then shown the drop-down list via the GUILayout method EditorGUILayout.Popup(…), and this method returns the index of whichever item is selected. The selected index is stored into selectedTagIndex, and the last statement in the method extracts the corresponding string and stores that string into the fitsLockTag variable.

Note

Note: Rather than displaying all possible tags, a further refinement might remove all items from array 'tags' that do not have the prefix 'Lock'. So the user is only presented with tags such as 'LockBlue' and 'LockGreen', and so on.

Logic to open doors with keys based on fitsLockTag

In our player collision logic, we can now search through our inventory to see if any key items fit the lock we have collided with. For example, if a green door was collided with, and the player was carrying a key that could open such doors, then that item should be removed from the inventory List<> and the door should be opened.

To implement this, you would need to add an if test inside the OnTriggerEnter() method to detected collision with the item tagged Door, and then logic to attempt to open the door, and, if unsuccessful, do the appropriate action (for example, play sound) to inform the player they cannot open the door yet (we'll assume we have written a door animation controller that plays the appropriate animation and sounds and when a door is to be opened):

if("Door" == hitCollider.tag){
  if(!OpenDoor(hitCollider.gameObject))
    DoorNotOpenedAction();
}

The OpenDoor() method would need to identify which item (if any) in the inventory can open such a door, and, if found, then that item should be removed from the List<> and the door should be opened by the appropriate method:

private bool OpenDoor(GameObject doorGO){
  // search for key to open the tag of doorGO
  int colorKeyIndex = FindItemIndex(doorGO.tag);
  if( colorKeyIndex > -1 ){
    // remove key item from inventory List<>
    inventory.RemoveAt( colorKeyIndex );

    // now open the door...
    DoorAnimationController doorAnimationController = doorGO.GetComponent<>(DoorAnimationController);
    doorAnimationController.OpenDoor();

    return true;
  }

  return false;
}

The following is the code for a method to find the inventory list key item fitting a door tag:

private int FindItemIndex(string doorTag){
  for (int i = 0; i < inventory.Count; i++){
    PickUp item = inventory[i];
    if( (PickUp.PickUpType.Key == item.type) && (item.fitsLockTag == doorTag))
      return i;
  }

  // not found
return -1;
}

The need to add [SerializeField] for private properties

Note that if we wished to create editor extensions to work with private variables, then we'd need to explicitly add [SerializeField] in the line immediately before the variable to be changed by the editor script. Public variables are serialized by default in Unity, so this was not required for our public type variable in script class PickUp, although it's good practice to flag ALL variables that are changeable via an Editor Extension in this way.

Learn more from the Unity documentation

Unity provides documentation pages about editor scripts at http://docs.unity3d.com/ScriptReference/Editor.html.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.17.18