In this chapter, we will go through the steps to get the final aspects of the iOS integration function and set up the main menu UI so that the player can navigate between playing the game, view leaderboards /achievements, and have the option to purchase "remove iAds" for the cost of ten thousand coins or 99 cents. We will also display iAds if the player has not purchased the remove iAds.
Similar to the last chapter, this one will have a fair bit of the tedious UI work. As it is difficult to explain the workflow, I will give you the exact locations and anchor points for the UI buttons and images. These locations are not definitive, meaning you can change them however you feel. Also, it won't affect the players experience with the UI because the logic for them is based in code. Feel free to take the liberty with the UI design if you are comfortable with it.
iAds, Leaderboards, and Achievements are all handled with the iOS plugin. Sadly, Unity has not integrated the native iOS functionality into the engine, and the iOS plugin is required to call the setting and get information for leaderboards and achievements and when and how to display the iAds. As I mentioned in the first chapter, I will use the plugin called IOS Native. It is on the Unity asset store for twenty dollars. If you do not want to spend that much money, there are some other options, although I have found that IOS Native is one of the better ones.
We will also need to expand our existing class in order to fill the gaps to save information regarding achievements, leaderboards, and store purchases. Apple also requires a restore purchase option.
In this chapter, we will cover the following topics:
LevelPieceManager
so that the character continues to run on a flat ground until the player chooses to start playingThe main menu UI will be its own Canvas GameObject. We will then handle the main menu and the game UI via the GameInfo
class. We will also use the GameInfo
class to manage button presses and the iOS integration.
In Hierarchy, right-click and select UI and then click on Canvas. Name this new Canvas GameObject MenuUI
.
Let's start by adding five buttons to achievements, playing, leaderboards, remove iAds, and restore purchase.
Right-click on the new MenuUI GameObject, navigate to UI, and left-click on Button. Do this four more times, so there are a total of five buttons that are children of the Menu UI GameObject.
Name the buttons and text children as follows:
PlayButton
, PlayText
LeaderboardButton
, LeaderboardText
AchievementButton
, AchievementText
RemoveAdsButton
, RemoveAdsText
RestorePurchaseButton
, RestorePurchaseText
Next, we need to import the art that will be used for the main menu UI. In the Assets/UI
folder, right-click and select Import New Asset…. Navigate to the Art
folder for this book and search for the chapter labeled ChapterSix_MenuUI
. Import all the images into this folder.
Select all the new images in the Assets/UI
folder and change their settings as follows:
256
Select PlayButton in Hierarchy and search for Inspector. Change its settings as follows:
0
115
0
128
128
MenuButton
Now, select PlayButtonText. In the Inspector window, change its settings as follows:
Select LeaderboardButton in the Hierarchy tab and search for Inspector. Change its settings as follows:
135
115
0
128
128
MenuButton
Select LeaderboardText. In the Inspector window, change its settings to:
Select AchievementButton. In Hierarchy, search for Inspector. Change its settings as follows:
-135
115
0
128
128
MenuButton
Now, select AchievementText and then in Inspector, change its settings to:
Select RemoveAdsButton in the Hierarchy tab and navigate to Inspector. Change its settings as follows:
-64
55
0
96
42
RestartButton
Now, select RemoveAdsText and then in the Inspector window, change its settings as shown here:
Let's select RestorePurchaseButton in the Hierarchy tab and search for Inspector. Change its settings as follows:
64
55
0
96
42
RestartButton
Now, select RestorePurchaseText and then in the Inspector window, change its settings as follows:
You should now have a button layout that looks similar to the following image:
We need to create a few panels that will show when the player clicks on RemoveAdsButton and what selection they choose from here. The selection to purchase remove iAds with ten thousand coins will check whether the player has that many. If they don't, we will display an error panel that tells how many coins they have and that the purchase has failed. We also need a panel to show when the player has successfully purchased the remove iAds purchase.
RemoveAdsBackgroundScreen
.RemoveAdsScreen
.RemoveAdsTextOption
.ForCoin
. Do this twice more. Name the next button ForCash
and the last button CloseRemoveAds
.ForCoinText
.ForCashText
.CloseRemoveAds
.Select RemoveAdsBackgroundScreen in the Hierarchy tab and navigate to the Inspector window. Change its settings as follows:
0
0
0
265
180
Background
Select RemoveAdsScreen in Hierarchy and search for Inspector. Change its settings as follows:
0
0
0
256
171
32
—red, 32
—green, 32
—blue, 255
—alphaThe next window we need is the panel that shows when the purchase succeeded.
To make your work easy, select RemoveAdsBackgroundScreen and then in the Inspector window, click on the checkbox next to its name in order to disable the GameObject. This will hide it from the scene, so when we create the next panel, they won't overlap.
PurchaseSucceededBackgroundScreen
.PurchaseSucceededScreen
.PurchaseSucceededText
.PurchaseSucceededAccept
. Name its Text child as PurchaseSucceededAcceptText
.PurchaseSucceededClose
and delete its Text child.We need the exact same window for our failed window. There are two ways we could handle this. The first and more involved way is to use the succeeded window and change the text, so when something fails, we say it failed. The other way is to duplicate the same window and rename everything that says "Succeeded" to "Failed". For the sake of ease, go ahead and left-click on PurchaseSucceededBackgroundScreen in Hierarchy to select it.
Then, click on Ctrl + D. This will duplicate the GameObject. In this duplicated GameObject, rename everything from Succeeded to Failed
, including the text component of PurchaseFailedText.
You should now have two separate Panel GameObjects in your Hierarchy, as shown in the following screenshot:
We now need to write the code for the buttons of the PurchaseSucceeded panel and the PurhcaseFailed panel.
In the Assets/Scripts
folder, double-click on the GameInfo
class to open it.
At the top of the
GameInfo
class, add the following variables:
// Reference to the RemoveAds screen private Transform RemoveAdsBackgroundScreen; // Reference to the Purchase screen private Transform PurchaseSucceededScreen; // Reference to the Purchase failed screen private Transform PurchaseFailedScreen;
Then, in the Start
function, add the following code:
// Called after Awake but before Update void Start( ) { if ( MainMenuUI != null ) { RemoveAdsBackgroundScreen = MainMenuUI.transform.Find( "RemoveAdsBackgroundScreen" ); PurchaseSucceededScreen = MainMenuUI.transform.Find( "PurchaseSucceededBackgroundScreen" ); PurchaseFailedScreen = MainMenuUI.transform.Find( "PurchaseFailedBackgroundScreen" ); } }
Under HideRestartButton
, write the following functions:
// Show or hide Purchase Window public void ShowPurchaseScreen( bool bShow ) { if (RemoveAdsBackgroundScreen != null) { RemoveAdsBackgroundScreen.gameObject.SetActive( bShow ); } } // Show or hide Success Window public void ShowSuccessScreen( bool bShow ) { if (PurchaseSucceededScreen != null) { PurchaseSucceededScreen.gameObject.SetActive( bShow ); } } // Show or hide Fail Window public void ShowFailScreen( bool bShow ) { if (PurchaseFailedScreen != null) { PurchaseFailedScreen.gameObject.SetActive( bShow ); } }
All three of these functions will handle their connected screen in the same way. If the bShow
bool value is set to true
, it will show the associated GameObject. If bShow
is set to false
, it will hide it. This is done using the SetActive
function.
Now, save the GameInfo
class.
We now need to make sure that the buttons in the MenuUI are using the correct function calls.
Let's go back to Unity and select the CloseRemoveAds GameObject in the MenuUI | RemoveAdsBackgroundScreen GameObject. In Inspector, search for the Button
component and click on the small plus symbol to add an element to the On Click () list. Left-click and drag the GameInfo GameObject onto the object field and then select the drop-down menu located at the right. Navigate to GameInfo and then select the ShowPurchaseScreen
function. Leave the small checkbox under the drop-down list unchecked.
Select the PurchaseSucceededAccept GameObject in the MenuUI | PurchaseSucceededBackgroundScreen GameObject. Again, navigate to the Inspector window. In the Button component, click on the small plus symbol to add an element to the On Click () list. Then, left-click and drag the GameInfo GameObject onto the object field. From the drop-down list, select GameInfo and then the ShowSuccessScreen function. Again, leave the small checkbox unchecked.
Do this again for the PurchaseSucceededClose GameObject, also a child of the PurchaseSucceededBackgroundScreen GameObject, which is in the MenuUI GameObject. Use the same ShowSuccessScreen function and leave the small checkbox unchecked.
Follow the same steps for PurchaseFailedBackgroundScreen, but instead of selecting ShowSuccessScreen, select ShowFailedScreen. Also, do this for the PurchaseFailedClose button.
At the top of the GameInfo
class, add the following variable:
// MainMenu UI reference public Canvas MainMenuUI;
Next, select the GameInfo GameObject in Hierarchy by left-clicking on it. Then, left-click and drag MenuUI onto the Main Menu UI object slot of the GameInfo GameObject in Inspector.
Then, select the GameUI GameObject in Hierarchy. If it is not already hidden, click on the checkbox at the top of the Inspector window near the GameUI name to hide it.
Now, select the children GameObjects in MenuUI named RemoveAdsBackgroundScreen, PurchaseSucceededBackgroundScreen, and PurchaseFailedBackgroundScreen. Then, use the checkbox at the top of the Inspector window near their names and hide them.
The menu should now only show the Achievements, Play, Leaderboards, Remove iAds, and Restore Purchase buttons.
My goal is to have the code compile at every section of this book. As of now, many of the alterations in this section will feel disorganized. It was a judgment call on my end to not have you do work on something that will break everything until a later chapter, which is why in this chapter, we will go back to some C# classes in order to expand or change the existing code within them. Although it is not uncommon for this sort of thing to happen while creating a video game, I know that it can be a bit irritating when you learn to have some back and forth.
For the main menu to have movement without any game play, we need to set a few things so that the level pieces are used only when they are needed. In the Assets/Scripts
folder, open the GameInfo
class by double-clicking on it.
At the top of the GameInfo
class, add the following global variables:
// If game is running or at menu idle [System.NonSerialized] public bool bGameRunning; // GameUI reference public Canvas GameUI;
Next, we need a function to call when Start button is pressed by the player. This button will transition MainMenuUI over to GameUI as well as change the way the LevelPieces are being connected. At the bottom of the GameInfo
class, add the following function:
// Game state button was pressed public void GameStateButtonPressed( bool bRunning ) { bGameRunning = bRunning; if (MainMenuUI != null) { MainMenuUI.gameObject.SetActive(!bRunning); } if (GameUI != null) { GameUI.gameObject.SetActive(bRunning); } RestartGame( ); }
This function will be used by PlayButton
and a button that we will create to return to the main menu. As our game state is either true or false, we can use the single function and enter what type of game state we are looking for. For PlayButton
, we want the game state to be set to true. For the menu button, we will set it to false.
In the last chapter, we saw how the RestartGame
function began the cycle of fading the screen to black and then back to transparent. At the midpoint of going back to transparent, we can restart the character and the LevelPieces
. As of the new flow of getting into the gameplay, we also need to update the RestartLevelAndCharacter
function so that it sends the correct information to LevelManager
. Change the RestartLevelAndCharacter
function as follows:
// Restart level and character private void RestartLevelAndCharacter( ) { bLevelAndCharacterRestart = false; if ( GameCharacter != null ) { GameCharacter.ReviveCharacter( ); } if ( LevelManager != null ) { LevelManager.ResetLevelPieces( bGameRunning ); } }
We will now pass the bGameRunning
bool value to the RestartLevelPieces
function of the LevelManager. To accommodate the new bool value, navigate to the Assets/Scripts
folder and double-click on the LevelPieceManager
C# file to open it.
Before we change the RestartLevelPieces
function, we need to add a couple of global variables to the class. At the top of the LevelPieceManager
class, add the following code:
[System.NonSerialized] // If the game is being played or at main menu public bool bGameRunning; // MainMenu level piece public LevelPiece IdleLevelPiece;
From the LevelPieceManager
perspective, bGameRunning
will handle the current state of the game. IdleLevelPiece
will be the secondary LevelPiece
that we will use during the menu portion of the game (where the character runs forever).
We need to change the Start
function as well because it was handling ActiveLevelPieces
too directly for what our game needs now. Change the Start
function as follows:
// Use this for initialization void Start( ) { ActiveLevelPieces = new LevelPiece[2]; ResetLevelPieces( bGameRunning ); }
This now only creates the ActiveLevelPieces
array and then uses the RestartLevelPieces
function to begin running the game in the correct state. As bool values default to false, we know that when we call this using Start
, the game will start in the idle mode.
Change the
RestartLevelPieces
function in the LevelPieceManager
class, as shown in the following code:
// Resets all LevelPieces public void ResetLevelPieces( bool bRunning ) { bGameRunning = bRunning; StartingLevelPiece.transform.position = StartingLevelPiece.GetInitialLocation(); StartingLevelPiece.gameObject.SetActive(true); IdleLevelPiece.gameObject.SetActive( !bGameRunning ); for (int i = 0; i < LevelPieces.Length; i++) { LevelPieces[i].transform.position = LevelPieces[i].GetInitialLocation(); LevelPieces[i].ResetAllChildrenCoins(); } if (bGameRunning) { SetGamePieces(); } else { SetIdlePieces(); } }
You'll see here that this class also has a bGameRunning
bool value. This is because LevelPieceManager
is in sync with GameInfo
. We can also reset StartingLevelPiece
and make sure that it can be seen because this piece is used in both the menu and game states. We can then set IdleLevelPiece
so that it is only active if the bGameRunning
bool is set to false
. We do not want to use it when the game is running.
No matter what the game state is, we need to reset the LevelPieces
before running SetIdlePieces
or SetGamePieces
. This way, the LevelPieces
are out of the view of the player until they are needed, which is handled in the Update
function. We accomplish this with the for
loop that sets the LevelPieces
to their GetInitialLocation
and also resets all of their children, coins, and obstacles so that they can be used again by the player.
This function also has two more function calls: SetGamePieces
and SetIdlePieces
. These two functions are designed to make the ActiveLevelPieces
fit to whichever game state we are in: idle or game.
In the Assets/Scripts
folder, double-click on the LevelPiece
code file to open it. At the top of the class, add the following global variable:
// End location of Level Piece public Transform EndLocation;
This will give us a direct reference to the EndLocation Transform
so that we don't have to find it every time we use it.
Save this file and go back to Unity. Select each of the LevelPieces
, take the GameObject with the name of EndLocation in Hierarchy, and left-click and drag it onto the new EndLocation object field of the LevelPiece
. Do this for all the LevelPiece
GameObjects in the scene.
Back in the LevelPieceManager
, under the Start
function, write the follow functions:
// Set ActiveLevelPieces to Idle void SetIdlePieces( ) { ActiveLevelPieces[ 0 ] = StartingLevelPiece; ActiveLevelPieces[ 1 ] = IdleLevelPiece; ActiveLevelPieces[ 1 ].transform.position = StartingLevelPiece.EndLocation.position; } // Set ActiveLevelPieces to Game void SetGamePieces( ) { ActiveLevelPieces[0] = StartingLevelPiece; ActiveLevelPieces[1] = GetRandomLevelPiece(); ActiveLevelPieces[1].transform.position = StartingLevelPiece.EndLocation.position; }
SetIdlePieces
takes StartingLevelPiece
and IdleLevelPiece
and assigns them to the zero and one elements of ActiveLevelPieces
. It also takes IdleLevelPiece
and moves it to the EndLocation
of the first element of ActiveLevelPieces
, which is more easily referred to as StartingLevelPiece
.
SetGamePieces
does the same thing as we had the Start
function doing earlier. It assigns the first element of ActiveLevelPieces
to StartingLevelPiece
and then uses GetRandomLevelPiece
to assign the next element. It then assigns the return LevelPiece
from the GetRandomLevelPiece
position to the EndLocation
of StartingLevelPiece
.
These two functions behave very similarly. The only difference is what's being assigned.
We now need to change the Update
function to change between moving the game LevelPieces
and the idle LevelPieces
. Change the current 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 (bGameRunning) { 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]).EndLocation.position; ActiveLevelPieces[i].ResetAllChildrenCoins(); } else { LevelPiece nextLevelPiece = ( i == 0 ) ? ActiveLevelPieces[ 1 ] : ActiveLevelPieces[ 0 ]; ActiveLevelPieces[ i ].transform.position = nextLevelPiece.EndLocation.position; } } } }
You'll see that this time, we are using the bGameRunning
bool to decide how to transition between the ActiveLevelPieces
. Luckily, the menu only uses two, so if bGameRunning
is set to false, we can assign nextLevelPiece
with the opposite of what element i
represents. As we know that there are only two ActiveLevelPieces
, if the game is not running, we can use the value of i
to get the other ActiveLevelPieces
element, using the ternary operator to check whether I is equal to zero. If it is, we will assign nextLevelPiece
to the second element of the ActiveLevelPieces
array. If it isn't, we will assign nextLevelPiece
to the first element of the ActiveLevelPieces
array.
Whichever piece gets chosen as nextLevelPiece
, we set the current i
value of the ActiveLevelPiece
array position to the end of it. This takes the ActiveLevelPiece
that has gone out of viewpoint of the player and moves it back so that it can be used again.
The code used if bGameRunning
is set to true is the same as we had before; if a piece gets out of view of the player, we can choose another with GetRandomLevelPiece
, reset it to default, and attach it at the end of the moving ActiveLevelPieces
so that it can be used again.
Now, save the LevelPieceManager
class.
The last step to getting the idle menu code working is adding it to the character so that it is aware when it should be counting up distance and when to accept input.
In the Assets/Scripts
folder, open the Character
C# file by double-clicking on it. At the top of the class, add the following global variable:
public LevelPieceManager LevelManager;
As there is a delay fading the GameInfo
class, we will use the bGameRunning
bool LevelPieceManager
as the more accurate one before the LevelPieceManager
becomes aware that the game is running. This way, the distance calculations and the input will be more in line with when the player starts to see the game scene.
We now need to use the LevelManager.bGameRunning
bool value in a couple of places. Navigate to the AddDistance
function and change if (GameUI != null)
to if (GameUI != null && LevelManager.bGameRunning)
.
This will prevent any distance calculations before the game is set to running. Next, navigate to the RecieveInput
function and change if (characterRigidBody != null && characterAnimator != null)
to if (characterRigidBody != null && characterAnimator != null && LevelManager.bGameRunning)
.
This again prevents any player input from manipulating the character unless the game is running.
Now, save the Character
class.
For the changes to be complete, we need to add the new references and GameObjects to the scene.
Open the Assets/Prefabs
folder and left-click and drag the StartingLevelPiece
prefab onto the scene. Move it away from the view of the camera; I put mine near the other LevelPieces
. In Hierarchy, rename it as IdleLevelPiece
.
Select the LevelPieceManager GameObject in Hierarchy and then left-click and drag the new IdleLevelPiece GameObject from Hierarchy onto the Idle Level Piece object slot of LevelPieceManager in the Inspector window.
Select the Character GameObject from Hierarchy by left-clicking on it. Then, left-click and drag the LevelPieceManager onto the Level Manager object slot of Character in the Inspector window.
Select the PlayButton GameObject under the MenuUI GameObject. In the Button component, in the Inspector window, click on the small plus sign to add a new On Click () list element. Left-click and drag the GameInfo GameObject from Hierarchy and move it into the object slot of the On Click list element. Left-click the drop-down menu for the On Click () list and select GameInfo and then select GameStateButtonPressed. Also, for PlayButton, we need to tick the small box that appears under the On Click () drop-down menu because we want PlayButton to enter true for the game state to change to true so that the LevelPieceManager and GameInfo will know that the game is running.
You should now be able to play the game and see the character run forever on the menu, but when you press play, MenuUI will disappear and GameUI will show. The screen will also fade to black and back to transparent, and the game will start normally with the LevelPieces
being put together and reset as the game continues. If the character dies, the restart button will be shown.
The next step is to allow the player to go back to the menu from the game when they want to. In Hierarchy, select MenuUI and use the checkbox next to its name in Inspector to hide it from the scene. Then, do the same for GameUI, but instead of hiding it, it shows it in the scene.
With the GameUI GameObject still selected in Hierarchy, right-click on it and navigate to UI and then select Button. Name this button BackToMenuButton
. Name its child Text GameObject BackToMenuText
.
Now, change the BackToMenuButton settings in Inspector as follows:
0
158.5
0
128
64
RestartButton
Now, change the BackToMenuText settings in the Inspector as follows:
255
—red, 255
—green, 255
—blue, 255
—alphaYou should now have another button before the Restart button with the Menu text, as shown in the following image:
We now need to update the HideRestartButton
function in the GameInfo
class so that it also hides or shows the Menu button with it. In the Assets/Scripts
folder, open the GameInfo
C# class by double-clicking on it.
Find HideRestartButton
and change it, as shown in the following code:
// Shows or hides the restart butt public void HideRestartButton(bool bHide) { if (GameUI != null) { GameUI.transform.Find( "RestartButton" ).gameObject.SetActive( !bHide ); GameUI.transform.Find( "BackToMenuButton" ).gameObject.SetActive( !bHide ); } }
Save the GameInfo
class and go back to Unity.
As we wrote the
GameStateButtonPressed
function earlier, we can also use it for the BackToMenu
button.
Select BackToMenuButton in Hierarchy under the GameUI GameObject. In Inspector, click on the small plus symbol to add an element to the On Click () list.
Then, left-click and drag the GameInfo GameObject from Hierarchy onto the On Click () object field for BackToMenuButton. In the drop-down list, navigate to GameInfo and select GameStateButtonPressed. Leave the checkbox that appears unchecked. We don't want this checked because we want to enter false so that the bGameRunning
value changes to false. This will call the SetIdlePieces
function and begin running the ActiveLevelPieces
for the menu in LevelPieceManager
.
Make sure that you hide the GameUI GameObject using the Inspector checkbox next to its name and unhide the MenuUI GameObject using the same checkbox before you run the game. If you do not do this, the first shown menu when the game starts will be of the GameUI, instead of the MenuUI.
You should now be able to click on Play button to start playing, Restart button to restart a game, and BackToMenu button to go back to the main menu.
In the Assets/Scripts
folder, double-click on the GameInfo
class to open it. Under the GameStateButtonPressed
function, add these two functions:
// Opens the leaderboards window public void ShowLeaderboards() { GameCenterManager.ShowLeaderboard( "G_Distance" ); } // Opens the achievements window public void ShowAchievements() { GameCenterManager.ShowAchievements( ); }
Both of these functions are very simple. As GameCenterManager
will manage most of the GameCenter
features, it includes the ShowLeaderboards
and ShowAchievements
functions that will open their related windows.
Save the GameInfo
class and go back to Unity.
Left-click on the Leaderboard button in Hierarchy under the MenuUI GameObject to select it. Search for Inspector. Now, in the Button component, click on the small plus symbol to add the On Click () elements. Left-click and drag the GameInfo GameObject onto the object field of the On Click () elements. Click on the drop-down menu that appears at the right-hand side, navigate to GameInfo, and select Show Leaderboard.
Left-click on Achievement Button in Hierarchy under the MenuUI GameObject to select it. Search for Inspector. Then, in the Button component, click on the small plus symbol to add the On Click () elements. Left-click and drag the GameInfo GameObject onto the object field for the On Click () elements. Click on the drop-down menu that appears at the right-hand side, navigate to GameInfo, and select Show Achievements.
This will allow the Leaderboard button and the Achievement button to open the windows so that the player can see the current state of each.
At this point, the final steps we need to take is to use the IOS Native plugin to call achievements, leaderboards, and store purchases.
If you don't remember, the three achievements we set up are:
10Rounds
100Pickups
100Yards
This means that we need to begin storing how many rounds the player has played with our PlayerPrefs
class. As we already know how many coins the player has, we don't need to save this, but we do need to have a condition so that we can award the achievement as soon as they collect 100 coins. Finally, we need to check how far the player has run every time we call the AddDistance
function in the Character
class to check whether they have reached 100 yards. If they have, we then can award the achievement.
In the Assets/Scripts
folder, double-click on the GameInfo
C# file to open it. At the top of the class, add the following enum
list:
public enum RunnerAchievements { RA_Rounds, RA_Pickups, RA_Yards, };
We will use this enum list to know which achievement to set the progress for or unlock.
Next, we need to set up GameCenter
so that it is running and available. Add the Awake
function so that it looks similar to the following code:
// When object starts void Awake( ) { if ( FadeObject != null ) { FadeObject.transform.position = new Vector3( 0.5f, 0.5f, 0.0f ); FadeTexture = FadeObject.GetComponent<GUITexture>( ); FadeTexture.pixelInset = new Rect (0.0f, 0.0f, Screen.width, Screen.height ); RestartGame( ); } // Register all achievements GameCenterManager.RegisterAchievement( "G_100Yards" ); GameCenterManager.RegisterAchievement( "G_100Pickups" ); GameCenterManager.RegisterAchievement( "G_10Rounds" ); // Delegates for Achievements GameCenterManager.Dispatcher.addEventListener( GameCenterManager.GAME_CENTER_ACHIEVEMENT_PROGRESS, OnAchievementProgress ); GameCenterManager.Dispatcher.addEventListener( GameCenterManager.GAME_CENTER_ACHIEVEMENTS_RESET, OnAchievementsReset ); // OnAchievementLoaded Delegate GameCenterManager.OnAchievementsLoaded += OnAchievementLoaded; // Init GameCenter GameCenterManager.init( ); // DEBUGGING ONLY -- REMOVE FOR FINAL BUILD // Reset Achievements GameCenterManager.ResetAchievements( ); }
The new code starts by using RegisterAchievement
, which takes in a string value. We do this so that we have access to these achievements, although they do not have any reported progress.
Next, we will do something that we haven't done yet. We will use the addEventListener
function to add a delegate function. A delegate is a way to call a function when something else happens. In our case, we will use it when we get the progress back from an achievement and when we reset achievements.
We then use the OnAchievementLoaded
delegate and assign it to GameCenterManager.OnAchievementLoaded
. Delegates look a bit strange because they are assigned with the +=
operator. What this will do is tell GameCenterManager
that when its OnAchievementLoaded
event is called to call the OnAchievement
loaded function in our GameInfo
class. This is the handle part of delegates; you don't have to check whether something has happened, but only assign what to do when it does happen.
After this, we can use the Init
function for GameCenterManager
. The Init
function starts GameCenterManager
so that the player gets logged in to GameCenter
, or if they haven't logged in before, the GameCenter
login screen will show for them.
Lastly, we will call the ResetAchievements
function. This is purely for testing purposes. This means that every time you load the game, the achievements will be reset. This is not ideal for the end user and should only be used for us, so we can test that everything is working. If we didn't reset the achievements, we could only unlock them once. This would become an issue if we wanted to check whether they can be unlocked.
In order to know what is going on with the achievements, we need some functions that get called when specific aspects of the achievement system gets used. At the bottom of the GameInfo
class, add the following function:
// When achievements are loaded private void OnAchievementLoaded(ISN_Result Result) { if (Result.IsSucceeded) { foreach (AchievementTemplate template in GameCenterManager.Achievements) { print( template.id + ": " + template.progress ); } } }
This function will let us know when an achievement has been loaded. Right now, it only prints out what the achievement ID is and its progress. If you were making a more complex game with a custom UI, you could use this to enter the information of the achievement that is being loaded.
Beneath the
OnAchievementLoaded
function, add the following code:
// When achievements are reset private void OnAchievementsReset() { // Achievements are reset }
Again, this is just another function that will be called when the achievements are reset. It doesn't do anything yet, but I wanted to add it for you if you would like to use it.
Next, add the following code under OnAchievementReset
:
// When achievements send progress private void OnAchievementProgress(CEvent Event) { ISN_AchievementProgressResult result = Event.data as ISN_AchievementProgressResult; if (result.IsSucceeded) { AchievementTemplate template = result.info; print( template.id + ": " + template.progress.ToString( ) ); } }
Once again, this function is only here if you want to use it. This will give you the achievement progress about the event data. It is doing nothing here but printing out the progress and ID that is being sent when its progress has changed. The CEvent
class as the function argument comes from the IOS Native plugin.
Finally, add the last of the event functions:
// Player is logged into GameCenter void OnAuthFinished(ISN_Result res) { if (res.IsSucceeded) { IOSNativePopUpManager.showMessage("Player Authored ", "ID: " + GameCenterManager.Player.PlayerId + " " + "Alias: " + GameCenterManager.Player.Alias); } else { IOSNativePopUpManager.showMessage("Game Center ", "Player auth failed"); } }
If ISN_Result
is successful, this function will let the player know that they have logged in to GameCenter
by displaying a message to them that they have (while in the game). If it is not, it will tell the player that the GameCenter
authentication has failed.
Next, we need to add the functions that we will use to set our achievements for our game. Add the following function under OnAchievementProgress
:
// Set achievement for RunnerGame public void SubmitAchievementProgress(RunnerAchievements Achievement, float AchievementValue ) { string lastAchievement = ""; switch (Achievement) { case RunnerAchievements.RA_Pickups: lastAchievement = "G_100Pickups"; break; case RunnerAchievements.RA_Rounds: lastAchievement = "G_10Rounds"; break; case RunnerAchievements.RA_Yards: lastAchievement = "G_100Yards"; break; } GameCenterManager.SubmitAchievement(GameCenterManager.GetAchievementProgress(lastAchievement) + AchievementValue, lastAchievement); }
As the SubmitAchievement
function takes a string, we need to take the enum value we have for our achievements and use it to create a string of the string ID of the achievements we set up in iTunes Connect.
We can use a switch statement to see what enum value was passed into the function and then based on that, assign LastAchievement
to the value of the string ID we set in iTunes Connect.
Based on the value of LastAchievement
, we can use the SubmitAchievement
function to submit the current progress value of the achievement plus the new value being passed. This will update the current progress of the achievement. If it reaches the value we set in iTunes Connect for when it unlocks, GameCenter
will send the user a broadcast banner that they have unlocked it.
Because of the way we unlock the distance achievement, we need a way to reset it back to zero if the player failed to reach the 100 distance marker. As a result, we don't want the achievement to continue to count upwards each attempt until the distance of 100 has been traveled and instead will only unlock if the player reaches the distance of 100 in a single attempt.
Under the SubmitAchievementProgress
function, add the following code:
// Instead of adding to an achievement progress value, set it as a whole public void SubmitAchievementAsWhole(RunnerAchievements Achievement, float AchievementWholeValue) { string lastAchievement = ""; switch (Achievement) { case RunnerAchievements.RA_Pickups: lastAchievement = "G_100Pickups"; break; case RunnerAchievements.RA_Rounds: lastAchievement = "G_10Rounds"; break; case RunnerAchievements.RA_Yards: lastAchievement = "G_100Yards"; break; } GameCenterManager.SubmitAchievement(AchievementWholeValue, lastAchievement); }
This function is very similar to the SubmitAchievementProgress
function, but instead of adding the existing progress of the achievement, we set a whole value to it or a complete value. If there is ever a point when you wanted to submit an achievement for a single action or all at once, this is a way to handle it. For the current game, it will be used to either reset the G_100Yards
achievement to zero or complete.
Next, we need a function to submit a leaderboard score. In the GameInfo
class, under the SubmitAchievementAsWhole
function, add the following code:
// Submits a leaderboard score public void SubmitLeaderboardScore(int DistanceScore) { GameCenterManager.ReportScore( DistanceScore, "G_Distance" ); }
This will submit an integer score to the leaderboard with the ID of G_Distance
, which is our only leaderboard.
We now need to use the Character
class to both keep track of the data we need, such as AttemptCount
, and call the GameInfo
achievement functions in order to update them.
At the top of the Character
class, add the following code:
[System.NonSerialized] public int AttemptCount;
This will be used to keep a tally on how many times the player has played the game.
Next, we need to save AttemptCount
with the PlayerPrefs
class. Change the Start
function so that it looks similar to the following code:
// Use this for initialization void Start () { RestartLocation = gameObject.transform.position; AttemptCount = PlayerPrefs.GetInt( "Attempts" ); CoinCount = PlayerPrefs.GetInt( "Coins" ); AddCoins( 0 ); }
We set the AttemptCount
to be set each time the character is loaded so that it is equal to what has been saved in PlayerPrefs
. This way, when we send it to the SubmitAchievement
function, it will get updated from the correct AttemptCount
value.
The last thing we need to do is to add the portion of code that will send the achievement data when the player is being revived.
Change the KillCharacter
function as follows:
// Kills the character public void KillCharacter() { Rigidbody2D characterRigidBody; if (!isDead) { characterRigidBody = gameObject.GetComponent<Rigidbody2D>(); if (characterRigidBody != null) { characterRigidBody.AddForce(new Vector2(Random.Range(-1, 1), 1) * 512); isDead = true; isFadeOut = true; if (Game != null) { Game.HideRestartButton(false); } PlayerPrefs.SetInt("Coins", CoinCount); PlayerPrefs.Save(); // Save attempts AttemptCount += 1; PlayerPrefs.SetInt("Attempts", AttemptCount); PlayerPrefs.Save(); if (Game != null) { Game.SubmitAchievementProgress(GameInfo.RunnerAchievements.RA_Rounds, AttemptCount); Game.SubmitAchievementAsWhole(GameInfo.RunnerAchievements.RA_Pickups, CoinCount); // If DistanceCount is less than 100 AND achievement hasn't been unlocked, reset it if (GameCenterManager.GetAchievementProgress("G_100Yards") < 100.0f && DistanceCount < 100) { Game.SubmitAchievementAsWhole(GameInfo.RunnerAchievements.RA_Yards, 0.0f); } // If DistanceCount is more than 100, set achievement as complete else { Game.SubmitAchievementProgress(GameInfo.RunnerAchievements.RA_Yards, DistanceCount); } Game.SubmitLeaderboardScore( DistanceCount ); } } } }
This moves on to what we already had at AttemptCount += 1
. From here, we set AttempCount
to PlayerPrefs
using the SetInt
function and then use the Save
function to save this value.
We then check to make sure that the reference to Game
exists before we submit the achievement progress using the SubmitAchievementProgress
function. We do this for AttemptCount
. We then use SubmitAchievementAsWhole
for CoinCount
because this progress will be a whole value from what is saved using PlayerPrefs
.
Before we set the distance achievement, we want to make sure that the progress of the distance achievement is less than one hundred and that the player had reached a DistanceCount
of
one hundred. If they didn't or if the achievement hasn't been unlocked, we reset its progress to zero. This way, if the player starts again, they won't be able to continue only from where they failed.
If the player did reach the one hundred distance, we can use the SubmitAchievementAsWhole
function to set the progress to the value of DistanceCount
, which will be at least one hundred, meaning they unlocked the achievement.
We can also call SubmitLeaderboardScore
that passes through DistanceCount
, so the leaderboard will reflect how far the player has gone as a record.
This is it for the achievement code. Make sure to save the Character
and GameInfo
classes.
As we want to display ads only to those who have not purchased to remove them, we need another value to save the PlayerPrefs
class. In the Assets/Scripts
folder, open the GameInfo
class by double-clicking on it.
At the bottom of the list of global variables, add the following code:
// If HideiAds has been unlocked private int HideiAds; // Reference to iAd banner private iAdBanner AdBanner;
As the PlayerPrefs
class doesn't store bool values, we have to store it as an int. We will use the default of zero to know that the player is going to see ads. Then, if they buy to remove them, we will save the HideiAds
value to one.
We also need to keep the reference to the created AdBanner
so that we can hide and show it, depending on the bGameRunning
value. If the bGameRunning
value is set to true
, we will hide the iAds so that it doesn't get in the way of GameUI.
At the bottom of the
GameInfo
class, write the following function:
// Creates instance of iAd if doesn't exist // Show or hide it depending on bShow public void ShowIAds(bool bShow) { HideiAds = PlayerPrefs.GetInt( "ShowiAds" ); if (HideiAds == 0) { if (AdBanner == null) { if (bShow) { AdBanner = iAdBannerController.instance.CreateAdBanner(TextAnchor.UpperCenter); AdBanner.Show(); AdBanner.AdViewFinishedAction += BannerLoaded; } } else { if (bShow) { AdBanner.ShowOnLoad = true; AdBanner.Show(); } else { AdBanner.ShowOnLoad = false; AdBanner.Hide(); } } } }
We start this function by assigning the value of HideiAds
to the PlayerPrefs
saved valued of it. If it's set to zero, when check whether the AdBanner
reference is null. We know that if it's null, we need to create it, which is what the code does if the banner is null and the bool value being passed is set to true. Once it's created, we show it using the Show
function. We also set a delegate so that we know when it's loaded. This is useful, so we can check whether the game is running when the banner has loaded. If the game is running, we can use the BannerLoaded
function to hide it.
If AdBanner
exists, we then check whether the bool value is set to true
or false
. If it's set to true
, we show the banner. If it' set to false
, we hide it.
Add the following function before the ShowIAds
function:
// Banner has loaded public void BannerLoaded() { if (bGameRunning) { AdBanner.Hide( ); } }
This is a very simple function that will be called when AdBanner
has been loaded. If the game is running, we hide it so that it doesn't get in the way of the gameplay.
We now need to add the GameStateButtonPressed
function so that it shows or hides the banner based on the value of bGameRunning
. Change the GameStateButtonPressed
function as follows:
// Game state button was pressed public void GameStateButtonPressed( bool bRunning ) { bGameRunning = bRunning; if (MainMenuUI != null) { MainMenuUI.gameObject.SetActive(!bRunning); } if (GameUI != null) { GameUI.gameObject.SetActive(bRunning); } ShowIAds( !bGameRunning ); RestartGame( ); }
The addition here is the ShowIAds
function, which passes the opposite value of bGameRunning
. More simply, it only shows the banner if bGameRunning
is set to false
.
Now, at the bottom of the Awake
function, add the following code:
ShowIAds( true );
This will try to show the
AdBanner
object as soon as the GameInfo
class starts running. It will fail if the HideiAds
value is not set to zero.
Save the GameInfo
class.
The first thing we need to do is finish setting up the iTunes Connect in-app purchases settings. Perform the following steps:
Art
files and search for the ChapterSix_InApp
folder.RemoveIAds.png
. (This will only work if you plan on using the UI we created. If you create your own game or your own UI art, you will need to include a screenshot for this.)In order for us to remove iAds for the cost of coins, we need to create another in-app purchase. Perform the following steps:
ChapterSix_InApp
, Remove IAds.png
, or what's suitable for you.We want this to be free because we will check to see how many coins the player has before purchasing. We also want the player to keep the purchase if they did use coins to buy it.
Go back to Unity. In the Assets/Scripts
folder, double-click on the GameInfo
class to open it. In the Awake
function, before the GameCenterManager
code, but under the FadeObject
code, add the following code:
// RemoveAds as Product IOSInAppPurchaseManager.instance.addProductId( "G_RemoveAds" ); IOSInAppPurchaseManager.instance.addProductId( "G_RemoveAdsCoins" ); // Add listening for Delegates IOSInAppPurchaseManager.instance.addEventListener( IOSInAppPurchaseManager.RESTORE_TRANSACTION_FAILED, OnRestoreTransactionFailed ); IOSInAppPurchaseManager.instance.addEventListener( IOSInAppPurchaseManager.VERIFICATION_RESPONSE, OnVerificationResponse ); // Assign delegates IOSInAppPurchaseManager.instance.OnStoreKitInitComplete += OnStoreKitComplete; IOSInAppPurchaseManager.instance.OnTransactionComplete += OnTransactionComplete; // Load the store. IOSInAppPurchaseManager.instance.loadStore( );
This is similar to what we did with the GameCenterManager
setup. The only difference this time is that it's specific for IOSInAppPurchaseManager
.
We start by adding a product ID, using addProductID
, which takes the name of our purchase: G_RemoveAds
.
We will then set up a couple of event listeners. This will be used for delegate calls when events related to InAppPurchase
are called.
Let's assign a couple of more delegates to OnStoreKitInitComplete
and OnTransactionComplete
.
Finally, we will load the store using the loadStore
function, which prepares the store to be used.
Next, we need to add the function calls that are used for the delegates. At the bottom of the GameInfo
class, under the ShowIAds
function, start by adding this function:
// Transaction Complete private static void OnTransactionComplete(IOSStoreKitResponse Response) { GameInfo game = FindObjectOfType<GameInfo>(); switch (Response.state) { case InAppPurchaseState.Purchased: case InAppPurchaseState.Restored: game.ShowSuccessScreen( true ); game.HaveUnlockedIAds( Response.productIdentifier == "G_RemoveAdsCoins", Response.state == InAppPurchaseState.Restored ); break; case InAppPurchaseState.Deferred: break; case InAppPurchaseState.Failed: game.ShowFailScreen( true ); break; } }
This is called when a transaction has been completed, including if the transaction fails for any reason. We start using this by getting a reference to the GameInfo
class by using the FindObjectOfType
function. We do this because this function is a static function, meaning it is usable even if there is no reference to the class instance. Due of this, the static function sort of lives in its own portion of code and doesn't know of the reference of the GameInfo
, although it exists within it, which is why we need a reference to it.
From there, we use a switch statement and check the Response.state
. If the state is Purchased
or Restored
, we show the success screen with ShowSuccessScreen
and then call a function in GameInfo
called HaveUnlockIAds
and pass productIdentifier
as equal to G_RemoveAdsCoins
. This means that if the product ID of the purchase is from coins, we know that because the bool value passed HaveUnlockedIAds
, it will be set to true. This is the same for if the response state is equal to Restored
.
The next three functions are delegates used by the store manager, but for our game, we do not need them. Just in case you do, use the following code:
// Verification response private static void OnVerificationResponse(CEvent e) { // Verified response on purchase } // Transaction failed private static void OnRestoreTransactionFailed() { // Transaction failed } // StoreKit Init private static void OnStoreKitComplete(ISN_Result result) { // Store was completed Init if (result.IsSucceeded) { // succeeded } else { // failed } }
OnVerificationResponse
is used when a store purchased has been verified.
OnRestoreTransactionFailed
is used when a restore transaction has failed; we will use our OnTransactionComplete
function to handle restore purchases.
OnStoreKitComplete
is used when the store has been initialized.
Under
OnStoreKitComplete
, add the following function:
// Purchase was successful, remove coins, and hide ad banner public void HaveUnlockedIAds(bool bForCoins, bool bRestored) { if (bForCoins && !bRestored) { GameCharacter.AddCoins(-10000); } PlayerPrefs.SetInt("ShowiAds", 1); if (AdBanner != null) { AdBanner.Hide(); AdBanner = null; } }
This is the function we will call from OnTransactionComplete
if the purchase succeeded or the restore purchase succeeded.
Let's start by checking whether the bForCoins
argument is set to true and bRestored
is set to false because we don't want to dock the player if the purchase is being restored. If it is, we subtract the ten thousand coin cost from the character using AddCoins
. If we pass a negative value into it, it will subtract the amount.
Next, we will set the ShowIAds
int value, which we save with PlayerPrefs
to one. This way, AdBanner
won't show anymore. Then, we need to check whether the AdBanner
exists by checking it against null. If it does, we hide it using the Hide function and then set the AdBanner
reference to null.
Lastly, we need a function that gets called from MenuUI when the player taps a button to buy something. Add the following code under the HaveUnlockedIAds
function:
// Tries to purchase RemoveiAds public void UnlockRemoveAds(bool bForCoins) { if (bForCoins) { if ( GameCharacter.CoinCount >= 10000) { IOSInAppPurchaseManager.instance.buyProduct("G_RemoveAdsCoins"); ShowPurchaseScreen(false); } } else { IOSInAppPurchaseManager.instance.buyProduct("G_RemoveAds"); ShowPurchaseScreen(false); } }
If the purchase is for coins, we then check whether the character has ten thousand or more coins. If they do, we can use IOSInAppPurchaseManager
to buy the product with buyProduct
. We also pass the product ID for the remove ads with coins. We then close the purchase screen so that it doesn't stay open during the purchase process. If the player is trying to pay with coins, but doesn't have enough coins, the window will simply stay open and nothing will happen.
If the purchase is not for coins, we will again use IOSInAppPurchaseManager
to buy a product, but we can pass the product that costs ninety nine cents. Again, close the purchase screen.
Now, save the GameInfo
class and go back to Unity.
If RemoveAdsBackgroundScreen
is hidden, select it in Hierarchy and use the Inspector window to unhide it by clicking on the small checkbox next to its name.
Select the button called For Coins and search for Inspector. In the Button component, click on the small plus symbol to add an element to the On Click () list. Then, left-click and drag the GameInfo GameObject onto the object field of the On Click () list. Click on the drop-down menu, navigate to GameInfo, and select UnlockRemoveAds. Then, check the small checkbox under the drop-down list because this purchase is for coins.
Next, select the button called For Cash and repeat the preceding steps. This time, do not click on the small checkbox under the On Click () drop-down list because this purchase is for money.
Open the
Assets/Scripts
folder and double-click on the GameInfo
class to open it. At the bottom of the class, add the following function:
// Attempts to restore purchases public void RestorePurchases() { IOSInAppPurchaseManager.instance.restorePurchases( ); }
This will attempt to restore purchases and then call OnTransactionComplete
so that we can handle how to use it.
Now, save the GameInfo
class and go back to Unity.
Select the RestorePurchase button in MenuUI and search for Inspector. In the Button component, click on the small plus symbol to add the On Click () list. Left-click and drag the GameInfo GameObject onto the On Click () object field and then click on the drop-down list. Now, select GameInfo and then RestorePurchase.
Then, save the Unity scene.