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:
EditorWindow
, which allows you to input settings and sprite files that will give you a box grid and simplify the level pieces creation.Use the prefabs we made in the C# script. This will move the level pieces of prefabs under the player character, simulating movement.
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:
Assets
folder. Right-click on this folder and select Create and then New Folder. Name this folder Level
.Level
folder and select Import New Asset….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.Script
folder, select Create and then C# Script. Name the script SpriteTiler
.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.
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.
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:
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.
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.
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
.
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
.
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
.
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
.
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:
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:
Floor_00
.Floor_01
.I chose X = 6
, Y = 8
, and the grid looks similar to the following screenshot:
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.