Implement full gamepad menu support and title screen start

This commit is contained in:
cmclark00 2025-03-31 07:52:01 -04:00
parent 779fa8eab1
commit f4f40c4c34
2 changed files with 155 additions and 3 deletions

View file

@ -41,7 +41,8 @@ import android.app.AlertDialog
class MainActivity : AppCompatActivity(),
GamepadController.GamepadConnectionListener,
GamepadController.GamepadMenuListener {
GamepadController.GamepadMenuListener,
GamepadController.GamepadNavigationListener {
companion object {
private const val TAG = "MainActivity"
@ -93,6 +94,10 @@ class MainActivity : AppCompatActivity(),
// Track connected gamepads to detect changes
private val connectedGamepads = mutableSetOf<String>()
// Track currently selected menu item in pause menu for gamepad navigation
private var currentMenuSelection = 0
private val pauseMenuItems = mutableListOf<View>()
override fun onCreate(savedInstanceState: Bundle?) {
// Register activity result launcher for high score entry
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -124,6 +129,7 @@ class MainActivity : AppCompatActivity(),
gamepadController = GamepadController(gameView)
gamepadController.setGamepadConnectionListener(this)
gamepadController.setGamepadMenuListener(this)
gamepadController.setGamepadNavigationListener(this)
// Set up touch event forwarding
binding.touchInterceptor?.setOnTouchListener { _, event ->
@ -395,6 +401,9 @@ class MainActivity : AppCompatActivity(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
}
// Initialize pause menu items for gamepad navigation
initPauseMenuNavigation()
}
/**
@ -576,7 +585,7 @@ class MainActivity : AppCompatActivity(),
unlockedThemes = progressionManager.getUnlockedThemes(),
currentTheme = currentTheme
)
// Update block skin selector - handle both standard and landscape versions
blockSkinSelector.updateBlockSkins(
progressionManager.getUnlockedBlocks(),
@ -591,6 +600,15 @@ class MainActivity : AppCompatActivity(),
gameView.getCurrentBlockSkin(),
progressionManager.getPlayerLevel()
)
// Initialize pause menu navigation
initPauseMenuNavigation()
// Reset and highlight first selectable menu item
if (pauseMenuItems.isNotEmpty()) {
currentMenuSelection = if (binding.resumeButton.visibility == View.VISIBLE) 0 else 1
highlightMenuItem(currentMenuSelection)
}
}
/**
@ -1013,6 +1031,13 @@ class MainActivity : AppCompatActivity(),
// First check if it's a gamepad input
if (GamepadController.isGamepad(event.device)) {
// Handle title screen start with gamepad
if (titleScreen.visibility == View.VISIBLE &&
(keyCode == KeyEvent.KEYCODE_BUTTON_A || keyCode == KeyEvent.KEYCODE_BUTTON_START)) {
titleScreen.performClick()
return true
}
// If gamepad input was handled by the controller, consume the event
if (gamepadController.handleKeyEvent(keyCode, event)) {
return true
@ -1086,7 +1111,93 @@ class MainActivity : AppCompatActivity(),
// If pause menu is showing, handle as a resume
hidePauseMenu()
resumeGame()
} else if (titleScreen.visibility == View.VISIBLE) {
// If title screen is showing, start the game
titleScreen.performClick()
}
}
}
/**
* Implements GamepadNavigationListener to handle menu navigation
*/
override fun onMenuUp() {
runOnUiThread {
if (binding.pauseContainer.visibility == View.VISIBLE) {
moveMenuSelectionUp()
}
}
}
override fun onMenuDown() {
runOnUiThread {
if (binding.pauseContainer.visibility == View.VISIBLE) {
moveMenuSelectionDown()
}
}
}
override fun onMenuSelect() {
runOnUiThread {
if (binding.pauseContainer.visibility == View.VISIBLE) {
activateSelectedMenuItem()
} else if (titleScreen.visibility == View.VISIBLE) {
titleScreen.performClick()
}
}
}
/**
* Initialize pause menu items for gamepad navigation
*/
private fun initPauseMenuNavigation() {
pauseMenuItems.clear()
pauseMenuItems.add(binding.resumeButton)
pauseMenuItems.add(binding.pauseStartButton)
pauseMenuItems.add(binding.pauseRestartButton)
pauseMenuItems.add(binding.highScoresButton)
pauseMenuItems.add(binding.statsButton)
pauseMenuItems.add(binding.pauseLevelUpButton)
pauseMenuItems.add(binding.pauseLevelDownButton)
}
/**
* Highlight the currently selected menu item
*/
private fun highlightMenuItem(index: Int) {
// Reset all items to normal state
pauseMenuItems.forEachIndexed { i, item ->
item.alpha = if (i == index) 1.0f else 0.7f
item.scaleX = if (i == index) 1.2f else 1.0f
item.scaleY = if (i == index) 1.2f else 1.0f
}
}
/**
* Move menu selection up
*/
private fun moveMenuSelectionUp() {
if (pauseMenuItems.isEmpty()) return
currentMenuSelection = (currentMenuSelection - 1 + pauseMenuItems.size) % pauseMenuItems.size
highlightMenuItem(currentMenuSelection)
gameHaptics.vibrateForPieceMove()
}
/**
* Move menu selection down
*/
private fun moveMenuSelectionDown() {
if (pauseMenuItems.isEmpty()) return
currentMenuSelection = (currentMenuSelection + 1) % pauseMenuItems.size
highlightMenuItem(currentMenuSelection)
gameHaptics.vibrateForPieceMove()
}
/**
* Activate the currently selected menu item
*/
private fun activateSelectedMenuItem() {
if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return
pauseMenuItems[currentMenuSelection].performClick()
}
}

View file

@ -149,9 +149,16 @@ class GamepadController(
fun onPauseRequested()
}
interface GamepadNavigationListener {
fun onMenuUp()
fun onMenuDown()
fun onMenuSelect()
}
// Listeners
private var connectionListener: GamepadConnectionListener? = null
private var menuListener: GamepadMenuListener? = null
private var navigationListener: GamepadNavigationListener? = null
// Currently active gamepad for rumble
private var activeGamepad: InputDevice? = null
@ -170,6 +177,13 @@ class GamepadController(
menuListener = listener
}
/**
* Set a listener for gamepad navigation events
*/
fun setGamepadNavigationListener(listener: GamepadNavigationListener) {
navigationListener = listener
}
/**
* Function to check for newly connected gamepads.
* Call this periodically from the activity to detect connection changes.
@ -253,7 +267,34 @@ class GamepadController(
*/
fun handleKeyEvent(keyCode: Int, event: KeyEvent): Boolean {
// Skip if game is not active
if (!gameView.isActive()) return false
if (!gameView.isActive()) {
// Handle menu navigation even when game is not active
if (event.action == KeyEvent.ACTION_DOWN) {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
navigationListener?.onMenuUp()
return true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
navigationListener?.onMenuDown()
return true
}
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_DPAD_CENTER -> {
navigationListener?.onMenuSelect()
return true
}
}
}
// Handle menu/start button for pause menu
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BUTTON_START) {
menuListener?.onPauseRequested()
return true
}
return false
}
val device = event.device
if (!isGamepad(device)) return false