mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-17 23:45:22 +01:00
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.
This commit is contained in:
parent
21b2513ad4
commit
220caa39f7
10 changed files with 279 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.pixelmintdrop">
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.games.APP_ID"
|
||||
android:value="@string/app_id" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
|
@ -13,6 +13,7 @@ import android.view.KeyEvent
|
|||
import android.view.InputDevice
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
|
@ -68,6 +69,11 @@ class HighScoresActivity : AppCompatActivity() {
|
|||
binding.backButton.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
// Set up leaderboard button
|
||||
binding.leaderboardButton?.setOnClickListener {
|
||||
showGlobalLeaderboard()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("HighScoresActivity", "Error in onCreate", e)
|
||||
// Show an error message if necessary, or finish gracefully
|
||||
|
@ -104,8 +110,9 @@ class HighScoresActivity : AppCompatActivity() {
|
|||
else -> 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)
|
||||
|
@ -123,6 +130,20 @@ class HighScoresActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
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()
|
||||
try {
|
||||
|
@ -143,6 +164,11 @@ class HighScoresActivity : AppCompatActivity() {
|
|||
finish()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_BUTTON_Y -> {
|
||||
// Y button shows global leaderboard
|
||||
showGlobalLeaderboard()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GoogleSignInAccount>): 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<List<HighScore>>() {}.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
|
||||
}
|
||||
}
|
|
@ -407,6 +407,19 @@
|
|||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/leaderboardButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="Global Leaderboard"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/statsButton"
|
||||
android:layout_width="200dp"
|
||||
|
|
|
@ -25,15 +25,37 @@
|
|||
android:layout_weight="1"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<Button
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/back"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:fontFamily="monospace"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/leaderboardButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="Global Leaderboard"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:fontFamily="monospace"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Pixel Mint Drop</string>
|
||||
<string name="app_id">630069234328</string>
|
||||
<string name="game_over">game over</string>
|
||||
<string name="score">score</string>
|
||||
<string name="level">level</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue