From 220caa39f72c2aefb0346636d388be405e459814 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Wed, 2 Apr 2025 14:32:09 -0400 Subject: [PATCH] Enhance: Integrate Google Play Games Services for global leaderboard - Added global leaderboard functionality via Google Play Games Services. - Implemented sign-in process for Google Play Games and score submission. - Updated UI to include leaderboard buttons in both MainActivity and HighScoresActivity. - Enhanced README to document new online features and leaderboard integration. - Added necessary dependencies in build.gradle and updated AndroidManifest.xml for Google Play Games configuration. --- README.md | 8 ++ app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 3 + .../com/pixelmintdrop/HighScoresActivity.kt | 28 +++++- .../java/com/pixelmintdrop/MainActivity.kt | 73 +++++++++++++- .../model/GooglePlayGamesManager.kt | 96 +++++++++++++++++++ .../pixelmintdrop/model/HighScoreManager.kt | 27 ++++++ app/src/main/res/layout/activity_main.xml | 13 +++ app/src/main/res/layout/high_scores.xml | 40 ++++++-- app/src/main/res/values/strings.xml | 1 + 10 files changed, 279 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/pixelmintdrop/model/GooglePlayGamesManager.kt diff --git a/README.md b/README.md index 3e4bad7..eb4281f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A modern falling block puzzle game for Android, featuring smooth animations, res - Hard drop and soft drop controls - Advanced move detection (e.g., T-Spins) and scoring - Persistent high score system (Top 5) +- Global leaderboard via Google Play Games Services ### Modern Android Features - Optimized for Android 11+ (API 30+) @@ -20,6 +21,7 @@ A modern falling block puzzle game for Android, featuring smooth animations, res - Automatic Dark theme support - Intuitive and responsive touch controls - Full edge-to-edge display utilization +- Google Play Games Services integration for leaderboards ### Scoring System @@ -74,6 +76,12 @@ Pixelmint Drop features a comprehensive scoring system designed to reward skillf - Multiple theme options with ability to change manually or enable Random Mode (unlocked when 2+ themes are available). - In Random Mode, themes change automatically every 10 line clears (1 level). +### Online Features +- Global leaderboard support through Google Play Games Services +- Sign in with your Google account to compete with players worldwide +- Your highest scores are automatically submitted to the leaderboard +- View the global leaderboard directly from the game + ## Technical Details ### Requirements diff --git a/app/build.gradle b/app/build.gradle index 0466731..42353b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,8 @@ dependencies { implementation 'androidx.window:window:1.2.0' // For better display support implementation 'androidx.window:window-java:1.2.0' implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.google.android.gms:play-services-games-v2:19.0.0' + implementation 'com.google.android.gms:play-services-auth:20.7.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f3d41de..33cbafa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.pixelmintdrop"> + Color.WHITE } - // Apply theme to back button + // Apply theme to buttons binding.backButton.setTextColor(textColor) + binding.leaderboardButton?.setTextColor(textColor) // Update adapter theme highScoreAdapter.applyTheme(themeId) @@ -122,6 +129,20 @@ class HighScoresActivity : AppCompatActivity() { Log.e("HighScoresActivity", "Error updating high scores", e) } } + + private fun showGlobalLeaderboard() { + try { + if (!highScoreManager.isGooglePlaySignedIn()) { + Toast.makeText(this, "Signing in to Google Play Games...", Toast.LENGTH_SHORT).show() + } + + // Show the leaderboard + highScoreManager.getGooglePlayGamesManager().showLeaderboard(this) + } catch (e: Exception) { + Log.e("HighScoresActivity", "Error showing leaderboard", e) + Toast.makeText(this, "Could not open leaderboard", Toast.LENGTH_SHORT).show() + } + } override fun onResume() { super.onResume() @@ -143,6 +164,11 @@ class HighScoresActivity : AppCompatActivity() { finish() return true } + KeyEvent.KEYCODE_BUTTON_Y -> { + // Y button shows global leaderboard + showGlobalLeaderboard() + return true + } } } diff --git a/app/src/main/java/com/pixelmintdrop/MainActivity.kt b/app/src/main/java/com/pixelmintdrop/MainActivity.kt index 5701d19..ea584df 100644 --- a/app/src/main/java/com/pixelmintdrop/MainActivity.kt +++ b/app/src/main/java/com/pixelmintdrop/MainActivity.kt @@ -55,6 +55,10 @@ import androidx.core.view.updatePadding import kotlin.math.max import kotlin.math.min import kotlin.random.Random +// Google Play Games Services imports +import com.google.android.gms.games.PlayGames +import com.google.android.gms.games.PlayGamesSdk +import com.google.android.gms.games.GamesSignInClient class MainActivity : AppCompatActivity(), GamepadController.GamepadConnectionListener, @@ -68,6 +72,9 @@ class MainActivity : AppCompatActivity(), // ViewModel private val viewModel: MainActivityViewModel by viewModels() // Added ViewModel + // Google Play Games Services + private lateinit var gamesSignInClient: GamesSignInClient + // UI components private lateinit var binding: ActivityMainBinding private lateinit var gameView: GameView @@ -157,6 +164,13 @@ class MainActivity : AppCompatActivity(), binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + // Initialize Google Play Games Services + PlayGamesSdk.initialize(this) + gamesSignInClient = PlayGames.getGamesSignInClient(this) + + // Request sign-in silently + signInToPlayGames() + // Store initial padding values before applying insets val initialPausePadding = Rect(binding.pauseContainer.paddingLeft, binding.pauseContainer.paddingTop, binding.pauseContainer.paddingRight, binding.pauseContainer.paddingBottom) @@ -529,9 +543,16 @@ class MainActivity : AppCompatActivity(), startGame() } + // High Scores button binding.highScoresButton.setOnClickListener { - gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) - showHighScores() + binding.pauseContainer.visibility = View.GONE + val intent = Intent(this, HighScoresActivity::class.java) + startActivity(intent) + } + + // Leaderboard button + binding.leaderboardButton?.setOnClickListener { + showLeaderboard() } binding.pauseLevelUpButton.setOnClickListener { @@ -815,6 +836,7 @@ class MainActivity : AppCompatActivity(), binding.pauseRestartButton.setTextColor(themeColor) binding.resumeButton.setTextColor(themeColor) binding.highScoresButton.setTextColor(themeColor) + binding.leaderboardButton?.setTextColor(themeColor) binding.statsButton.setTextColor(themeColor) binding.pauseLevelUpButton?.setTextColor(themeColor) // Safe call binding.pauseLevelDownButton?.setTextColor(themeColor) // Safe call @@ -1640,6 +1662,7 @@ class MainActivity : AppCompatActivity(), // Group 2: Stats and Scoring orderedViews.add(binding.highScoresButton) + orderedViews.add(binding.leaderboardButton) orderedViews.add(binding.statsButton) // Group 3: Level selection (use safe calls) @@ -2206,4 +2229,50 @@ class MainActivity : AppCompatActivity(), Log.d("RandomMode", "Cannot apply random theme/skin - no unlocked options available") } } + + /** + * Handles sign-in to Google Play Games Services + */ + private fun signInToPlayGames() { + gamesSignInClient.isAuthenticated.addOnCompleteListener { task -> + val isAuthenticated = task.isSuccessful && task.result.isAuthenticated + + if (!isAuthenticated) { + // Silent sign-in failed, offer to sign in explicitly + Log.d(TAG, "Not authenticated with Play Games, will prompt user later") + + // We don't show UI for this immediately, we'll add a button to the pause menu + } else { + Log.d(TAG, "Already authenticated with Play Games") + + // If we were already authenticated, make sure scores are synced + val lastHighScore = highScoreManager.getHighScores().maxByOrNull { it.score } + lastHighScore?.let { + highScoreManager.getGooglePlayGamesManager().submitScore(it.score.toLong(), this) + } + } + } + } + + // Method to show Google Play Games leaderboard + private fun showLeaderboard() { + gamesSignInClient.isAuthenticated.addOnCompleteListener { task -> + val isAuthenticated = task.isSuccessful && task.result.isAuthenticated + + if (isAuthenticated) { + // Show leaderboard + highScoreManager.getGooglePlayGamesManager().showLeaderboard(this) + } else { + // Sign in first + gamesSignInClient.signIn().addOnCompleteListener { signInTask -> + if (signInTask.isSuccessful) { + // Now try to show leaderboard again + highScoreManager.getGooglePlayGamesManager().showLeaderboard(this) + } else { + Toast.makeText(this, "Sign-in required to view leaderboard", Toast.LENGTH_SHORT).show() + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelmintdrop/model/GooglePlayGamesManager.kt b/app/src/main/java/com/pixelmintdrop/model/GooglePlayGamesManager.kt new file mode 100644 index 0000000..3dcf6ae --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/model/GooglePlayGamesManager.kt @@ -0,0 +1,96 @@ +package com.pixelmintdrop.model + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.games.PlayGames +import com.google.android.gms.games.PlayGamesSdk +import com.google.android.gms.tasks.Task + +class GooglePlayGamesManager(private val context: Context) { + private val TAG = "GooglePlayGamesManager" + + // Leaderboard ID + companion object { + const val LEADERBOARD_ID = "CgkImJW2mKsSEAIQAQ" + } + + // Initialize the Play Games SDK + init { + try { + PlayGamesSdk.initialize(context) + Log.d(TAG, "PlayGamesSdk initialized") + } catch (e: Exception) { + Log.e(TAG, "Error initializing PlayGamesSdk", e) + } + } + + // Check if user is already signed in + fun isSignedIn(): Boolean { + val account = GoogleSignIn.getLastSignedInAccount(context) + return account != null && !account.isExpired + } + + // Get the sign-in client + fun getSignInClient(): GoogleSignInClient { + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).build() + return GoogleSignIn.getClient(context, signInOptions) + } + + // Handle sign-in result + fun handleSignInResult(task: Task): Boolean { + return if (task.isSuccessful) { + Log.d(TAG, "Sign-in successful") + true + } else { + Log.e(TAG, "Sign-in failed", task.exception) + false + } + } + + // Submit score to leaderboard (requires an activity for potential sign-in) + fun submitScore(score: Long, activity: Activity? = null) { + val account = GoogleSignIn.getLastSignedInAccount(context) + if (account == null) { + Log.w(TAG, "Not signed in, score will not be submitted to Google Play Games") + return + } + + try { + val leaderboardsClient = PlayGames.getLeaderboardsClient(activity ?: return) + leaderboardsClient.submitScore(LEADERBOARD_ID, score) + Log.d(TAG, "Score $score submitted to leaderboard $LEADERBOARD_ID") + } catch (e: Exception) { + Log.e(TAG, "Error submitting score to leaderboard", e) + } + } + + // Show the leaderboard + fun showLeaderboard(activity: Activity) { + val account = GoogleSignIn.getLastSignedInAccount(context) + if (account == null) { + Log.w(TAG, "Not signed in, cannot show leaderboard") + return + } + + try { + val leaderboardsClient = PlayGames.getLeaderboardsClient(activity) + leaderboardsClient + .getLeaderboardIntent(LEADERBOARD_ID) + .addOnSuccessListener { intent -> + activity.startActivity(intent) + } + .addOnFailureListener { e -> + Log.e(TAG, "Error showing leaderboard", e) + } + } catch (e: Exception) { + Log.e(TAG, "Error showing leaderboard", e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt b/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt index bf35f52..de48594 100644 --- a/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt +++ b/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt @@ -10,6 +10,7 @@ class HighScoreManager(private val context: Context) { private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) private val gson = Gson() private val type: Type = object : TypeToken>() {}.type + private val googlePlayGamesManager = GooglePlayGamesManager(context) companion object { private const val PREFS_NAME = "pixelmintdrop_highscores" @@ -27,6 +28,7 @@ class HighScoreManager(private val context: Context) { } fun addHighScore(highScore: HighScore) { + // Save to local high scores val currentScores = getHighScores().toMutableList() currentScores.add(highScore) @@ -37,6 +39,9 @@ class HighScoreManager(private val context: Context) { // Save to SharedPreferences val json = gson.toJson(topScores) prefs.edit().putString(KEY_HIGHSCORES, json).apply() + + // Submit to Google Play Games leaderboard if signed in + submitScoreToGooglePlay(highScore.score) } fun isHighScore(score: Int): Boolean { @@ -44,4 +49,26 @@ class HighScoreManager(private val context: Context) { return currentScores.size < MAX_HIGHSCORES || score > (currentScores.lastOrNull()?.score ?: 0) } + + // Submit score to Google Play Games leaderboard + private fun submitScoreToGooglePlay(score: Int) { + // We don't have an activity here, so we can't submit the score directly + // The score will be submitted later when an activity is available + try { + googlePlayGamesManager.submitScore(score.toLong(), null) + } catch (e: Exception) { + // Log error but don't crash + android.util.Log.e("HighScoreManager", "Error submitting score to leaderboard", e) + } + } + + // Method to check if user is signed in to Google Play Games + fun isGooglePlaySignedIn(): Boolean { + return googlePlayGamesManager.isSignedIn() + } + + // Get Google Play Games manager + fun getGooglePlayGamesManager(): GooglePlayGamesManager { + return googlePlayGamesManager + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8552880..9dbef3f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -406,6 +406,19 @@ android:textStyle="bold" android:fontFamily="sans-serif" android:textAllCaps="false" /> + +