I wanted to build something very similar but I didn't love the idea of manipulating the cursor like the other answer as it feels really hacky, though it might work.
In my case, I was also looking to be able to control everything with a controller as well. So I started with creating this concept of a SelectableButtonGroup which can take a Table of Buttons.
import com.badlogic.gdx.scenes.scene2d.Group class SelectableButtonGroup(private val buttonGroup: Group) : SimulatesTouchOverButtons() { private var currentButtonIndex = 0 fun changeSelectedButton(isUpPressed: Boolean): Boolean { unselectButton(buttonGroup.children[currentButtonIndex]) val change = if (isUpPressed) { -1 } else { 1 } // make sure the index wraps around currentButtonIndex = (currentButtonIndex + buttonGroup.children.size + change) % buttonGroup.children.size return selectButton(buttonGroup.children[currentButtonIndex]) } fun clickSelected(): Boolean { val currentButton = buttonGroup.children[currentButtonIndex] return clickButton(currentButton) } fun releaseSelected(): Boolean { val currentButton = buttonGroup.children[currentButtonIndex] return releaseButton(currentButton) } fun isEmpty(): Boolean { return buttonGroup.children.size == 0 } }
Above uses this somewhat generic class for mimicking the mouse/touch events:
import com.badlogic.gdx.Input import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.utils.Pools open class SimulatesTouchOverButtons { /** * Simulate button click down. * * @param button * @return */ protected fun clickButton(button: Actor): Boolean { val event = Pools.obtain( InputEvent::class.java ) event.type = InputEvent.Type.touchDown event.pointer = -1 event.button = Input.Buttons.LEFT button.fire(event) val handled = event.isHandled Pools.free(event) return handled } /** * Simulate button click release. * * @param button * @return */ protected fun releaseButton(button: Actor): Boolean { val event = Pools.obtain( InputEvent::class.java ) event.type = InputEvent.Type.touchUp event.pointer = -1 event.button = Input.Buttons.LEFT button.fire(event) val handled = event.isHandled Pools.free(event) return handled } /** * Simulate mousing over a button. * * @param button * @return */ protected fun selectButton(button: Actor): Boolean { val event = Pools.obtain( InputEvent::class.java ) event.type = InputEvent.Type.enter event.pointer = -1 button.fire(event) val handled = event.isHandled Pools.free(event) return handled } /** * Simulate mousing off of a button. * * @param button * @return */ protected fun unselectButton(button: Actor): Boolean { val event = Pools.obtain( InputEvent::class.java ) event.type = InputEvent.Type.exit event.pointer = -1 button.fire(event) val handled = event.isHandled Pools.free(event) return handled } }
With SelectableButtonGroup we can implement a com.badlogic.gdx.InputProcessor or com.badlogic.gdx.controllers.ControllerListener and by sharing the same one the switching between keyboard and controller works pretty well with the shared state.
Here's the InputProcessor:
import com.badlogic.gdx.Input import com.badlogic.gdx.InputAdapter import com.badlogic.gdx.InputProcessor // ButtonGroupInputProcessor wraps an input processor by intercepting up and down and modifying // a SelectableButtonGroup. class ButtonGroupInputProcessor( private val inputProcessor: InputProcessor, private val selectableButtonGroup: SelectableButtonGroup, ) : InputAdapter() { override fun touchDragged(screenX: Int, screenY: Int, pointer: Int): Boolean { return inputProcessor.touchDragged(screenX, screenY, pointer) } override fun mouseMoved(screenX: Int, screenY: Int): Boolean { return inputProcessor.mouseMoved(screenX, screenY) } override fun keyDown(keycode: Int): Boolean { when (keycode) { Input.Keys.UP -> selectableButtonGroup.changeSelectedButton(isUpPressed = true) Input.Keys.DOWN -> selectableButtonGroup.changeSelectedButton(isUpPressed = false) Input.Keys.ENTER -> selectableButtonGroup.clickSelected() } return inputProcessor.keyDown(keycode) } override fun keyUp(keycode: Int): Boolean { when (keycode) { Input.Keys.ENTER -> selectableButtonGroup.releaseSelected() } return inputProcessor.keyUp(keycode) } override fun keyTyped(character: Char): Boolean { return inputProcessor.keyTyped(character) } override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean { return inputProcessor.touchDown(screenX, screenY, pointer, button) } override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean { return inputProcessor.touchUp(screenX, screenY, pointer, button) } override fun touchCancelled(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean { return inputProcessor.touchCancelled(screenX, screenY, pointer, button) } override fun scrolled(amountX: Float, amountY: Float): Boolean { return inputProcessor.scrolled(amountX, amountY) } }
Then back to your Stage code it could look something like this:
... val buttonGroup = Table() buttonGroup.add(startButton).row() buttonGroup.add(optionsButton).row() buttonGroup.add(exitButton) // Add the table to the stage stage.addActor(buttonGroup) // Add the listeners for the buttons val selectableButtonGroup = SelectableButtonGroup(buttonGroup) Controllers.addListener(ButtonGroupControllerListener(selectableButtonGroup)) // add a keyboard listener Gdx.input.inputProcessor = ButtonGroupInputProcessor(stage, selectableButtonGroup) ```