Indie Gameleon is over, so it’s time to get a bit more technically again.
Programming resources are rare in most projects, so it’s important to work as effective as possible. The most time wasting things are compiling and, if you are developing for another platform than PC/Mac, deploying the game to your device to test it there. This post is about saving time by deploying as little times as possible.
To test our game we need at least an engine that allows to deploy our game on our development platform as well as on our target platform.
Most likely you already fulfilled the basic requirement by using Unity. In multiplayer games you have to run a local server as well.
Mapping input
Now you have to make sure that your game stays compatible with several platforms. This means for example to allow multiple input methods. For mobile games you need touch input while for PC the mouse and keyboard act as the input devices.
You can achieve this independence by separating the physical input from your game input. For example you don’t send the command “Space pressed” to your game, but instead catching the space press in your input manager and forwarding a “Fire weapon” command to your game.
This should be done no matter if you are developing for multiple platforms or just for a single one. It allows to support different input schemes for your game and to change them without touching (and possibly breaking) your game logic.
Using and creating 3rd party plugins
This kind of separation is also supported by many 3rd party plugins, e.g. FingerGestures for the input or OpenIAB as an payment solution. If they want to support different platforms (Android, iOS, Windows Phone,…), they have to abstract the functionality from the platform specific implementation. This can make your local development really easy. Or a bit labor-intensive at first.
Google Play Games Services: A concrete example
One of my main tasks in the sprint of our current project was to allow the user to connect his game with Google Plus, so he could play on multiple devices and his progress is safe even if he deletes the game from his device. Therefore I had to login to Google on the client and send the credentials to the server to validate them there and connect the Google account to the game account.
To do this for a Unity game, Google fortunately offers a Plugin for Unity to easily authenticate with Google. Unfortunately though this plugin only offers a very unfunctional implementation to run in the Unity editor. This implementation will only log the functions called in the implementation to the Unity console.
This doesn’t allow us of course to test any functionality we have to test locally, but only on the device itself. Which would be very time consuming. Even if you think your feature is done and should work immediately, there are most of the time a handful of little things that break on first try, so you have to do multiple deployments.
If there were multiple plugins to provide the functionality I need, I would probably try a different one at this time. But for Google Play Services it’s probably best to go with the official implementation. So before working on the feature itself I bit the bullet and added my own implementation for the editor.
First iteration: Sending what we need
The first attempt was to set the token we need on the server hard coded in the editor client and returning it when the client was asked to authenticate with Google. There is a developer service called OAuth Playground where you can generate such tokens.
This way I could already develop the server-side of the connect feature with the correct implementation to validate the token sent by the client. But the token is only valid for a limited duration, so it would have to be changed regularly (every hour to be accurate). This is not really optimal for the further development.
Second iteration: Asking the user for his credentials
The optimal implementation on editor side would be to ask the user for his Google credentials. So it works the same way in the editor as it does on the device.
Unfortunately (the second time) there wasn’t even a method to call the authentication endpoint for the Google service in the editor. Writing an own one should be possible as it’s just an HTTP service where you send your credentials and get your tokens back.
But I decided that in this case it’s not really worth it and it would be enough to have a way to insert the token you got from the OAuth Playground manually instead of having it hard-coded inside your code.

So every time the player tries to authenticate with Google, an editor window opens to allow the user to enter his ID token. This allowed me to test almost every use case inside the editor:
- Connecting a game with Google for the first time
- Connecting with a Google account that’s already connected to a different game
- Connecting to a game that is already owned by a different Google account
- Sending wrong/expired tokens
Implementation
If you are using the Google Play Services in your game as well, here is my implementation for the Unity editor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
using System; using GooglePlayGames.BasicApi; using GooglePlayGames.BasicApi.Events; using GooglePlayGames.BasicApi.Multiplayer; using GooglePlayGames.BasicApi.Quests; using GooglePlayGames.BasicApi.SavedGame; using UnityEngine; using UnityEngine.SocialPlatforms; /// <summary> /// Client to connect with google that works in the editor for testing purposes. /// </summary> public class EditorPlayGamesClient : IPlayGamesClient { #region Fields private bool isAuthenticated; #endregion #region Properties public string IdToken { get; private set; } #endregion #region Public Methods and Operators public void Authenticate(Action<bool> callback, bool silent) { if (!this.isAuthenticated) { // Call UI. var uiGameObject = new GameObject("Editor Play Games Client UI"); var editorPlayGamesClientUI = uiGameObject.AddComponent<EditorPlayGamesClientUI>(); editorPlayGamesClientUI.LoggedIn += idToken => { // Store id token. this.IdToken = idToken; this.isAuthenticated = true; callback(true); }; editorPlayGamesClientUI.LoginCanceled += () => { callback(false); }; } else { callback(false); } } public string GetAccessToken() { throw new NotImplementedException(); } public Achievement GetAchievement(string achievementId) { throw new NotImplementedException(); } public IEventsClient GetEventsClient() { throw new NotImplementedException(); } public IUserProfile[] GetFriends() { throw new NotImplementedException(); } public string GetIdToken() { return this.IdToken; } public IQuestsClient GetQuestsClient() { throw new NotImplementedException(); } public IRealTimeMultiplayerClient GetRtmpClient() { throw new NotImplementedException(); } public ISavedGameClient GetSavedGameClient() { throw new NotImplementedException(); } public ITurnBasedMultiplayerClient GetTbmpClient() { throw new NotImplementedException(); } public string GetToken() { throw new NotImplementedException(); } public string GetUserDisplayName() { throw new NotImplementedException(); } public string GetUserEmail() { throw new NotImplementedException(); } public string GetUserId() { throw new NotImplementedException(); } public string GetUserImageUrl() { throw new NotImplementedException(); } public void IncrementAchievement(string achievementId, int steps, Action<bool> successOrFailureCalllback) { throw new NotImplementedException(); } public bool IsAuthenticated() { return this.isAuthenticated; } public int LeaderboardMaxResults() { throw new NotImplementedException(); } public void LoadAchievements(Action<Achievement[]> callback) { throw new NotImplementedException(); } public void LoadFriends(Action<bool> callback) { throw new NotImplementedException(); } public void LoadMoreScores(ScorePageToken token, int rowCount, Action<LeaderboardScoreData> callback) { throw new NotImplementedException(); } public void LoadScores( string leaderboardId, LeaderboardStart start, int rowCount, LeaderboardCollection collection, LeaderboardTimeSpan timeSpan, Action<LeaderboardScoreData> callback) { throw new NotImplementedException(); } public void LoadUsers(string[] userIds, Action<IUserProfile[]> callback) { throw new NotImplementedException(); } public void RegisterInvitationDelegate(InvitationReceivedDelegate invitationDelegate) { throw new NotImplementedException(); } public void RevealAchievement(string achievementId, Action<bool> successOrFailureCalllback) { throw new NotImplementedException(); } public void SetStepsAtLeast(string achId, int steps, Action<bool> callback) { throw new NotImplementedException(); } public void ShowAchievementsUI(Action<UIStatus> callback) { throw new NotImplementedException(); } public void ShowLeaderboardUI(string leaderboardId, LeaderboardTimeSpan span, Action<UIStatus> callback) { throw new NotImplementedException(); } public void SignOut() { if (this.isAuthenticated) { this.isAuthenticated = false; } } public void SubmitScore(string leaderboardId, long score, Action<bool> successOrFailureCalllback) { throw new NotImplementedException(); } public void SubmitScore( string leaderboardId, long score, string metadata, Action<bool> successOrFailureCalllback) { throw new NotImplementedException(); } public void UnlockAchievement(string achievementId, Action<bool> successOrFailureCalllback) { throw new NotImplementedException(); } #endregion } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
using System; using System.Reflection; using UnityEngine; public sealed class EditorPlayGamesClientUI : MonoBehaviour { #region Constants private const int WindowId = 123; #endregion #region Fields private string idToken; private Rect windowRect = new Rect(100, 100, 400, 300); #endregion #region Constructors and Destructors public EditorPlayGamesClientUI() { this.idToken = string.Empty; } #endregion #region Events public event Action<string> LoggedIn; public event Action LoginCanceled; #endregion #region Methods private void ConsoleWindow(int id) { GUILayout.Label( "Use https://developers.google.com/oauthplayground to create a id token of your google account for testing."); GUILayout.Space(10); GUILayout.Label("ID Token: "); this.idToken = GUILayout.TextArea(this.idToken, GUILayout.MinHeight(50)); if (GUILayout.Button("Login")) { this.OnLoggedIn(this.idToken); // Close window. Destroy(this.gameObject); } if (GUILayout.Button("Close")) { this.OnLoginCanceled(); // Close window. Destroy(this.gameObject); } GUI.DragWindow(); } [Obfuscation(Exclude = true)] private void OnGUI() { this.windowRect = GUILayout.Window(WindowId, this.windowRect, this.ConsoleWindow, "Google Login"); } private void OnLoggedIn(string idToken) { var handler = this.LoggedIn; if (handler != null) { handler(idToken); } } private void OnLoginCanceled() { var handler = this.LoginCanceled; if (handler != null) { handler(); } } #endregion } |
If you extended it (maybe to allow the user to enter his real Google credentials), feel free to send me an updated version 🙂
An appeal to asset developers
If you are developer of assets that other developers use, try to develop them in a way that saves your comrades as much time as possible. This is the reason for using 3rd party assets anyway, so make sure to consider the deployment time as well.
It’s not really required that you build a fancy UI in the editor. Most of the time a really quick editor UI or even some inspector functionality will do just fine. The closer your editor implementation is to the device one the better though.
Conclusion
Deployment time is one of the big time wasters when it comes to developing for a mobile or console platform. So every minute you invest to create tools that allow to test on your development platform instead of your target platform may save you hours later on.
Unity already solves a lot of cross-platform issues (aside from some catches you discover quickly). So make sure your game-specific code works on your development platform as well.
If you are using third party assets you may be lucky that the developers already considered your needs as a cross-platform developer. If not it may make sense to add an editor implementation yourself instead of having to test your feature on the device solely.