Chapter 4. Editor Tool, Prefabs, and Game Level

In the last chapter, we set up what will become the core of our gameplay. The player has the ability to send input to the device, and we will handle this by manipulating the player character GameObject. We also set up some game logic so that the player character can interact with positive and negative world objects, such as Coins and Obstacles. To further develop the sense of a complete game, we need to create the pieces of the game world that represent a floor that the player will run on.

To create these pieces, we will create a Unity EditorWindow class that will help us create grids that will represent the ground the player runs on and the dirt below it. Traditionally, you would have to place each sprite one at a time. With this editor tool, we will be able to create bigger boxes in a grid based on our settings.

After we have our editor tool running, we will begin to create the prefabs that will hold multiple GameObjects and their components in a single file. Finally, we will write the code needed to move the floor and ground pieces below the player character, simulating the character as running forward.

To summarize, in this chapter, we will cover the following topics:

  • Writing a Unity C# class that extends EditorWindow, which allows you to input settings and sprite files that will give you a box grid and simplify the level pieces creation.
  • Creating the game-related prefabs so that you have grouped files in an easy-to-use file.

Use the prefabs we made in the C# script. This will move the level pieces of prefabs under the player character, simulating movement.

Making the sprite tile editor tool

The Unity engine is incredibly flexible for all the aspects of game development, including creating custom editor tools to help fast track the more tedious aspects of development. In our case, it will be beneficial to have a tool that creates a root GameObject that will then create children GameObjects in a grid. The grid will be spaced out by the size of the sprite component that each of the children GameObject is using.

For example, if you were to place say 24 GameObjects one at a time, it could take some time to make sure that all are snapped correctly together. With our tool, we will be able to select the X value and the Y value for the grid, the sprite that represents the ground, and the sprite that represents the dirt below the ground. Perform the following steps:

  1. To begin with, navigate to the Assets folder. Right-click on this folder and select Create and then New Folder. Name this folder Level.
  2. Right-click on the new Level folder and select Import New Asset….
  3. In the files for this book, navigate to the folder named Art and then ChapterFour_Floor. Import all the four floor files: Floor_00, Floor_01, Floor_02, and Floor_03. We will use these files to create our game-level pieces.
  4. Right-click on the Script folder, select Create and then C# Script. Name the script SpriteTiler.

The SpriteTiler C# class

Double-click on the SpriteTiler C# file to open it. Change the file so that it looks similar to the following code:

using UnityEngine;
using UnityEditor;
using System.Collections;

public class SpriteTiler : EditorWindow
{
}

The big changes from the normally generated code file, is the addition to using UnityEditor, changing the inherited class to EditorWindow, and removing the Start( ) and Update( ) functions.

Global variables

We now want to add the global variables for this class. Add the following code in the class block:

    // Grid settings to make tiled by
    public float GridXSlider = 1;
    public float GridYSlider = 1;

    // Sprites for both the ground and dirt
    public Sprite TileGroundSprite;
    public Sprite TileDirtSprite;

    // Name of the GameObject that holds our tiled Objects
    public string TileSpriteRootGameObjectName = "Tiled Object";

The GridXSlider and GridYSlider values will be used to generate our grid, X being left to right and Y being top down. For example, if you had X set to five and Y set to three, the grid would generate columns of five elements and rows of three elements or five sprites long and three sprites down.

The TileGroundSprite and TileDirtSprite sprite files will make up the ground and dirt levels.

TileSpriteRootGameObjectName is the GameObject name that will hold the GameObjects children that have the Sprite components. This is editable by you so that you can choose the name of the GameObject that gets created to avoid having the default new GameObject for each one made.

The MenuItem creation

Next, we need to create the MenuItem function. This will represent the Editor selection drop-down list so that we can use our tool. Add the following function to the SpriteTiler class under the global variables:

    // Menu option to bring up Sprite Tiler window
    [MenuItem("RushRunner/Sprite Tile")]
    public static void OpenSpriteTileWindow()
    {
      EditorWindow.GetWindow< SpriteTiler > ( true, "Sprite Tiler" );
    }

As this class extends EditorWindow, and the preceding function is declared as MenuItem, it will create a dropdown in the Editor named RushRunner. This will hold a selection called Sprite Tile:

The MenuItem creation

Tip

You can name the dropdown and selection anything you like by changing the string that is passed into MenuItem, such as "MyEditorTool / Editor Tool Name".

If you save the SpRiteTiler.cs file and go back to Unity and allow the engine to compile, you will be able to click on the Sprite Tile button under RushRunner. This will create an editor window named Sprite Tiler.

The OnGUI function

Next, we need to add the function that will be used to draw all the window GUI elements or the fields that we will use to get the settings to make the grid. Under our OpenSpriteTileWindow function, add the following code:

    // Called to render GUI frames and elements
    void OnGUI()
    {
    }

OnGUI is the function that will draw our GUI elements to the window. This allows you to manipulate these GUI elements so that we have values to use when we create the GameObject grid and its children GameObjects with sprite components.

The GUILayout and OnGUI setup

To begin with the OnGUI function, we want to add the GUI elements to the window. In the OnGUI function, add the following code:

        // Setting for GameObject name that holds our tiled Objects
        GUILayout.Label("Tile Level Object Name", EditorStyles.boldLabel);
        TileSpriteRootGameObjectName = GUILayout.TextField( TileSpriteRootGameObjectName, 25 );

        // Slider for X grid value (left to right)
        GUILayout.Label("X: " + GridXSlider, EditorStyles.boldLabel);
        GridXSlider = GUILayout.HorizontalScrollbar( GridXSlider, 1.0f, 0.0f, 30.0f );
        GridXSlider = (int)GridXSlider;

        // Slider for Y grid value(up to down)
        GUILayout.Label("Y: " + GridYSlider, EditorStyles.boldLabel);
        GridYSlider = GUILayout.HorizontalScrollbar(GridYSlider, 1.0f, 0.0f, 30.0f);
        GridYSlider = (int)GridYSlider;

        // File chose to be our Ground Sprite
        GUILayout.Label("Sprite Ground File", EditorStyles.boldLabel);
        TileGroundSprite = EditorGUILayout.ObjectField(TileGroundSprite, typeof(Sprite), true) as Sprite;

        // File chose to be our Dirt Sprite
        GUILayout.Label("Sprite Dirt File", EditorStyles.boldLabel);
        TileDirtSprite = EditorGUILayout.ObjectField(TileDirtSprite, typeof(Sprite), true) as Sprite;

GUILayout.Label is a function that creates a text label in the window we are using. Its first use is to let the user know that the next setting is for Tile Level Object Name: the name of the root GameObject that will hold children GameObjects with sprite components. By default, this is set to Tiled Object, although we allow the user to change it.

In order to allow the user to change it, we need to give them a TextField parameter to input a new string. We do this by saying that TileSpriteRootGameObjectName is equal to the GUILayout.TextField setting. As this is used in OnGUI, anything the user inputs will change the value of TileSpriteRootGameObjectName. We will use this later when the user wants to create the GameObject.

We then need to create two HorizontalSlider GUI elements so that we can get values from them that represent the X and Y values of the grid. Similar to TextField, we can start each of the HorizontalSlider elements with GUILayout.Label. This describes what the slider is for. We will then assign the GridXSlider and GridYSlider values to what the HorizontalSlider is set to, which is one by default.

As the user adjusts the sliders, the GridXSlider and GridYSlider values will change so that when the user clicks on a button to create the GameObject, we will have a reference to the values that they want to use for the grid.

After HorizontalSliders, we want to have ObjectFields so that the user can search for and assign sprite files that will represent the ground and dirt of the grid. The user then uses EditorGUILayout.ObjectField to select a sprite reference from the Asset folder. As we assigned ObjectField to only accept sprites, that is all that the user will see when they want to input one. As we want this ObjectField to be for sprites, we will set the type of object to typeof( Sprite ) and then cast the result that is assigned to TileGroundSprite or TileDirtSprite to the sprite by using as Sprite.

The OnGUI create tiled button

In order to know when the user wants to create the root GameObject and its grid of children GameObjects, we will need a button. Add the following code under the last GUI elements:

    // If butt "Create Tiled" is clicked
    if (GUILayout.Button("Create Tiled"))
    {
      // If the Grid settings are both zero, 
      // send notification to user
      if (GridXSlider == 0 && GridYSlider == 0)
      {
        ShowNotification(new GUIContent("Must have either X or Y grid set to a value greater than 0"));
        return;
      }

      // if Dirt and Ground Sprite exist
      if (TileDirtSprite != null && TileGroundSprite !=null)
      {
        // If the Sprites sizes don't match,
        // send notification to user
        if (TileDirtSprite.bounds.size.x != TileGroundSprite.bounds.size.x || TileDirtSprite.bounds.size.y != TileGroundSprite.bounds.size.y)
        {
          ShowNotification(new GUIContent("Both Sprites must be of matching size."));
          return;
        }

        // Create GameObject and tiled 
        // Objects with user settings
        CreateSpriteTiledGameObject(GridXSlider, GridYSlider, TileGroundSprite, TileDirtSprite, TileSpriteRootGameObjectName);
      }
      else
      {
        // If either Dirt or Ground Sprite don't exist,
        // send notification to user
        ShowNotification( new GUIContent( "Must have Dirt and Ground Sprite selected." ) );
        return;
      }
    }

The first condition we have set is the GUILayout.Button( "Create Tiled" ) function. The Button function will return true as soon as it is clicked on, but it will still render to the window if false. This means that although the button is not active, it'll still be seen by the user.

As some settings will create a scenario that is not ideal for the concept of our SpriteTiler function, we first want to make sure that the settings are in line with what we have designed the tool to perform.

We will first check whether GridXSlider and GridYSlider are set to zero. If both of these values are set to zero, the grid won't create anything, and as the concept of the tool is to create a grid of children sprites, we will tell the user that they must have a selection above zero for either GridXSlider or GridYSlider.

We then check whether TileDirtSprite and TileGroundSprite have a value. If either of these values are null, the settings are not complete. This results in you telling the user that dirt and ground sprites need a selection.

If the user has set dirt and ground sprites to something, but their sizing is not the same, such as one being 32 x 32 and the other being 64 x 64, we will tell the user that both the sprites need to be of the same size. If we didn't check for this, the grid wouldn't align correctly, creating negative results and making the tool not function as we want it to.

If the user settings are in order, we will call the CreateSpriteTiledGameObject function and pass GridXSlider, GridYSlider, TileGroundSprite, TileDirtSprite, and TileSpriteRootGameObjectName.

The CreateSpriteTiledGameObject function

This function is designed to take the user settings and create the grid from them. Add the following function under the OnGUI function:

// Create GameObject and tiled children based on user settings
public static void CreateSpriteTiledGameObject(float GridXSlider, float GridYSlider, Sprite SpriteGroundFile, Sprite SpriteDirtFile, string RootObjectName)
{
  // Store size of Sprite
  float spriteX = SpriteGroundFile.bounds.size.x;
  float spriteY = SpriteGroundFile.bounds.size.y;

  // Create the root GameObject which will hold children that tile
  GameObject rootObject = new GameObject( );

  // Set position in world to 0,0,0
  rootObject.transform.position = new Vector3( 0.0f, 0.0f, 0.0f );

  // Name it based on user settings
  rootObject.name = RootObjectName;

  // Create starting values for while loop
  int currentObjectCount = 0;
  int currentColumn = 0;
  int currentRow = 0;
  Vector3 currentLocation = new Vector3( 0.0f, 0.0f, 0.0f );

  // Continue loop until all rows 
  // and columns have been filled
  while (currentRow < GridYSlider)
  {
    // Create a child GameObject, set its parent to root, 
    // name it, and offset its location based on current location
    GameObject gridObject = new GameObject( );
    gridObject.transform.SetParent( rootObject.transform );
    gridObject.name = RootObjectName + "_" + currentObjectCount;
    gridObject.transform.position = currentLocation;

    // Give child gridObject a SpriteRenderer and set sprite on CurrentRow
    SpriteRenderer gridRenderer = gridObject.AddComponent<SpriteRenderer>( );
    gridRenderer.sprite = ( currentRow == 0 ) ? SpriteGroundFile : SpriteDirtFile;

    // Give the gridObject a BoxCollider
    gridObject.AddComponent<BoxCollider2D>();

    // Offset currentLocation for next gridObject to use
    currentLocation.x += spriteX;

    // Increment current column by one
    currentColumn++;

    // If the current column is greater than the X slider
    if (currentColumn >= GridXSlider)
    {
      // Reset column, increment row, reset x location
      // and offset y location downwards
      currentColumn = 0;
      currentRow++;
      currentLocation.x = 0;
      currentLocation.y -= spriteY;
    }

    // Add to currentObjectCount for naming of 
    // gridObject children.
    currentObjectCount++;
  }
}

To start with, we must first have the X and Y sizes of the sprite we want to create so that we can offset the location of the children GameObjects that were created. As we originally checked to make sure that both sprites are of the same size, it doesn't matter which sprite object we get the size from. In our case, we will use SpriteGroundFile.

We will then move the rootObject position to 0X, 0Y, and 0Z so that it is in the center of our scene. This can be set to anything you like, although when rootObject and its children get created, it is easier to find it at the center of the scene world.

After it has been moved, we can set its name to the setting that the user had entered or Tiled Object (the default).

Once we have rootObject set up, we can create its children GameObjects. To start this cycle, we will need a few variables to reference and change:

  • currentObjectCount: This specifies the total number of children that will be created. This increments for each one created.
  • currentColumn: This denotes the current column we are on in the row.
  • currentRow: This specifies the current row we are on.
  • currentLocation: This denotes the current location that the children GameObject will use and sets its position too. This is changed after each new child is created based on the X or Y setting of the sprite size.

Now that we have our rootObject and the variables we need to create the children, we can use a while loop. A while loop is a loop that will continue until its condition fails. In our case, we will check whether currentRow is less than the GridYSlider value. As soon as currentRow is equal to or greater than GridYSlider, the loop will stop because the condition failed.

The reason we will look at currentRow is that for each column created, we can reset its value to zero and increment currentRow by one. This means that each row will hold as many columns as were set by the GridXSlider value, and we know that the grid is complete when currentRow is equal to or greater than GridYSlider.

For example, if we had a grid setting of 3X and 3Y, the first row will hold three columns. When the first row is done, the row changes to two and adds three more columns. In the last row, it completes three more columns and then the while condition fails because the row value is equal to GridYSlider.

In each loop of the while loop, we start by creating gridObject. We set this grid object's parent to that of rootObject, set its name to RootObjectName, and concatenate an underscore, followed by currentObjectCount and then set the gridObject position to the currentLocation value, which will change based on the size of the sprite and the column/row.

We will then add a SpriteRenderer component to gridObject and assign a sprite to it. We will change the sprite based on whether currentRow is equal to zero or not. If it is, in the first row, we will set the sprite to SpriteGroundFile. If currentRow is not equal to zero, we will set the sprite to SpriteDirtFile.

The ternary operator is a sort of shorthand for if else. If the condition is true, we will set the value to what is behind the question mark. If the condition is false, we will set the value based on what's behind the colon. The question mark represents if, whereas the colon represents else.

Tip

The ternary operator is as follows:

Value = ( condition == true ) ? ifTrue : elseNotTrue;

Once we have the sprite assigned to the SpriteRenderer component of gridObject, we can assign a Box Collider 2D component, and the Box Collider 2D component will make itself the same size as the sprite. If we were to add the Box Collider 2D component to SpriteRenderer, it would be the default size of 1,1,1, which would be too big.

We will then offset currentLocation by the spriteX size, so the next gridObject will offset the size of the spriteX size.

The currentColumn parameter is incremented by one, and we then check whether currentColumn is greater than or equal to the GridXSlider value. If it is, we know that we need to start the next row.

To do this, we reset currentColumn to zero, increment currentRow by one, set the currentLocation.x value to zero, and offset currentLocation.y by a negative spriteY size. This not only results in an offset location down, but also resets the X value to zero, making it possible for the columns to be created again; just down from the size of spriteY.

Finally, we increment currentObjectCount by one.

Testing the SpriteTiler file

Make sure to save the SpriteTiler.cs file and then go back to Unity and allow it to compile.

When it does, you should continue to see the RushRunner drop-down selection on the top editor bar. Click on it and then on its child: Sprite Tile. This should open the Sprite Tiler window with all of our settings:

Testing the SpriteTiler file

The Sprite Tiler window now allows you to input the settings so that you can begin to create GameObjects, which hold a grid of children GameObjects, which in turn hold a sprite component of what is set within the Sprite Tiler window.

To begin, follow these steps:

  1. To test Sprite Tiler, you can keep the default Tile Level Object Name.
  2. Now, change X to a bigger value.
  3. Then, change Y to a bigger value.
  4. Now, click on the small circle to the right of Sprite Ground File and add Floor_00.
  5. Then, click on the small circle to the right of Sprite Dirt File and add Floor_01.
  6. Finally, click on Create Tiled.

I chose X = 6, Y = 8, and the grid looks similar to the following screenshot:

Testing the SpriteTiler file

Tip

If you want to test a bigger grid, choose Floor_03 as the ground sprite and Floor_02 as the dirt sprite. These images are bigger; however, because they are of the same size, you can use them as Floor_00 and Floor_01. They will make a bigger floor in the X axis because they are much wider.

Sprite Tiler wrap-up

This editor tool allows you to create GameObjects that hold child GameObjects that have SpriteRenderers on them. This sort of method can be extended and modified to create more complex shapes and use different types of sprite images to create them. For now, this allows you to create the basic structure of our game-level pieces with much less effort than if we didn't have this tool to assist us.

Extending Unity's EditorWindow allows you and your team to create an assortment of custom tools that can handle any sort of efficiency or workflow problem specific to the type of game you will create.

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

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