First Android Game - Part 5 July 1, 2023
This is part 5 of making my first Android Game. If you missed part4, you can find it here Currently when the player collides with the enemy it gets disabled, goes back to its original position and get reneabled. However this does not sync across the network. So I changed the code to call a RPC function in OnTriggerEnter2D I disabled the explosions code as the explosions are not synced across yet. The RPC function will call a coroutine. The coroutine is doing the same thing as before I synced the collisions across the network. You can also find a video version on youtube here Since the boxcollider2D is attached to the parent of the player, and the gameobject being SetActive(false) is on the child. I needed to disable the boxcollider2D and renable to avoid player getting collided with other gameobjects even though the player is invisible. I created a BoxCollider2D variable. In Awake() I would Get the Component of the BoxCollider2D. Then in the Coroutine I would enable and disable it. You can also find a video version on youtube here If the user does not enter a name and decides to click on the Connect button in the mainmenu, nothing will happen. This is bad UI design because the user has no feedback. So I added a error text that will get enabled when this edge case occurs. In ConnectToServer.cs, I added a TextMeshProUGUI variable called errorText. This gameobject is disabled in Awake. It is enabled and its text is changed to "Enter name" when the user does not enter a name and selects connect. Now the usr will see this error feedback which is a better design. I did the same thing in the Lobby when the user tries to create a room but does not enter a name I noticed in the Lobby, when the "Create" button is pressed, there is no feedback given to the user. So I decided to do the same thing as the main menu and changed "Create" to "Creating" when the user presses the button. However, if the user leaves the room and back to the lobby, the "Creating..." text was still there. This is because I am still in the same scene. So I created a LobbyCreateButton.cs that has a string property called ButtonText. This will update the text on the button. When the button is enabled -which occurs when entering the scene or leaving the room - then the button text displays "Create". In LobbyManager, instead of referencing the TextMeshProUGUI, I create a LobbyCreateButton variable called createButtonText. This way I can reference the ButtonText property. The CreateButton will say "Creating..." or "Create" depending on the scenarios I mentioned above When the user presses the connect button in the main menu, the user can still press the connect button. So I made sure to disable them to prevent the user from pressing connect multiple times which can result in this warning The disabled state of the button looks like this Simlarily, when the user is trying to create a room and clicks create multiple times, this error will occur So in LobbyCreateButton.cs, I added a boolean property called PressButton. This let's me set when the button is interactable The disabled state of the button looks like this. You can see the video version here Updating the score requires syncing across the network as well. The Asteroid game example from the PUN2 asset had its own score system. So I took a look at that. First I used PlayerOverViewPanel.cs as my example. I created a TestScorePanel.cs script. In the Awake, I create a dictionary that will store the Photon.Realtime.Player ActorNumber as the key and the Score gameobjects as the value. This way I can search for which player's score I want to modify. I would loop through the PlayerList which will contain all the players that had entered the lobby. For each player in the list, I would spawn the Score gameobject. In this case, the ScoreGameobject would be childed to a gameobject that has the horizontal layout group so the score prefab won't overlap. I would then set the text to the player's name, score and lives. These are the initial values at the start of the game OnPlayerPropertiesUpdate would be needed because when SetCustomProperties is called, it will call OnPlayerPropertiesUpdate. In this function I would look for my player in the dictionary and get the score gameobject. I would pass update my score and lives. The score would the most updated because of GetScore(). Then there is the PunPlayerScores.cs. This would be the script example I used for TestPlayerScore.cs. The first function would be SetScore. Note that it passes in this Photon.Realtime.Player. This means I won't need to pass in the Photon.Realtime.Player. The script would need to be static in this case. I store "score" in the hashtable score variable. I would then update this player's custom properties by calling SetCustomProperties. This would automatically call OnPlayerPropertiesUpdate() from TestScorePanel.cs Then there AddScore(). Similar to SetScore but it would add to the current score Last is GetScore().This would return this player's score which is stored in the custom properties Last sample script I references was ScoreHelper.cs. I created a TestScoreHelper.cs. In Update(), I would say if the mouse is clicked which is equivalent to if the screen is tapped, check if local player exists then set the score. I created a Test Scene. It contains a TestPanel gameobject. This has an Image component, TestScorePanel(Script) component and a Horizontal Layout group component. The ScoreHelper gameobject has the TestScoreHelper(Script) component. The TestPlayerScore.cs is not in the scene, and can be referenced because it is static I made a build with MainMenu, Lobby and TestScene. And the Score was being updated and synced! Now I had to move over the same logic to the actual game. I want to sync up the score and coins. Since a lot of code for the scores were based on ScoreManager.Instance - this would not work with this logic. So I had to comment those out. I first created a PlayerTextInformation.cs. This is so I have access to the scores and coins text. Unlike the test I did, the text was the gameobject itself so I could GetComponent TextMeshProUGUI. In this case, I would GetComponent PlayerTextInformation to access the Properties Coin or Score This script is attached to PlayerTextInformation prefab which will contain the players name, lives, coin count and score. Using TestPlayerScore.cs as the example, I transferred the same logic over to PlayerInformation.cs and included a const string for coin and score On the Scoreboard prefab. The player's information has been removed, because the PlayerTextInformation will be spawned on to the "UI board Small parchment" which contains a horizontal layout group. The Scoreboard prefab has the ScoreManager(Script) component The ScoreManager.cs is based on TestScorePanel.cs. The difference is instead of GetComponent TextMeshProUGUI to get the Score and Coins, I GetComponent PlayerTextInformation and access the Properties Coins and Score Anywhere I was adding score or coins I would change it to the new GetCoin() and GetScore(). This includes the bullet hitting the enemy to add score As well as player collecting coins I tested to see if this working by testing with the editor and on a mobile build. The score appears to be syncing. The coins however was being incremented for both players even if only one player is collecting the coins. Reminder that because of the new photon code, all the codes for when to spawn the enemies, subtracting lives are commented out. The player's names are not implemented yet and the bullets are not synced across the network yet I was getting an error when I attempt to GetCoins() The null reference error was because I deleted the check to see if player collieded with an gameobject with the tag CoinA, CoinB or CoinC. However, when 1 player collects the coins, both player's coins are still increased. I am guessing the reason the score isn't increasing for both players is because the bullets are not synced across the network yet. Therefore, it looks like the score is behaving correctly. This however does not tell me why the coins are increasing. Turns out the solution was to add view.IsMine so that the only correct coins count increases for that player. Voila! Example of the correct score and coin count being incremented and synced across the network Next is to display the correct NickName. I created a NickName property in PlayerTextInformation. Then assigned the player's nickname in ScoreManager However, the name that I typed on the local player is displayed for both players. This is wrong! The reason that the nickname was wrong is because i was using PhotonNetwork.LocalPlayer.NickName instead of Photon.Realtime.Player.NickName The result of the correct nicknames being displayed. Although the score, coins and nicknames are synced. The lives are not synced yet. I looked at the Asteroid example. In PlayerListEntry.cs. In the Start(), the script would SetCustomProperty for the lives and would set the initial score as well. For now, let's ignore the Player ready you see in the image. We may or may not come back to that in this or another blog The PlayerListEntry.cs is attached to the PlayerListEntry prefab in the Asteroid sample. I did not set the score before entering the game in the lobby (and for sure I didn't set the lives yet). So let's set the score. Since setting it to 0 would be hard to tell if the game is working since by default I've set it to 0. So I set the score to 7 as a test. I checked the game and the score is set to 7. I changed 7 to 0 because I know the score is set properly Now I need to set the coins as well Time to get the lives in. The Asteroid sample has a AsteroidGame.cs. This script is not Monobehaviour. AsteroidGame.cs stores data such as const int PLAYER_MAX_LIVES as well as const string PLAYER_LIVES which will be used as the key for our ExitGames.Client.Photon.HashTable Using the sample above, I created a TomatoGame.cs which stores the int PLAYER_MAX_LIVES and string PLAYER_LIVES Then in PlayerItem.cs, I would set the PLAYER_LIVES to PLAYER_MAX_LIVES because this is the default amount of lives before player enters the game. This is so the hashtable value contains the max lives. This is the same idea as what PlayerListEntry.cs did from the Asteroid sample In PlayerTextInformation.cs I would create a Lives property of type string. It would get and set the field lives.text to display the lives on the In ScoreManager.cs, I would set the Lives property to PLAYER_MAX_LIVES in Awake(). This is so the text on the scoreboard shows the number of lives. This is the same idea as the Asteroid sample's PlayerOverViewPanel.cs Before I continue, I did a quick test to make sure the lives are being set to the value I set in PLAYER_MAX_VALUES OnPlayerPropertiesUpdate() I would set the lives the the Photon.Realtime.Player.CustomProperties[TomatoGame.PLAYER_LIVES]. This hopefully would sync the lives up across the network when a SetCustomProperties is called Using the Asteroid sample's Spaceship.cs as the example. In Player.cs of my own script, when the player collides with the enemy, in HitByEnemyRoutine(), I would check if PLAYER_LIVES is in the hashtable for this player, if yes, modify the value of PLAYER_LIVES, if it's less then or equal to 1, set lives to 0, otherwise subtract 1. As a comparison, here is Spaceship.cs from Asteroid sample. They are using PhotonNetwork.LocalPlayer.CustomProperties. This would result in the same player the way I did it. I just happen to have the player passed into the parameter. From the build, you can see the lives are synced up. The screen is showing the editor, but the player moving is the mobile, and when the lives are decreased, the editor see the lives decreasing as well In Player.cs, I added the PunRPC attribute to the Shoot method because I want the bullet to sync across the network. In Bullet.cs, I removed enabling and disabling code. The bullet would be destroyed after timeBeforeDestroy (in seconds) In Player.cs, in the Update() instead of calling the BulletPoolInstance, I call the RPC function Shoot(). Note that I am using RPCTarget.AllViaServer so that the order of who shot first is correct I made a build and you can see that one player can see the bullets of both players. However, notice that there are some new bugs: score is being increased for both players, the bullet is still shooting even when the player is destroyed. To resolve the score increasing for both players instead of itself. In Bullet.cs, there is a InitializeBullet function that would assign the Owner property of type Photon.Realtime.Player to owner field that is passed in from the Player.cs script. In OnTriggerEnter2D, instead of adding score the PhotonNetWork.LocalPlayer, I used the Owner property to call the AddScore. In Player.cs in the Shoot() function, is where InitializeBullet function is called and the PhotonView.Owner is passed in to InitializeBullet. Now only the score of the player hitting the enemy has its score increased. To resolve bullets still shooting when player is disabled. The reason is because the root being disabled is the child of the gameobject that the Player.cs is attached to, therefore, Player.cs is still running. Therefore, I can check if root is active in hierarchy or not. Since when the player collides with the enemy, the root gets disabled The result shows the bullets stops shooting when the player is disabled. Let's continue syncing gameobjects in Part 6 which can be found here
Recent blogs See all blogs