In order to create a prefab, you must first have a collection of GameObjects that you want to keep together as one prefab.
Perform the following steps:
Assets
folder. Right-click on it and select Create and then Folder. Name this Prefabs
.Prefabs
folder.This will create an Axe prefab that we can use in the future that holds everything the current Axe GameObject does.
Anytime in the future, when we want to create an axe, we will have a complete file that holds everything we know as axe.
This is essentially all it takes to create prefabs. These are an incredibly simple and useful aspect of creating games with Unity.
Let's create a couple of different varieties of coins, so when we use them in our level, we will have a more efficient system. Perform the following steps:
CoinPattern_00
.0X
, 0Y
, and 0Z
in the world.Now, move the existing coin in the scene. Name it Pickup_Coin_0
so that it becomes a child of the CoinPattern_00 GameObject. Center this coin to 0X
, 0Y
, and 0Z
. This location is referenced by its parent, so it is a local position and not a world one. The CoinPattern_00 GameObject has the world location and the Coin
child is relative to its position.
Coin
child and click on Ctrl + D to duplicate it. Do this a few times and lay out the coins in an arch, as shown in the following screenshot:Assets/Prefabs
folder.Once the prefab is created for CoinPattern_00, you can drag a prefab onto the scene and see the power of prefabs. As the prefab stores all the references you created, when they are put into the scene, the prefab is exactly the same as the CoinPattern_00 GameObject, which we created earlier.
Follow the same steps to create a couple of different CoinPattern GameObjects and make sure to create prefabs for all of them. I made three CoinPattern GameObjects and turned them into a prefab.
If you want to alter a prefab, enter it in the scene and make your changes. When you are done with your changes, click on the parent GameObject and then drag it onto the existing prefab you want to edit. There is also an option in the Inspector window that says Apply. If you click on this, the prefab will be updated to the current settings in the Scene window.
The last step of our prefab setup is to create the level pieces. These will be the pieces we put together with some code to create a "random" level. As the level pieces will be moving under the player, simulating the player moving, we will have to move these level prefab pieces around in the world and introduce coin and obstacles around them, so the player has a somewhat different experience as the game is running.
To start with, use our Sprite Tiler tool to create some tiled GameObjects. I suggest adding some slopes by taking the floor sprites, duplicating them and then rotating them. Here is an example:
This is a grid GameObject created with the Sprite Tiler tool with an additional row copied to the top and a couple more single grid objects duplicated and rotated. Use your imagination and create at least three of these. When you create these, make sure to keep the Y
grid amount the same for the beginning and ending of all pieces. I will use five for a Y
setting, as seen in the preceding image.
When you are done, take each of the root GameObjects that were created and create prefabs for each of them. Remember to make them unique prefabs with single root GameObjects because we will use each grid individually.
In addition, we will need a starting level piece. This will be the level piece that always starts the game. It will give the player a longer amount of time to get situated with the game and will give us a starting point to begin moving and attaching the other level pieces together. Perform the following steps:
StartingLevelPiece
.Floor_03
.Floor_02
.Assets/Prefabs
folder to create its prefab.At this point, you should have a collection of CoinPattern, LevelPieces, and Axe GameObject(s) prefabs in the Prefabs
folder. This is the list I have:
The level pieces will need two extra GameObjects so that we know where they all begin and end. This way, we will be able to place them together by putting the beginning of the next piece at the end of the previous one.
Make sure that any of the level pieces in the Hierarchy tab are saved as a prefab and delete them from the Hierarchy tab. When the scene is clear of level pieces, take StartingLevelPiece and put it in the scene with the following steps:
0
, 0
, and 0
in the world with the Inspector window under Transform.EndLocation
.Assets/Prefabs
folder. This will overwrite the current version of the prefab with the updated one.Assets/Prefabs
folder and place it on the Scene window, you should see EndLocation as a child of it in Hierarchy and positioned in the bottom-right corner of the sprite children, as shown in the following image:This will give us a position we know as the root object position, which when we change it in code will move the rest of the children in a way we can rely on. The EndLocation GameObject also gives us a reliable position to connect another LevelPiece to it without needing to have any offsets.
Repeat these steps for the rest of the LevelPiece GameObjects. Make sure that the root GameObject is placed in the bottom-left corner of the GameObject, and the EndLocation is placed in the bottom-right corner of the GameObject.
In order to keep track of all the LevelPieces, we need a small script that stores their initial location. Right-click on the Assets/Scripts
folder and select Create. Then, click on C# Script and name it LevelPiece
.
Double-click on the
LevelPiece
file to open it and make sure that it looks similar to the following code:
using UnityEngine; using System.Collections; public class LevelPiece : MonoBehaviour { // The initial location of this level piece private Vector3 InitialLocation; // Use this for initialization void Awake () { InitialLocation = transform.position; } // Get the initial location of this // LevelPiece public Vector3 GetInitialLocation() { return InitialLocation; } }
This is a very simple class that is designed to keep track of where the LevelPiece starts. This will be used to reset the LevelPiece after it has been used so that it can be used again.
InitialLocation
is a Vector3
struct that gets assigned during the Start
function. Awake
is called as soon as the class is initialized or as soon as the game starts running because the class will exist on the LevelPieces, which will be in the world when the game starts.
We don't need the Update
function, which is why it was removed from the class.
GetInitialLocation
is a function that we can use from the next C# class that we will write so that it can manage the LevelPiece GameObject(s) by getting that location and moving the LevelPiece to it after we have used it.
The difference between Awake and Start is that Awake gets called before the Update or Start function is called. Then, Start gets called following Awake, but still before the first Update call. This means the order of functions getting called when the class is created is Awake->Start->Update. As the next class we will use will immediately start its Update function, we need to make sure that InitialLocation of LevelPiece is assigned before its location gets moved.
We now have everything we need to begin piecing the LevelPieces together with code.
Perform the following steps:
Assets/Scripts
folder and select Create. Then, select C# Script. Name it LevelPieceManager
.Vector3
position to check against. In Hierarchy, right-click and select Create Empty. Name this LevelPieceManager
.LevelPieceManager
and the Inspector | Transform window as -7.75
. This will offset LevelPieceManager
so that when the game starts, the character will have a longer flat path to run along before reaching the first attached LevelPiece.LevelPieceManager
and select LevelPieceManager.LevelPieceManager
script file in the Assets/Scripts
folder, open it, and add the following code to it:using UnityEngine; using System.Collections; public class LevelPieceManager : MonoBehaviour { // Starting level piece public LevelPiece StartingLevelPiece; // Level pieces to rotate public LevelPiece[] LevelPieces; // How quickly the level pieces move public float LevelPiecesMoveRate; // The currently active Level Piece private LevelPiece[] ActiveLevelPieces; // Use this for initialization void Start() { } // Update is called once per frame void Update() { } }
LevelPieceManager
is a class that will connect all the LevelPieces
together and move them across the screen, simulating that the player is running forward. This is a design decision on my end. Although Unity does have what you can call a "scene size limit", my decision here is to try to keep the organization of the scene to a smaller area, hopefully giving you a more relaxed scene to work in.
The current code is explained here:
StartingLevelPiece
: This is the initial LevelPiece GameObject. It is a flat portion to get the game started without having to worry about placing anything as soon as the game starts.LevelPieces
: This is an array that will hold the other LevelPiece
prefabs we created. It is the list that will be used to assign a new piece to the actively moving pieces when another piece has reached a point where it is no longer needed, that is, when it is out of view of the player.LevelPiecesMoveRate
: This is the speed at which the LevelPiece GameObjects will be moving across the screen.ActiveLevelPieces
: This is a list of LevelPiece GameObjects that will move across the screen. It is set to private to prevent any other class from accessing and/or changing it.Next, we want to add the code for the Start
function. Add the following code to the LevelPieceManager
class:
// Use this for initialization void Start( ) { ActiveLevelPieces = new LevelPiece[2]; ActiveLevelPieces[0] = StartingLevelPiece; ActiveLevelPieces[1] = GetRandomLevelPiece(); ActiveLevelPieces[1].transform.position = StartingLevelPiece.gameObject.transform.FindChild("EndLocation").position; }
The Start
function is called as soon as this class is active in the scene, just after Awake
and just before Update
, giving us room to assign some values before anything else happens in this class.
To begin with, we will create an array of two LevelPiece
classes and assign it to ActiveLevelPieces
.
Then, we will give the first element in the ActiveLevelPiece
array the value of StartingLevelPiece
because it is the first LevelPiece we want to use.
We will then get a "random" LevelPiece from the GetRandomLevelPiece
function, which we will write soon; it is going to cycle through the array of LevelPieces
from the scene. Make sure that the one we want to use isn't currently active.
After we have the random LevelPiece, we have to assign its location to the EndLocation of the StartingLevelPiece. This is done by using the transform.FindChild
function. This function will look for the name of the Transform child, which in our case is EndLocation. When we have this, we can use the transform position of EndLocation and assign the random LevelPiece
position to it.
Earlier, I mentioned why the LevelPiece
class uses Awake
instead of Start
. In the case of LevelPieceManager
, we want to make sure that the LevelPiece
class is done with all its assignments before LevelPieceManager
begins to do anything, which will happen because the Awake
function of LevelPiece will be called before the Start
function of LevelPieceManager
, giving us the order of LevelPiece Awake
, LevelPieceManager Start
, LevelPieceManager Update
.
Next, we want to write the Update
function for LevelPieceManager
. Add the following code:
// Update is called once per frame void Update ( ) { for (int i = 0; i < ActiveLevelPieces.Length; i++) { Vector3 newLocation = ActiveLevelPieces[ i ].transform.position; newLocation.x -= LevelPiecesMoveRate * Time.deltaTime; ActiveLevelPieces[ i ].transform.position = newLocation; if (ActiveLevelPieces[ i ].transform.position.x < transform.position.x) { if (ActiveLevelPieces[ i ] == StartingLevelPiece) { ActiveLevelPieces[ i ].gameObject.SetActive( false ); } ActiveLevelPieces[ i ].transform.position = ActiveLevelPieces[ i ].GetInitialLocation( ); ActiveLevelPieces[ i ] = GetRandomLevelPiece( ); ActiveLevelPieces[ i ].transform.position = FindOtherLevelPiece( ActiveLevelPieces[ i ] ).gameObject.transform.FindChild( "EndLocation" ).position; } } }
As mentioned before, the Update
function is called for every frame that is rendered. This means that if your game is running at thirty frames per second, the Update function gets called thirty times a second as well.
As we will always have two ActiveLevelPieces
and want them to move together, we will use a for
loop that will loop through these ActiveLevelPieces
, which if you recall is two.
For each one of these
ActiveLevelPieces
, we first want to get their location, which we named newLocation
.
We then want to change the location.x
value of this newLocation
by LevelPieceMoveRate
multiplied by the Time.deltaTime
value, which is the time it took to render the last frame to this frame.
When we have newLocation
assigned, we then want to update the location of the current ActiveLevelPiece
element to the newLocation Vector3
value, which will move the ActiveLevelPiece
element.
We then want to check whether the ActiveLevelPiece
element has passed the bounds of the LevelPieceManager GameObject position.x
value. If it has, we check whether the ActiveLevelPiece
element is StartingLevelPiece
. If it is, we know that we don't want to use it again, so we will use the SetActive
function to hide it by saying that it is not active anymore.
As the ActiveLevelPiece
element has reached the point where it no longer needs to be used, we will reset its location to its InitialLocation
with the GetInitialLocation
function written in the LevelPiece
class.
After it has been reset, we will use the GetRandomLevelPiece
function so that we can begin to use an available LevelPiece. As mentioned before, this function returns a LevelPiece that is currently not being used.
After we have our next LevelPiece, we then want to set its location to the EndLocation
of the other ActiveLevelPiece
, which is done with a function named FindOtherLevelPiece
, in which we pass the current ActiveLevelPiece
to compare against the rest of them, making sure that we don't accidentally return the ActiveLevelPiece
that needs replacement.
Let's simplify what is going on here. We are taking a location of GameObject and offsetting it a small amount and then assigning the GameObject position to the new offset location. If this offset location is outside the bounds of what we have set, we can replace it with a new GameObject that begins the cycle all over again.
Next, we need the
FindOtherLevelPiece
function. Add the following function under Update
:
// Get the other LevelPiece // from the LevelPieces // Array private LevelPiece FindOtherLevelPiece(LevelPiece CurrentLevelPiece) { for (int i = 0; i < ActiveLevelPieces.Length; i++) { if (ActiveLevelPieces[ i ] != CurrentLevelPiece) { return ActiveLevelPieces[ i ]; } } return null; }
This function is simple. We check the value of CurrentLevelPiece
against the array elements of ActiveLevelPieces
. As soon as we find that the ActiveLevelPieces
array element is not equal to CurrentLevelPiece
, we will return it. This means that we found the other ActiveLevelPieces
array element (as there are only two). Also, it doesn't match the CurrentLevelPiece
argument.
Next, we need to write the GetRandomLevelPiece
function. Add the following function under
FindOtherLevelPiece
:
// Get random level piece // from LevelPieces Array private LevelPiece GetRandomLevelPiece() { LevelPiece returnPiece = null; while (returnPiece == null) { for (int i = 0; i < LevelPieces.Length; i++) { if ( !isActivePiece( LevelPieces[ i ] ) ) { returnPiece = LevelPieces[ i ]; } } } return returnPiece; }
As mentioned before, the while
loop will continue to loop until the condition is no longer true
. In our case, we want to make sure that the loop stops when returnPiece
has a value or is not equal to null.
For this, let's first create a for
loop that loops through LevelPieces
. These are the LevelPiece prefab GameObjects in our scene. We then check whether the LevelPieces
array element is not active by passing it to a function called isActivePiece
and then checking whether this function returns false
.
When we have the LevelPieces
array element that is not being used, we can assign its returnPiece
to that LevelPieces
array element. Once this is done, the while
loop condition will become false
and then the next line after the while
loop returns the returnPiece
found.
Simply put, we will look at all the LevelPieces
and return the first one that is not being used.
The last function we need to write is the isAlreadyActive
function. This will return true if the piece is active and false if it is not active. Under the GetRandomLevelPiece
function, add the isActivePiece
function:
// Check if LevelPiece // is already used. private bool isActivePiece(LevelPiece Piece) { for (int i = 0; i < ActiveLevelPieces.Length; i++) { if (Piece == ActiveLevelPieces[ i ]) { return true; } } return false; }
Similar to the FindOtherLevelPiece
function, we loop through the ActiveLevelPieces
array and check whether the Piece
argument is passed. If any of the ActiveLevelPieces
array elements match the Piece
argument being passed, the function will return true because the condition is true. If the function loops through the entire ActiveLevelPieces
array and the Piece
argument doesn't match any of them, the function will return false because the condition in the for
loop was never true.
This is the
LevelPieceManager
class. If you save and open/tab back over to Unity and then compile, you can set up the LevelPieceManager settings.
If not already created, right-click on the Hierarchy tab and select Create Empty. Rename this GameObject as LevelPieceManager
.
With the LevelPieceManager GameObject selected, click on Add Component in the Inspector window.
Search for LevelPieceManager and select LevelPieceManager to add the component.
With the LevelPieceManager GameObject still selected, change its X position to -7.75 using the Inspector | Transform window. The Y and Z locations don't matter, although you can set them both to zero if you want to keep things organized.
Perform the following steps:
0X
, 0Y
, and 0Z
by selecting it from Hierarchy and using the Inspector | Transform settings.LevelPieces
of the LevelPieceManager GameObject.0
to 1.35
:If you play the game now, you should see LevelPieces
moving and connecting to each other, simulating that the player character is moving. You will also see that at times, the player character is getting pushed backwards by the slope of LevelPieces
. To fix this, open the Character
script from the Assets/Scripts
folder.
In the Update
function, before the condition that checks isFadeOut
, add the following code:
// Update is called once per frame void Update () { Vector3 lockXPosition = transform.position; lockXPosition.x = 2.25f; transform.position = lockXPosition; if (isFadeOut) {
If you play the game now, you will see that the character is locked in the X position of 2.5
, meaning that the slopes won't affect it anymore.
The next step is to add coins and obstacles to our LevelPiece prefabs. As our LevelPieceManager
can handle more than three
LevelPiece prefabs, you can create duplicates to have different coin and obstacle layouts. Alternatively, you can keep it simple and only use the current three LevelPiece prefabs.
Drag and drop the Coin and Obstacle prefabs onto the scene and position them around the LevelPiece prefabs. For each of the LevelPiece prefabs, create as many obstacles or coins as you want, and after they are placed, make sure to drag them onto the LevelPiece GameObject, making the obstacles and coins children of LevelPiece. When you are done, you should have something similar to the following screenshot:
If your Axe obstacle is rendering behind LevelPiece, left-click on the 2D button at the top of the Scene window.
You can use the following controls to navigate the scene:
Select the Axe GameObject that needs to be moved and use the move gizmo to move it in front of LevelPiece, as shown in the following screenshot:
In the Assets/Scripts
folder, double-click on the LevelPiece.cs
file to open it. Add the following code at the top of the class:
// Coin children public Coin[] Coins;
Save the LevelPiece.cs
file and tab back or open Unity to let the code compile. Start by selecting one of the LevelPiece GameObjects in Hierarchy, and in the top-right corner of the Inspector window, click on the small lock; it should change from an open lock to a closed lock. Once it has, you can select all the coins from Hierarchy for the specific LevelPiece GameObject and drag them onto the LevelPiece component in the Inspector window; it will automatically fill and assign the Coins array, as shown in the following screenshot:
Do this for all the coins in all the LevelPiece GameObjects. Also, make sure to match them to the correct Coins
array. Remember that the lock will keep the LevelPiece GameObject selected until you unlock it by clicking on the lock again. You will have to do this for each LevelPiece.
In the Assets/Scripts
folder, open the Coin.cs
class. We will change how the coin is being used here. Earlier, we were destroying it after the player collided with it. We do not want this now because this was only useful for testing purposes. We now want the coin to be in the scene always and managed in a way where it can be reactivated when LevelPiece becomes active.
In the OnTriggerEnter2D
function, remove the following line:
Destroy( gameObject );
Then, replace it with the following code:
ActivateCoin( false );
Now, we need to write the ActivateCoin
function. Add the following code under OntriggerEnter2D
:
// Activates or deactivates // coin public void ActivateCoin(bool bActivate) { SpriteRenderer renderer; BoxCollider2D collider; renderer = gameObject.GetComponent<SpriteRenderer>( ); collider = gameObject.GetComponent<BoxCollider2D>( ); collider.enabled = bActivate; renderer.enabled = bActivate; }
We first have the SpriteRenderer and Box Collider 2D components for the coin and then set them to the value of bActivate
. This means that if the character collects the coin, the bActivate value will be false. This is because we don't want it activated after being collected, which will set enabled to false for both the collider and the renderer. Whereas, if we reset the game, we can call this same function but set bActivate to true. This will turn enabled to true for both the collider and renderer, making it so that the character can collect it again.
Furthermore, this function allows you to control the coin after it has been used so that it can be used again by setting the Box Collider 2D and SpriteRenderer components to enabled or not enabled. By setting these two components to not enabled, they won't react to the player character anymore, but if we set them to enabled, the player can again interact with them. This is good for us because we need to enable them every time LevelPiece gets used, although it has been used before.
Save this Coin.cs
file.
In the Assets/Scripts
folder, open the LevelPiece
class. At the bottom of the class, add the following function:
// Resets all children // coins public void ResetAllChildrenCoins() { for (int i = 0; i < Coins.Length; i++) { Coins[ i ].ActivateCoin( true ); } }
This does exactly as the function describes. It loops through all the coins that we added from the Hierarchy tab to the LevelPiece
script component and calls the ActivateCoin
function we wrote early, setting the enabled bool value to true
. This is what we will pass in ActivateCoin
.
Then, we need to call the ResetAllChildrenCoins
function from LevelPieceManager
, when it decides that it needs a new LevelPiece. In the Assets/Scripts
folder, open the LevelPieceManager
code file.
Change the Update
function as follows:
// Update is called once per frame void Update ( ) { for (int i = 0; i < ActiveLevelPieces.Length; i++) { Vector3 newLocation = ActiveLevelPieces[ i ].transform.position; newLocation.x -= LevelPiecesMoveRate * Time.deltaTime; ActiveLevelPieces[ i ].transform.position = newLocation; if (ActiveLevelPieces[ i ].transform.position.x < transform.position.x) { if (ActiveLevelPieces[ i ] == StartingLevelPiece) { ActiveLevelPieces[ i ].gameObject.SetActive( false ); } ActiveLevelPieces[ i ].transform.position = ActiveLevelPieces[ i ].GetInitialLocation( ); ActiveLevelPieces[ i ] = GetRandomLevelPiece( ); ActiveLevelPieces[ i ].transform.position = FindOtherLevelPiece( ActiveLevelPieces[ i ] ).gameObject.transform.FindChild( "EndLocation" ).position; ActiveLevelPieces[ i ].ResetAllChildrenCoins( ); } } }
The only change here is the last line. When we have a new ActiveLevelPiece
and when we update its location, we call ResetAllChildrenCoins
for the ActiveLevelPiece
so that the ActiveLevelPiece
is fully playable again as all the coins are back to being able to be collected.
Save the
LevelPieceManager.cs
file and go back to Unity. After the code has compiled, you should see your entire gameplay going, including all the LevelPieces
moving, connecting, and resetting.