/RadialGamePad

A customizable Android library for creating virtual gamepads

Primary LanguageKotlinGNU General Public License v3.0GPL-3.0

RadialGamePad

RadialGamePad is an Android library for creating gamepads overlays. It has been designed with customization in mind and it's currently powering all the layouts you see in Lemuroid.

Screen 1 Screen 2 Screen 3
Screen1 Screen2 Screen3

Getting started

RadialGamePad is distributed through jitpack, modify your build.gradle file to include this:

repositories {
    maven { url "https://jitpack.io" }
}

dependencies {
    implementation 'com.github.swordfish90:radialgamepad:$LAST_RELEASE'
    
    // To handle the Flow of events from RadialGamePad
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
}

Usage

As the name suggests, RadialGamePad is built around the idea circular dials. There is a primary dial in the center which contains a primary control such as Cross, Stick or PrimaryButtons, surrounded by optional secondary dials.

To define the layout a RadialGamePadConfig object is passed to the constructor, and the library will take care of sizing and positioning controls to optimize the available space.

Events are returned as a Kotlin Flow and are composed of three different types:

  • Direction: fired from Sticks and Crosses and indicates a direction through the two components xAxis and yAxis
  • Button: indicates when a control has been pressed or released (KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP)
  • Gesture: a higher level event such as tap, double tap or triple tap

Here's a simple example which creates a remote control pad and handles its events:

class MainActivity : Activity() {
    private lateinit var pad: RadialGamePad

    private val padConfig = RadialGamePadConfig(
        sockets = 6,
        primaryDial = PrimaryDialConfig.Cross(
            CrossConfig(
                id = 0,
                useDiagonals = false
            )
        ),
        secondaryDials = listOf(
            SecondaryDialConfig.SingleButton(
                index = 1,
                scale = 1f,
                distance = 0f,
                ButtonConfig(
                    id = KeyEvent.KEYCODE_BUTTON_SELECT,
                    iconId = R.drawable.ic_play
                )
            ),
            SecondaryDialConfig.SingleButton(
                index = 2,
                scale = 1f,
                distance = 0f,
                ButtonConfig(
                    id = KeyEvent.KEYCODE_BUTTON_L1,
                    iconId = R.drawable.ic_stop
                )
            ),
            SecondaryDialConfig.SingleButton(
                index = 4,
                scale = 1f,
                distance = 0f,
                ButtonConfig(
                    id = KeyEvent.KEYCODE_BUTTON_MODE,
                    iconId = R.drawable.ic_volume_down
                )
            ),
            SecondaryDialConfig.SingleButton(
                index = 5,
                scale = 1f,
                distance = 0f,
                ButtonConfig(
                    id = KeyEvent.KEYCODE_BUTTON_MODE,
                    iconId = R.drawable.ic_volume_up
                )
            )
        )
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.main_activity)

        // Create a pad with default theme and 8dp of margins
        pad = RadialGamePad(padConfig, 8f, requireContext())

        findViewById<FrameLayout>(R.id.container).addView(pad)

        // Collect the Flow of events to a handler
        lifecycleScope.launch {
            pad.events()
                .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
                .collect {
                    handleEvent(it)
                }
        }
    }

    private fun handleEvent(event: Event) {
        // Do something with the event.
        when (event) {
            is Event.Button -> {
                Log.d("Event", "Button event from control ${event.id}")
            }
            is Event.Gesture -> {
                Log.d("Event", "Gesture event from control ${event.id}")
            }
            is Event.Direction -> {
                Log.d("Event", "Direction event from control ${event.id}")
            }
        }
    }
}

Advanced usage

Check the included app sample. If you want to see even more layouts check Lemuroid.

Random tips

  • Do not put margins around RadialGamePad. If you need them use the margin variable, so that touch events will be still forwarded to the view
  • If your gamepad is split into multiple views (very likely for games), consider using SecondaryDialConfig.Empty to enforce symmetry.