A Godot 4.2+ Android plugin for Google Play Games Services v2.
Tested with: Godot 4.5.1, Play Games Services v2 SDK 21.0.0, Android SDK 35
This plugin integrates Google Play Games Services into Godot games using the modern v2 API. It uses the Godot v2 plugin format with EditorExportPlugin for seamless integration.
| Feature | Status | Description |
|---|---|---|
| Automatic Sign-in | ✅ | Adaptive startup auth check (silent on most devices, deferred explicit flow on Xiaomi-family devices) |
| Manual Sign-in | ✅ | Trigger sign-in for users who declined initially |
| Player Info | ✅ | Get player ID and display name |
| Cloud Save | ✅ | Save/load/delete game data via Snapshots API |
| Signals | ✅ | Async events via Godot signals |
| Achievements | ⏳ | Planned for v1.2 |
| Leaderboards | ✅ | Submit scores + load top scores + player rank |
- No Play Games app required - Users don't need to install the Play Games app
- Automatic sign-in - Returning users are signed in silently
- OS-managed accounts - Sign-out handled in Android settings, not in-app
- Simplified API - No deprecated GoogleSignIn classes
- Failure status codes are emitted as Kotlin
Longand declared as Godot signalLong(int64) to avoid bridge crashes such asInvalid type for argument #1. Should be of type long. - Snapshot operations (
saveGame,loadGame,deleteGame) are guarded when signed out and emit*_failedwith status-3and"Not signed in"instead of entering unstable resolution flows. - Leaderboard operations retry sign-in one time automatically when API returns
SIGN_IN_REQUIRED(statusCode=4) and then retry the same request once. loadPlayerScore(...)always emitsleaderboard_player_score_loaded(including fallback/error payloads with optionalstatus_codeanderrorfields) to keep UI rendering stable.leaderboard_player_score_failedremains declared for API compatibility but is currently not emitted by the runtime implementation.- Startup auth check is intentionally skipped on Xiaomi/Redmi/Poco devices to avoid repeated profile chooser prompts; explicit login UI should be triggered from game UI when needed.
- Recommended app-side lifecycle hook: call
refreshAuthStatus()onNOTIFICATION_APPLICATION_RESUMED.
- Godot Engine 4.2+ (tested with 4.5.1)
- Android Studio (for building the plugin)
- Google Play Console account with Play Games Services configured
- JDK 17 or later
PlayGamesPlugin/
├── app/
│ ├── build.gradle.kts # Dependencies and build config
│ └── src/main/
│ ├── assets/
│ │ └── godot_plugin.json # Plugin metadata (bundled in AAR)
│ ├── AndroidManifest.xml # Permissions + v2 plugin registration
│ └── java/.../PlayGamesPlugin.kt
├── addons/
│ └── play_games_plugin/ # Copy this to your Godot project
│ ├── plugin.cfg # EditorPlugin config
│ └── play_games_plugin.gd # EditorExportPlugin
├── INSTALLATION_GUIDE.md # Detailed installation steps
├── DEVELOPMENT.md # Developer reference
└── example_plugin_usage.gd # Example GDScript
If you just want to use the plugin, download the release bundle and copy the folder into your project:
# Example for v1.1.5
unzip PlayGamesPlugin-v1.1.5-addons.zip
cp -r play_games_plugin /path/to/your_project/addons/Expected target structure:
your_project/addons/play_games_plugin/
├── plugin.cfg
├── play_games_plugin.gd
├── PlayGamesPlugin-debug.aar
└── PlayGamesPlugin-release.aar
./gradlew clean assembleDebug assembleReleaseCopy to your Godot project:
your_project/addons/play_games_plugin/
├── plugin.cfg
├── play_games_plugin.gd
├── PlayGamesPlugin-debug.aar # from app/build/outputs/aar/app-debug.aar
└── PlayGamesPlugin-release.aar # from app/build/outputs/aar/app-release.aar
Edit addons/play_games_plugin/play_games_plugin.gd:
const PLAY_GAMES_APP_ID = "123456789012" # Your App ID from Play ConsoleIn Godot: Project → Project Settings → Plugins → Enable "PlayGamesPlugin"
In Godot: Project → Install Android Build Template
This is required for Gradle-based Android plugins.
var play_games = null
func _ready():
if Engine.has_singleton("PlayGamesPlugin"):
play_games = Engine.get_singleton("PlayGamesPlugin")
play_games.sign_in_success.connect(_on_sign_in_success)
play_games.sign_in_failed.connect(_on_sign_in_failed)
play_games.player_info_loaded.connect(_on_player_info_loaded)
# Check if already signed in (automatic sign-in)
if play_games.isSignedIn():
print("Already signed in as: ", play_games.getPlayerDisplayName())
func _notification(what):
# Refresh auth when app resumes (important!)
if what == NOTIFICATION_APPLICATION_RESUMED and play_games:
play_games.refreshAuthStatus()
func _on_sign_in_success(player_id: String, player_name: String):
print("Signed in: ", player_name)
func _on_sign_in_failed(status_code: int64, message: String):
print("Sign-in failed: ", status_code, " ", message)
func _on_player_info_loaded(player_id: String, player_name: String):
print("Player info loaded: ", player_name)
func _on_sign_in_button_pressed():
play_games.signIn()func submit_score(score: int):
if play_games and play_games.isSignedIn():
play_games.submitScore("leaderboard_id", score)
func load_daily_leaderboard():
play_games.loadTopScores("leaderboard_id", "daily", "public", 10, true)
play_games.loadPlayerScore("leaderboard_id", "daily", "public", true)
func _on_leaderboard_top_scores_loaded(leaderboard_id: String, json: String):
var data = JSON.parse_string(json)
print(data)Top Scores Payload:
{
"leaderboard_id": "leaderboard_id",
"time_span": "daily",
"collection": "public",
"scores": [
{
"rank": 1,
"rank_value": 1,
"rank_known": true,
"score": 1234,
"display_name": "Player",
"player_id": "abc",
"is_player": false
}
]
}Player Score Payload:
{
"leaderboard_id": "leaderboard_id",
"time_span": "daily",
"collection": "public",
"rank": 12,
"rank_value": 12,
"rank_known": true,
"score": 987,
"display_name": "You",
"player_id": "abc",
"is_player": true
}rank = -1 with rank_known = false means Google Play Games returned an unknown rank (for example, score exists but public rank is hidden).
- Cloud Save methods require the user to be authenticated (
isSignedIn() == true). - Saved Games must be enabled in Play Console for the linked Play Games project. If disabled, snapshot operations fail with
IllegalStateException: Cannot use snapshots without enabling the 'Saved Game' feature. - If called while signed out, the plugin fails gracefully and emits
*_failedwith status code-3and message"Not signed in"(instead of triggering Play Games resolution UI unexpectedly). - On some device/account states, Games APIs can still return
SIGN_IN_REQUIREDafter a successful sign-in callback. Guard cloud/leaderboard UI and allow manual retry from the player. - Prefer waiting for
sign_in_successorplayer_info_loadedbefore callingloadGame,saveGame, ordeleteGame.
func _ready():
# ... after getting the singleton ...
play_games.save_game_success.connect(_on_save_game_success)
play_games.save_game_failed.connect(_on_save_game_failed)
play_games.load_game_success.connect(_on_load_game_success)
play_games.load_game_failed.connect(_on_load_game_failed)
func save_player_state():
var data = JSON.stringify({"hp": 85, "level": 12, "gold": 500})
play_games.saveGame("autosave", data, "Autosave - Level 12")
func load_player_state():
if not play_games.isSignedIn():
return
play_games.loadGame("autosave")
func _on_save_game_success(save_name: String):
print("Saved: ", save_name)
func _on_save_game_failed(save_name: String, status_code: int64, message: String):
print("Save failed: ", message)
func _on_load_game_success(save_name: String, data: String):
var state = JSON.parse_string(data)
print("HP: ", state.get("hp", 100))
func _on_load_game_failed(save_name: String, status_code: int64, message: String):
print("Load failed: ", message)| Method | Returns | Description |
|---|---|---|
helloWorld() |
String | Test method, returns "Hello from PlayGamesPlugin v2!" |
signIn() |
void | Trigger manual sign-in UI |
signOut() |
void | No-op (v2 handles via OS settings) |
isSignedIn() |
bool | Check current auth status |
refreshAuthStatus() |
void | Re-check auth (call on app resume) |
getPlayerId() |
String | Get player's unique ID |
getPlayerDisplayName() |
String | Get player's display name |
requestServerAuthCode() |
void | Request/refresh Play Games server auth code for backend verification |
getServerAuthCode() |
String | Get last cached Play Games server auth code |
getServerAuthCodeRefreshedAtMs() |
int64 | Unix ms timestamp when auth code was last refreshed |
saveGame(saveName, data, description) |
void | Save data to a named cloud snapshot |
loadGame(saveName) |
void | Load data from a named cloud snapshot |
deleteGame(saveName) |
void | Delete a cloud snapshot |
submitScore(leaderboardId, score) |
void | Submit score to a leaderboard |
loadTopScores(leaderboardId, timeSpan, collection, maxResults, forceReload) |
void | Load top scores (returns JSON via signal). timeSpan: daily/weekly/all_time (others fallback to all_time), collection: public/friends (others fallback to public) |
loadPlayerScore(leaderboardId, timeSpan, collection, forceReload) |
void | Load current player score/rank (returns JSON via signal). Uses same timeSpan/collection normalization; forceReload is currently accepted for API symmetry |
Snake_case compatibility aliases are also exposed for integrations that prefer Godot-style naming:
sign_in, sign_out, is_signed_in, refresh_auth_status, request_server_auth_code,
get_server_auth_code, get_server_auth_code_refreshed_at_ms, save_game, load_game, delete_game.
The plugin exposes both camelCase and snake_case for core auth/cloud methods:
| camelCase | snake_case |
|---|---|
signIn() |
sign_in() |
signOut() |
sign_out() |
isSignedIn() |
is_signed_in() |
refreshAuthStatus() |
refresh_auth_status() |
requestServerAuthCode() |
request_server_auth_code() |
getServerAuthCode() |
get_server_auth_code() |
getServerAuthCodeRefreshedAtMs() |
get_server_auth_code_refreshed_at_ms() |
saveGame(...) |
save_game(...) |
loadGame(...) |
load_game(...) |
deleteGame(...) |
delete_game(...) |
| Signal | Parameters | Description |
|---|---|---|
sign_in_success |
player_id: String, player_name: String | Emitted when manual sign-in succeeds; player_info_loaded is the authoritative profile refresh signal |
sign_in_failed |
status_code: int64, message: String | Emitted when sign-in fails/declined |
player_info_loaded |
player_id: String, player_name: String | Emitted when player info is loaded (auto sign-in) |
save_game_success |
save_name: String | Emitted when cloud save succeeds |
save_game_failed |
save_name: String, status_code: int64, message: String | Emitted when cloud save fails |
load_game_success |
save_name: String, data: String | Emitted when cloud load succeeds (data is the saved string) |
load_game_failed |
save_name: String, status_code: int64, message: String | Emitted when cloud load fails |
delete_game_success |
save_name: String | Emitted when cloud delete succeeds |
delete_game_failed |
save_name: String, status_code: int64, message: String | Emitted when cloud delete fails |
leaderboard_submit_success |
leaderboard_id: String | Emitted when score submit succeeds |
leaderboard_submit_failed |
leaderboard_id: String, status_code: int64, message: String | Emitted when score submit fails |
leaderboard_top_scores_loaded |
leaderboard_id: String, json: String | Emitted with top scores JSON |
leaderboard_top_scores_failed |
leaderboard_id: String, status_code: int64, message: String | Emitted when top scores load fails |
leaderboard_player_score_loaded |
leaderboard_id: String, json: String | Emitted with player score JSON on both success and fallback/error paths |
leaderboard_player_score_failed |
leaderboard_id: String, status_code: int64, message: String | Declared for API compatibility; currently not emitted |
Note: failure status values are emitted as 64-bit numeric values from Kotlin (
Long) for reliable Godot bridge marshalling. In GDScript, handling them asintorVariantis safe.Common status sentinels used by the plugin:
-2(user canceled),-3(snapshot call while signed out),-1(local/unknown failure). Non-negative values usually map to Play GamesApiException.statusCode.
| Component | Version |
|---|---|
| Compile SDK | 35 |
| Min SDK | 24 (Android 7.0) |
| Target SDK | 35 |
| Play Games SDK (AAR compileOnly) | 21.0.0 |
| Play Games SDK (Godot export dependency) | 20.1.0 |
| Godot Library | 4.5.1.stable |
| JDK | 17 |
INTERNET- Required for Play Games ServicesACCESS_NETWORK_STATE- Network connectivity check
If you use backend verification with requestServerAuthCode(), your Android app resources must include one of:
@string/default_web_client_id(preferred)@string/server_client_id
These come from your Google/Firebase config for the app package and are required for GamesSignInClient.requestServerSideAccess(...).
- Go to Google Play Console
- Select your app → Play Games Services → Setup and management → Configuration
- Copy the numeric Project ID (12+ digits, not the OAuth client ID)
This is the most common issue. Check the following:
-
AndroidManifest.xml must have v2 plugin registration:
<application> <meta-data android:name="org.godotengine.plugin.v2.PlayGamesPlugin" android:value="com.mladenstojanovic.playgamesplugin.PlayGamesPlugin" /> </application>
-
AAR files must be in the correct location with correct names:
addons/play_games_plugin/PlayGamesPlugin-debug.aaraddons/play_games_plugin/PlayGamesPlugin-release.aar
-
Plugin must be enabled in Project Settings → Plugins
-
Android Build Template must be installed (Project → Install Android Build Template)
- Verify your App ID is correct in
play_games_plugin.gd - Check that your app's SHA-1 fingerprint is registered in Play Console
- Ensure Play Games Services is properly configured in Play Console
- Ensure your exported app declares
INTERNETpermission (export_presets.cfg). - Verify that your test account has an initialized Play Games profile on that specific device/account state.
- Check logcat for detailed error messages:
adb logcat | grep -E "(PlayGames|GamesSignIn)"
- Xiaomi-family devices (Xiaomi/Redmi/Poco) can repeatedly show account/profile chooser dialogs if auth checks run too early at startup.
- Current plugin behavior intentionally defers startup auth check on those devices; it waits for explicit
signIn()calls from UI flow. - If you require auto-login UX, trigger it after first menu frame/UI settle, not during earliest app bootstrap.
| Error | Cause | Solution |
|---|---|---|
DEVELOPER_ERROR |
SHA-1 mismatch or wrong App ID | Register debug/release SHA-1 in Play Console |
SIGN_IN_REQUIRED |
No valid active Games auth session for that API call | Trigger signIn(); plugin retries once for leaderboard calls |
NETWORK_ERROR |
No internet connection | Check device connectivity |
IllegalStateException: Cannot use snapshots without enabling the 'Saved Game' feature |
Saved Games disabled in Play Console | Enable Saved games in Play Games Services configuration |
java.lang.IllegalArgumentException: Invalid type for argument #1. Should be of type long |
Signal argument type mismatch (Int emitted for signal declared as long) |
Emit Kotlin Long (toLong()), declare signal parameter as Long::class.javaObjectType |
Not signed in with status -3 on snapshot calls |
Cloud operation called before confirmed auth | Wait for sign_in_success/player_info_loaded or gate with isSignedIn() |
loadPlayerScore(...) is resilient by design: if API lookup fails, plugin still emits leaderboard_player_score_loaded with a fallback payload (rank: "-", score: 0) and includes optional status_code / error fields for UI diagnostics.
This keeps leaderboard UI rendering stable without forcing a hard failure signal path.
- Device must have Google Play Services installed
- Some emulators don't include Play Services (use a physical device)
If you see errors about missing Material Components resources:
- The AAR should NOT include Material Components resources
- Rebuild the plugin with
compileOnlydependencies (notimplementation) - Current build.gradle.kts uses:
dependencies { compileOnly("org.godotengine:godot:4.5.1.stable") compileOnly("com.google.android.gms:play-services-games-v2:21.0.0") }
If leaderboard calls crash or fail in debug builds:
- Make sure your debug keystore SHA-1 is added in Play Console → Play Games Services.
- If the user is not signed in, leaderboard calls will fail. Guard with
isSignedIn()in GDScript. - Physical device is recommended; some emulators don’t support Play Games sign-in.
unlockAchievement(id)incrementAchievement(id, steps)showAchievementsUI()
- Optional native leaderboard intent wrappers (
showLeaderboardUI,showAllLeaderboardsUI) - Optional paging/filter helpers for large custom leaderboard views
- Play Games Services v2 Migration Guide
- Android Sign-in Documentation
- Godot Android Plugin Documentation
- Google Play Console
- Godot Android Plugin Template
MIT License - Use freely in your Godot projects.