1
\$\begingroup\$

I am trying to implement keyboard movement in game main menu which is libGDX Stage with UI-Actors. I'm also trying to code this feature myself since I can't neither find anything relevant to it in the docs nor find libraries for the feature.

I have a bunch of Buttons in the VerticalGroup. Using keyboard UP and DOWN keys, I want to somehow focus, highlight or hover buttons like the way that mouse hovering do. Hovewer, firing FocusEvent on children of the VerticalGroup do not change Button appearance.

So how do I focus these buttons or any other UI element (because there are other menus with more complex UI architecture) ?

Here is the code for the menu (Kotlin, using ktx-scene2d builders and ktx-actors event methods):

class MainMenuUi: Stage() {
    val root = verticalGroup {
        center()
        setFillParent(true)
        expand(true)
        space(spacing)
        fill(0.5f)
        wrap(false)

        textButton("Start")
        textButton("Settings")
        textButton("Exit")

        onKeyUp { key ->
            when(key) {
                Input.Keys.UP -> { /* ??? */ }
                Input.Keys.DOWN -> { /* ??? */ }
            }
        }
    }.also {
        addActor(it)
        keyboardFocus = it
    }
}
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

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)
```
\$\endgroup\$
1
  • \$\begingroup\$ I just realized today that I could have done this more simply with a com.badlogic.gdx.InputMultiplexer. \$\endgroup\$
    – myyk
    Oct 13, 2023 at 8:42
0
\$\begingroup\$

I looked up in the library source code how do Scene2d Actors understand that they are hovered, and it seems to work this way:

  • Button: calling internal ClickListener.isOver(), which, in fact, checking over or pressed private members. Member over is setted when there is an InputEvent with enter type (over is true) or exit type (over is false) and pointer == -1;

  • SelectBox: exact same way that Button does. Except SelectBox itself doesn't have isOver method.

  • Slider: implementing hovering itself, using private mouseOver member; It is adding to itself InputListener that's observe InputEvent hovering the same way that ClickListener does, but change the mouseOver member instead of over.

Other UI elements either inherit from the Button (e.g. CheckBox), or don't have any hovering detection ability (and so they can't change the appearance when they are hovered).

Knowing that, I can implement HoverEvent that can trigger hovering in that limited list of UI elements:

class HoverEvent: InputEvent() {
    init {
        type = Type.enter
        pointer = -1
    }
}

It is change the appearance of the elements, however to un-hover such element I must manually move mouse to it and then outside of it. Or write some UnhoverEvent. This approach, though it seems to me the most correct one, in the end turns out to be inconvenient and in some way impractical.


So, as a workaround, I wrote extension function to the Actor that move cursor position to Actor's center:

fun Actor.moveCursorToIt() {
    val stageCoords = localToStageCoordinates(Vector2())
    val screenCoords = stage.stageToScreenCoordinates(stageCoords)
    Gdx.input.setCursorPosition(
            (screenCoords.x + width / 2).toInt(),
            (screenCoords.y - height / 2).toInt()
    )
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .