Now that we have the basic functionality in, it wouldn't be that enjoyable if all we could do was added and remove walls. We also want to be able to spawn collectibles and change the player's starting location. Let's work on that next:
MonoDevelop
in the LevelEditor
class, we're going to want to first add in an OnGUI
function to display the types of things we can create:void OnGUI() { GUILayout.BeginArea(new Rect(Screen.width - 110, 20, 100, 800)); foreach(Transform item in tiles) { if (GUILayout.Button (item.name)) { toCreate = item; } } GUILayout.EndArea(); }
OnGUI
function is called for rendering and handling GUI events using IMGUI which is a code-driven GUI system. While it was the way to create GUIs before Unity 4.6, it currently is primarily used by programmers for tools development and custom inspectors for scripts or new editors to extend Unity itself. The most important concept to grasp first is the GUILayout.Button
function. This is what we refer to as a
GUI control, and there are many others that we will be using in the future. So, to clear up any confusion, let's talk about how all of these work now.
Creating any GUILayout
control consists of the following:
ControlType(Content)
The parts of the preceding line of code are explained in the following sections:
The ControlType
function is a function that exists in Unity's GUI and GUILayout
class, and is the name of the element that you want to create in the world. In the preceding code, we used GUILayout.Button
, but there are many more.
The argument for the control is the actual content that we want to display with the ControlType
we are using. Right now, we're just passing in a string to display, but we can also display images and other content as well, including other controls. We will talk about other pieces of content that we can add in later.
One of the most common UI elements is the Button
control. This function is used to render a clickable object. If you look at the code we just wrote, you'll note that the button is cased inside of an if
statement. This is because if the button is clicked on, then the function will return true
when the button is released. If this is true, we will set toCreate
to whatever object has that name.
By default, the GUILayout
class will just put the buttons up at the top-left side, but we may also want our objects to be grouped together So, I specify an area that I want the menu to be in using the BeginArea
function. Anything I place before I call the EndArea
function will be inside that area, and GUILayout
will attempt to place it in a pleasing way for me.
If you want to have precise control on where and how things are drawn, you can make use of the GUI class. However, if you do not want to manually specify a position and are okay with Unity automatically modifying the size and position of controls, you can use the GUILayout
class that adds a parameter for a position.
For more information on IMGUI check out http://docs.unity3d.com/Manual/GUIScriptingGuide.html.
GameController
class, add the following code to our Update
function (create the function as well if it doesn't exist in your current implementation, such as the example code):void Update() { if(Input.GetKeyDown("f2")) { this.gameObject.GetComponent<LevelEditor>().enabled = true; } }
Now, if we move back to the game and press the F2 key, you'll see that a menu pops up, which we can then select items from. This works fine for the walls and the collectibles, but there's a bit of an issue with the player and the collectibles. Take a look at the following screenshot:
As you can see, we are spawning more players that all respond to player input, and the number of collectibles on our screen are not reflected properly in our text. We will solve both of these issues now. We will first create a new object named PlayerSpawner
, which will act as the place where the player will start when the game starts, and make it such that we can only have one of them.
PlayerSpawn
by clicking on the name of the material in the project browser, typing in the new name and then pressing Enter.PlayerSpawn
object selected, set the Rendering Mode to Transparent so that we can make the material semitransparent. Then, change the Main Color property to a red color with a low alpha value. If all goes well, it should look like the following screenshot:
PlayerSpawn
. Switch to the Scene view if you haven't so that you can see the newly created object.PlayerSpawn
material. Take a look at the following screenshot:Scripts
folder and create a new C# script named PlayerStart
. Once that's finished, open your IDE and use the following code for the file:using UnityEngine; using System.Collections; public class PlayerStart : MonoBehaviour { //A reference to our player prefab public Transform player; //Have we spawned yet? public static bool spawned = false; public static PlayerStart _instance; // Use this for initialization void Start () { // If another PlayerStart exists, this will replace it if(_instance != null) Destroy(_instance.gameObject); _instance = this; // Have we spawned yet? If not, spawn the player if(!spawned) { SpawnPlayer(); spawned = true; } } void SpawnPlayer() { Transform newObject = Instantiate(player, this.transform.position, Quaternion.identity) as Transform; newObject.name = "Player"; } }
Player
prefab.PlayerStart
object from Hierarchy to the Prefabs
folder to make it a prefab. Then, delete the object from Hierarchy.GameController
object and assign the PlayerStart
prefab where you used to see the player in Tiles | Element 1. Save your scene and play the game. Take a look at the following screenshot:We can now select the PlayerStart object from the button and place it wherever we want, and there will always just be one. Also, once we have levels saving/loading, the code will properly spawn the player wherever the PlayerStart object is placed!
public void UpdateOrbTotals(bool reset = false) { if (reset) orbsCollected = 0; GameObject[] orbs; orbs = GameObject.FindGameObjectsWithTag("Orb"); orbsTotal = orbs.Length; scoreText.text = "Orbs: " + orbsCollected + "/" + orbsTotal; }
LevelEditor
class and add the following line to the end of our Start
function:GameController._instance.UpdateOrbTotals(true);
Update
function, we'll need to add the following lines in bold:void Update() { // Left click - Create object if (Input.GetMouseButton(0) && GUIUtility.hotControl == 0) { Vector3 mousePos = Input.mousePosition; /* Set the position in the z axis to the opposite of the camera's so that the position is on the world so ScreenToWorldPoint will give us valid values. */ mousePos.z = Camera.main.transform.position.z * -1; Vector3 pos = Camera.main.ScreenToWorldPoint(mousePos); // Deal with the mouse being not exactly on a // block int posX = Mathf.FloorToInt(pos.x + .5f); int posY = Mathf.FloorToInt(pos.y + .5f); Collider[] hitColliders = Physics.OverlapSphere(pos, 0.45f); int i = 0; while (i < hitColliders.Length) { if (toCreate.name != hitColliders[i].gameObject.name) { DestroyImmediate(hitColliders[i].gameObject); } else { // Already exists, no need to create // another return; } i++; } CreateBlock(tiles.IndexOf(toCreate) + 1, posX, posY); GameController._instance.UpdateOrbTotals(); } // Right clicking - Delete object if (Input.GetMouseButton(1) && GUIUtility.hotControl == 0) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit = new RaycastHit(); Physics.Raycast(ray, out hit, 100); // If we hit something other than the player, we // want to destroy it! if ((hit.collider != null) && (hit.collider.name != "Player")) { Destroy(hit.collider.gameObject); } GameController._instance.UpdateOrbTotals(); } }
As you can see, we're now able to draw over the other object and place everything that we want for our level!