EDGAR ALEXANDER KNAUER
Programmer & Designer

About Birdo
Platform: Mobile
Project Duration: ~2 Months (Ongoing)
Project Type: Solo
Engine: Unity
Team Size: 1
My Tasks
Birdo is a project I worked on next to university to further deepen my knowledge about mobile game development. My main interests/ tasks lay in getting the Google Play Games Services API up and running. That specifically includes the "Save Games", "Achievements", and "Leaderboards" functionality. Next to that, I worked on finite state machines, characters and character selection, the game flow system, and many more things (Excluding the audio and ...obviously... visuals). Furthermore, for this project I made use of Source Control with GitHub.
The Game
Birdo started as a feature I worked on next to a student project called Forest Paint (Which you can also find via the "work" page or by clicking here). Since production for Forest Paint was delayed, I experimented in Birdo with a possible implementation of the Google Play Games Services. I was able to properly integrate the "Save Games", "Achievements", and "Leaderboards" functionality in it, however not for the final Forest Paint build. I am still working on the project from time to time on the side, looking to make it a full-blown app when I get the chance to. From its beginnings as a API integration test, it has become a game inspired by Missiles, which is sadly not available on the AppStore anymore. I added some custom power ups and changed narrative design to work with birds instead of warplanes, hence the name Birdo.
Feature Example
This code snippet shows the GPGSManager class for Birdo. It handles player authentication, saved games (with encryption), achievement, and leaderboard functionality. Additionally, it handles errors during any of the GPGS processes and makes UI changes for the player to recognize something went wrong.
using UnityEngine;
using UnityEngine.SocialPlatforms;
using GooglePlayGames;
using GooglePlayGames.BasicApi;
using TMPro;
using System;
using System.Text;
using GooglePlayGames.BasicApi.SavedGame;
public class GPGSManager : MonoBehaviour
{
[Header("Manager References")]
private GameManager gameManager;
private CanvasManager canvasManager;
private ProgressManager progressManager;
private CharacterSelection characterSelection;
private ButtonFunctionality buttonFunctionality;
[Header("Authentication Functionality")]
private bool isAuthenticated;
private string username;
private string userID;
[Header("Saved Games Functionality")]
public persistentDataOperator PDOperator;
private PersistentData oldPersistentData;
private PersistentData newPersistentData;
private readonly string keyWord = "286541288354258";
#region Data to be saved
//Last Character Played
public int characterIndex;
//Player Currency
public int playerCurrency;
//Player Progress
public bool[] charactersUnlocked = new bool[0];
#endregion
[SerializeField] private TextMeshProUGUI testText;
private void Awake()
{
gameManager = FindAnyObjectByType<GameManager>();
canvasManager = FindAnyObjectByType<CanvasManager>();
progressManager = FindAnyObjectByType<ProgressManager>();
characterSelection = FindAnyObjectByType<CharacterSelection>();
buttonFunctionality = FindAnyObjectByType<ButtonFunctionality>();
PlayGamesPlatform.Activate();
}
#region SignIn & Authentification Functionality
public void Authenticate()
{
PlayGamesPlatform.Instance.Authenticate(SignIn);
}
internal void SignIn(SignInStatus signInStatus)
{
if (signInStatus == SignInStatus.Success)
{
SetAuthentificationInformation(true);
}
else
{
SetAuthentificationInformation(false);
}
}
internal void SetAuthentificationInformation(bool authentication)
{
isAuthenticated = authentication;
username = isAuthenticated ? Social.localUser.userName : "Guest";
userID = isAuthenticated ? Social.localUser.id : null;
if (!isAuthenticated)
{
canvasManager.ChangeUI(canvasManager.currentUIPanel, canvasManager.NoAuthWarningPanel);
}
else { }
if (gameManager.initialization == true)
{
gameManager.initialization = false;
gameManager.UpdateGameState(GameManager.GameStates.StartUp);
}
else { }
}
#endregion
#region Saved Games Functionality
public void LoadPlayerData()
{
PDOperator = persistentDataOperator.reading;
OpenSavedGame("PersistentDataBirdo");
}
public void SavePlayerData()
{
PDOperator = persistentDataOperator.saving;
OpenSavedGame("PersistentDataBirdo");
}
public void DeletePlayerData()
{
PDOperator = persistentDataOperator.deleting;
OpenSavedGame("PersistentDataBirdo");
}
public void SetPDOperator(string persistentDataMode)
{
if (persistentDataMode == "saving")
{
PDOperator = persistentDataOperator.saving;
}
else if (persistentDataMode == "reading")
{
PDOperator = persistentDataOperator.reading;
}
else if (persistentDataMode == "deleting")
{
PDOperator = persistentDataOperator.deleting;
}
}
internal void OpenSavedGame(string filename)
{
ISavedGameClient savedGameClient = PlayGamesPlatform.Instance.SavedGame;
savedGameClient.OpenWithAutomaticConflictResolution(filename, DataSource.ReadCacheOrNetwork,
ConflictResolutionStrategy.UseLongestPlaytime, OnSavedGameOpened);
}
internal void OnSavedGameOpened(SavedGameRequestStatus status, ISavedGameMetadata metaData)
{
if (status == SavedGameRequestStatus.Success)
{
if (PDOperator == persistentDataOperator.saving) //On Saving new Data
{
byte[] persistentData = System.Text.ASCIIEncoding.ASCII.GetBytes(GetSaveString());
SavedGameMetadataUpdate updateForMetaData = new SavedGameMetadataUpdate.Builder().WithUpdatedDescription("Game updated at: " + DateTime.Now.ToString()).Build();
PlayGamesPlatform.Instance.SavedGame.CommitUpdate(metaData, updateForMetaData, persistentData, SaveCallback);
}
else if (PDOperator == persistentDataOperator.reading) //On reading old data
{
PlayGamesPlatform.Instance.SavedGame.ReadBinaryData(metaData, LoadCallback);
}
else if (PDOperator == persistentDataOperator.deleting)
{
ISavedGameClient savedGameClient = PlayGamesPlatform.Instance.SavedGame;
savedGameClient.Delete(metaData);
}
}
else
{
canvasManager.ChangeUI(canvasManager.currentUIPanel, canvasManager.SaveFileWarningPanel);
}
}
internal string GetSaveString()
{
newPersistentData = new PersistentData();
newPersistentData.characterIndex = characterSelection.characterIndex;
newPersistentData.playerCurrency = progressManager.playerCurrency;
newPersistentData.charactersUnlocked = progressManager.charactersUnlocked;
newPersistentData.snackAchievementsCounter = progressManager.snackAchievementsCounter;
newPersistentData.snackLVL1 = progressManager.snackLVL1;
newPersistentData.snackLVL2 = progressManager.snackLVL2;
newPersistentData.snackLVL3 = progressManager.snackLVL3;
newPersistentData.basicBirdAchievementsCounter = progressManager.basicBirdAchievementsCounter;
newPersistentData.bBirdLVL1 = progressManager.bBirdLVL1;
newPersistentData.bBirdLVL2 = progressManager.bBirdLVL2;
newPersistentData.bBirdLVL3 = progressManager.bBirdLVL3;
newPersistentData.gamesPlayedAchievement = progressManager.gamesPlayedAchievementsCounter;
newPersistentData.gamesPlayedLVL1 = progressManager.gamesPlayedLVL1;
newPersistentData.gamesPlayedLVL2 = progressManager.gamesPlayedLVL2;
newPersistentData.gamesPlayedLVL3 = progressManager.gamesPlayedLVL3;
newPersistentData.competitorBool = progressManager.competitorBool;
newPersistentData.finalScoreLVL1 = progressManager.finalScoreLVL1;
newPersistentData.finalScoreLVL2 = progressManager.finalScoreLVL2;
newPersistentData.finalScoreLVL3 = progressManager.finalScoreLVL3;
string JSON = JsonUtility.ToJson(newPersistentData);
string encryptedJSON = EncryptDecrypt(JSON);
return encryptedJSON;
}
internal void SaveCallback(SavedGameRequestStatus status, ISavedGameMetadata metaData)
{
//Callback for saving a game
}
internal void LoadCallback(SavedGameRequestStatus status, byte[] persistantData)
{
if (status == SavedGameRequestStatus.Success)
{
string encryptedJSON = System.Text.ASCIIEncoding.ASCII.GetString(persistantData);
oldPersistentData = JsonUtility.FromJson<PersistentData>(EncryptDecrypt(encryptedJSON));
//Writing JSON values
characterSelection.characterIndex = oldPersistentData.characterIndex;
progressManager.playerCurrency = oldPersistentData.playerCurrency;
progressManager.charactersUnlocked = oldPersistentData.charactersUnlocked;
progressManager.snackAchievementsCounter = oldPersistentData.snackAchievementsCounter;
progressManager.snackLVL1 = oldPersistentData.snackLVL1;
progressManager.snackLVL2 = oldPersistentData.snackLVL2;
progressManager.snackLVL3 = oldPersistentData.snackLVL3;
progressManager.basicBirdAchievementsCounter = oldPersistentData.basicBirdAchievementsCounter;
progressManager.bBirdLVL1 = oldPersistentData.bBirdLVL1;
progressManager.bBirdLVL2 = oldPersistentData.bBirdLVL2;
progressManager.bBirdLVL3 = oldPersistentData.bBirdLVL3;
progressManager.gamesPlayedAchievementsCounter = oldPersistentData.gamesPlayedAchievement;
progressManager.gamesPlayedLVL1 = oldPersistentData.gamesPlayedLVL1;
progressManager.gamesPlayedLVL2 = oldPersistentData.gamesPlayedLVL2;
progressManager.gamesPlayedLVL3 = oldPersistentData.gamesPlayedLVL3;
progressManager.competitorBool = oldPersistentData.competitorBool;
progressManager.finalScoreLVL1 = oldPersistentData.finalScoreLVL1;
progressManager.finalScoreLVL2 = oldPersistentData.finalScoreLVL2;
progressManager.finalScoreLVL3 = oldPersistentData.finalScoreLVL3;
}
else { return; }
}
internal string EncryptDecrypt(string JSON)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < JSON.Length; i++)
{
builder.Append((char)(JSON[i] ^ keyWord[i % keyWord.Length]));
}
string result = builder.ToString();
return result;
}
#endregion
#region Achievements Functionality
public void ShowAchievements()
{
if (isAuthenticated == true)
{
PlayGamesPlatform.Instance.ShowAchievementsUI();
}
else
{
PlayGamesPlatform.Instance.Authenticate(SignIn);
}
}
public void IncrementAchievement(string achievementCode, int incrementAmount)
{
PlayGamesPlatform.Instance.IncrementAchievement(
achievementCode, incrementAmount, (bool success) =>
{
if (success == true)
{
//Increment trackers for achievement reveal
switch(achievementCode)
{
case GPGSIds.achievement_feasting:
progressManager.snackAchievementsCounter += 1;
break;
case GPGSIds.achievement_scourge_of_the_sky:
progressManager.basicBirdAchievementsCounter += 1;
break;
case GPGSIds.achievement_addict:
progressManager.gamesPlayedAchievementsCounter += 1;
break;
}
if (progressManager.snackAchievementsCounter == 30 && progressManager.snackLVL1 == false)
{
progressManager.snackLVL1 = true;
RevealOrUnlockAchievement(GPGSIds.achievement_dining, false);
progressManager.AddAchievementBonus(100);
}
else if (progressManager.snackAchievementsCounter == 100 && progressManager.snackLVL2 == false)
{
progressManager.snackLVL2 = true;
RevealOrUnlockAchievement(GPGSIds.achievement_feasting, false);
progressManager.AddAchievementBonus(300);
}
else if (progressManager.snackAchievementsCounter == 200 && progressManager.snackLVL3 == false)
{
progressManager.snackLVL3 = true;
progressManager.AddAchievementBonus(1000);
}
if (progressManager.basicBirdAchievementsCounter == 10 && progressManager.bBirdLVL1 == false)
{
progressManager.bBirdLVL1 = true;
RevealOrUnlockAchievement(GPGSIds.achievement_hunter, false);
progressManager.AddAchievementBonus(100);
}
else if (progressManager.basicBirdAchievementsCounter == 50 && progressManager.bBirdLVL2 == false)
{
progressManager.bBirdLVL2 = true;
RevealOrUnlockAchievement(GPGSIds.achievement_scourge_of_the_sky, false);
progressManager.AddAchievementBonus(300);
}
else if (progressManager.basicBirdAchievementsCounter == 150 && progressManager.bBirdLVL3 == false)
{
progressManager.bBirdLVL3 = true;
progressManager.AddAchievementBonus(1000);
}
if (progressManager.gamesPlayedAchievementsCounter == 5 && progressManager.gamesPlayedLVL1 == false)
{
progressManager.gamesPlayedLVL1 = true;
RevealOrUnlockAchievement(GPGSIds.achievement_professional, false);
progressManager.AddAchievementBonus(100);
}
else if (progressManager.gamesPlayedAchievementsCounter == 50 && progressManager.gamesPlayedLVL2 == false)
{
progressManager.gamesPlayedLVL2 = true;
RevealOrUnlockAchievement(GPGSIds.achievement_addict, false);
progressManager.AddAchievementBonus(500);
}
else if (progressManager.gamesPlayedAchievementsCounter == 150 && progressManager.gamesPlayedLVL3 == false)
{
progressManager.gamesPlayedLVL3 = true;
progressManager.AddAchievementBonus(1500);
}
}
else
{
}
});
}
public void RevealOrUnlockAchievement(string achievementCode, bool unlockAchievement)
{
int unlock = 0;
if (unlockAchievement == true)
{
unlock = 100;
}
else
{
unlock = 0;
}
Social.ReportProgress(achievementCode, unlock, (bool success) =>
{
if (success == true)
{
}
else
{
}
});
}
#endregion
#region Leaderboards Functionality
public void ShowLeaderBoardUI()
{
if (isAuthenticated == true)
{
PlayGamesPlatform.Instance.ShowLeaderboardUI("CgkImPCa29AJEAIQBg");
}
else
{
PlayGamesPlatform.Instance.Authenticate(SignIn);
}
}
public void PostScoreToLeaderBoard()
{
Social.ReportScore(progressManager.int_finalScore, GPGSIds.leaderboard_biggest_birds, (bool success) => {
if (success == true)
{
if (progressManager.competitorBool == false)
{
progressManager.competitorBool = true;
RevealOrUnlockAchievement(GPGSIds.achievement_competitor, true);
progressManager.AddAchievementBonus(100);
}
PlayGamesPlatform.Instance.ShowLeaderboardUI("CgkImPCa29AJEAIQBg");
}
else
{
}
});
}
#endregion
public enum persistentDataOperator
{
saving,
reading,
deleting
}
}
