Enhanced haptic feedback for gamepad users: - Added gamepad connection tracking - Increased vibration intensity by 50% when using gamepad - Adjusted vibration durations and amplitudes for better feedback

This commit is contained in:
cmclark00 2025-03-31 08:40:54 -04:00
parent dbaebb8b60
commit c4f103ae1e
3 changed files with 92 additions and 32 deletions

View file

@ -269,6 +269,15 @@ class MainActivity : AppCompatActivity(),
updateUI(score, level, lines)
}
// Track pieces placed using callback
gameBoard.onPieceLock = {
// Increment pieces placed counter
piecesPlaced++
// Provide haptic feedback
gameHaptics.vibrateForPieceLock()
}
gameView.onGameOver = { finalScore ->
// Pause music on game over
gameMusic.pause()
@ -325,15 +334,6 @@ class MainActivity : AppCompatActivity(),
}
}
// Track pieces placed using callback
gameView.onPieceLock = {
// Increment pieces placed counter
piecesPlaced++
// Provide haptic feedback
gameHaptics.vibrateForPieceLock()
}
// Set up button click listeners with haptic feedback
binding.playAgainButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
@ -484,6 +484,12 @@ class MainActivity : AppCompatActivity(),
Log.d("MainActivity", "Triggering game over animation")
gameView.startGameOverAnimation()
// Hide game UI elements in landscape mode
if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
binding.leftControlsPanel?.visibility = View.GONE
binding.rightControlsPanel?.visibility = View.GONE
}
// Wait a moment before showing progression screen to let animation be visible
Handler(Looper.getMainLooper()).postDelayed({
// Show progression screen first with XP animation
@ -660,10 +666,12 @@ class MainActivity : AppCompatActivity(),
* Start a new game
*/
private fun startGame() {
// Reset pieces placed counter
piecesPlaced = 0
// Set initial game state
currentScore = 0
currentLevel = selectedLevel
piecesPlaced = 0
gameStartTime = System.currentTimeMillis()
// Update UI to show initial values
@ -673,8 +681,23 @@ class MainActivity : AppCompatActivity(),
binding.comboText.text = "0"
// Reset game view and game board
gameBoard.reset()
gameView.reset()
// Show game elements
gameView.visibility = View.VISIBLE
binding.gameControlsContainer.visibility = View.VISIBLE
binding.gameOverContainer.visibility = View.GONE
binding.pauseContainer.visibility = View.GONE
titleScreen.visibility = View.GONE
progressionScreen.visibility = View.GONE
// Show game UI elements in landscape mode
if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
binding.leftControlsPanel?.visibility = View.VISIBLE
binding.rightControlsPanel?.visibility = View.VISIBLE
}
// Configure callbacks
gameView.onGameStateChanged = { score, level, lines ->
currentScore = score
@ -729,15 +752,19 @@ class MainActivity : AppCompatActivity(),
statsManager.recordLineClear(lineCount)
}
// Start the game
gameView.start()
gameMusic.setEnabled(isMusicEnabled)
// Reset pause button state
binding.pauseStartButton.visibility = View.VISIBLE
binding.resumeButton.visibility = View.GONE
// Start background music if enabled
if (isMusicEnabled) {
gameMusic.start()
}
// Start the game
gameView.start()
gameMusic.setEnabled(isMusicEnabled)
// Reset session stats
statsManager.startNewSession()
progressionManager.startNewSession()
@ -957,6 +984,8 @@ class MainActivity : AppCompatActivity(),
override fun onGamepadConnected(gamepadName: String) {
runOnUiThread {
Toast.makeText(this, "Gamepad connected: $gamepadName", Toast.LENGTH_SHORT).show()
// Set gamepad connected state in haptics
gameHaptics.setGamepadConnected(true)
// Provide haptic feedback for gamepad connection
gameHaptics.vibrateForPieceLock()
@ -970,6 +999,8 @@ class MainActivity : AppCompatActivity(),
override fun onGamepadDisconnected(gamepadName: String) {
runOnUiThread {
Toast.makeText(this, "Gamepad disconnected: $gamepadName", Toast.LENGTH_SHORT).show()
// Set gamepad disconnected state in haptics
gameHaptics.setGamepadConnected(false)
}
}

View file

@ -27,6 +27,19 @@ class GameHaptics(private val context: Context) {
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
// Track if gamepad is connected
private var isGamepadConnected = false
// Set gamepad connection state
fun setGamepadConnected(connected: Boolean) {
isGamepadConnected = connected
}
// Get vibration multiplier based on input method
private fun getVibrationMultiplier(): Float {
return if (isGamepadConnected) 1.5f else 1.0f
}
// Vibrate for line clear (more intense for more lines)
fun vibrateForLineClear(lineCount: Int) {
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
@ -34,22 +47,22 @@ class GameHaptics(private val context: Context) {
// Only proceed if the device has a vibrator and it's available
if (!vibrator.hasVibrator()) return
// Scale duration and amplitude based on line count
// More lines = longer and stronger vibration
// Scale duration and amplitude based on line count and input method
val multiplier = getVibrationMultiplier()
val duration = when(lineCount) {
1 -> 50L // Single line: short vibration
2 -> 80L // Double line: slightly longer
3 -> 120L // Triple line: even longer
4 -> 200L // Tetris: longest vibration
else -> 50L
1 -> (50L * multiplier).toLong() // Single line: short vibration
2 -> (80L * multiplier).toLong() // Double line: slightly longer
3 -> (120L * multiplier).toLong() // Triple line: even longer
4 -> (200L * multiplier).toLong() // Tetris: longest vibration
else -> (50L * multiplier).toLong()
}
val amplitude = when(lineCount) {
1 -> 80 // Single line: mild vibration (80/255)
2 -> 120 // Double line: medium vibration (120/255)
3 -> 180 // Triple line: strong vibration (180/255)
4 -> 255 // Tetris: maximum vibration (255/255)
else -> 80
1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration
2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration
3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration
4 -> 255 // Tetris: maximum vibration
else -> (80 * multiplier).toInt().coerceAtMost(255)
}
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
@ -79,15 +92,20 @@ class GameHaptics(private val context: Context) {
fun vibrateForPieceLock() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE)
val multiplier = getVibrationMultiplier()
val duration = (50L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
}
}
fun vibrateForPieceMove() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3).toInt().coerceAtLeast(1)
val vibrationEffect = VibrationEffect.createOneShot(20L, amplitude)
val multiplier = getVibrationMultiplier()
val duration = (20L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3 * multiplier).toInt().coerceAtLeast(1).coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
}
}
@ -100,8 +118,10 @@ class GameHaptics(private val context: Context) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create a strong, long vibration effect
val vibrationEffect = VibrationEffect.createOneShot(300L, VibrationEffect.DEFAULT_AMPLITUDE)
val multiplier = getVibrationMultiplier()
val duration = (300L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
Log.d(TAG, "Game over vibration triggered successfully")
} else {

View file

@ -245,6 +245,7 @@ class TitleScreen @JvmOverloads constructor(
// Draw high scores using pre-allocated manager
val highScores: List<HighScore> = highScoreManager.getHighScores()
val highScoreY = height * 0.5f
var lastHighScoreY = highScoreY
if (highScores.isNotEmpty()) {
// Calculate the starting X position to center the entire block of scores
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
@ -252,6 +253,7 @@ class TitleScreen @JvmOverloads constructor(
highScores.forEachIndexed { index: Int, score: HighScore ->
val y = highScoreY + (index * 80f)
lastHighScoreY = y // Track the last high score's Y position
// Pad the rank number to ensure alignment
val rank = (index + 1).toString().padStart(2, ' ')
// Pad the name to ensure score alignment
@ -260,8 +262,15 @@ class TitleScreen @JvmOverloads constructor(
}
}
// Draw "touch to start" prompt
canvas.drawText("touch to start", width / 2f, height * 0.7f, promptPaint)
// Draw "touch to start" prompt below the high scores
val promptY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
// In landscape mode, position below the last high score with some padding
lastHighScoreY + 100f
} else {
// In portrait mode, use the original position
height * 0.7f
}
canvas.drawText("touch to start", width / 2f, promptY, promptPaint)
// Request another frame
invalidate()