Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gamepad support to MacOS on the JVM #1050

Merged
merged 5 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions korge-sandbox/src/commonMain/kotlin/samples/MainInput.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
package samples

import com.soywiz.klock.DateFormat
import com.soywiz.klock.DateTime
import com.soywiz.korge.component.onStageResized
import com.soywiz.korge.input.gamepad
import com.soywiz.korge.input.keys
import com.soywiz.korge.input.mouse
import com.soywiz.korge.scene.ScaledScene
import com.soywiz.korge.scene.Scene
import com.soywiz.korge.view.SContainer
import com.soywiz.korge.view.position
import com.soywiz.korge.view.textOld
import com.soywiz.klock.*
import com.soywiz.korge.component.*
import com.soywiz.korge.input.*
import com.soywiz.korge.scene.*
import com.soywiz.korge.view.*

class MainInput : ScaledScene(1920, 1080) {
override suspend fun SContainer.sceneMain() {
Expand All @@ -30,6 +24,7 @@ class MainInput : ScaledScene(1920, 1080) {
val mouseScrollText = textLine("MouseScroll")
val resizeText = textLine("Resize")
val gamepadConnectedText = textLine("GamepadConnectedEv")
val gamepadText = textLine("GamepadTextEv")
val gamepadButtonText = textLine("GamepadButtonEv")
val gamepadStickText = textLine("GamepadStickEv")
val gamepadUpdateText = textLine("GamepadUpdateEv")
Expand All @@ -39,6 +34,10 @@ class MainInput : ScaledScene(1920, 1080) {
resizeText.text = "Resize ${nowTime()} $width,$height"
}

gamepad {
updatedGamepad.invoke { gamepadText.text = "$it" }
}

gamepad {
button.invoke { gamepadButtonText.text = "$it" }
stick.invoke { gamepadStickText.text = "$it" }
Expand Down
33 changes: 16 additions & 17 deletions korge/src/commonMain/kotlin/com/soywiz/korge/input/GamepadEvents.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
package com.soywiz.korge.input

import com.soywiz.kds.Extra
import com.soywiz.kds.iterators.fastForEach
import com.soywiz.korev.GameButton
import com.soywiz.korev.GamePadButtonEvent
import com.soywiz.korev.GamePadConnectionEvent
import com.soywiz.korev.GamePadStickEvent
import com.soywiz.korev.GamePadUpdateEvent
import com.soywiz.korev.GameStick
import com.soywiz.korev.GamepadInfo
import com.soywiz.korge.component.GamepadComponent
import com.soywiz.korge.view.View
import com.soywiz.korge.view.Views
import com.soywiz.korio.async.Signal
import com.soywiz.korio.async.launchImmediately
import kotlin.native.concurrent.ThreadLocal
import com.soywiz.kds.*
import com.soywiz.kds.iterators.*
import com.soywiz.korev.*
import com.soywiz.korge.component.*
import com.soywiz.korge.view.*
import com.soywiz.korio.async.*
import kotlin.native.concurrent.*

class GamePadEvents(override val view: View) : GamepadComponent {
@PublishedApi
Expand Down Expand Up @@ -88,12 +80,15 @@ class GamePadEvents(override val view: View) : GamepadComponent {
override fun onGamepadEvent(views: Views, event: GamePadUpdateEvent) {
this.views = views
gamepads.copyFrom(event)
var gamepadsUpdated = false
// Compute diff
for (gamepadIndex in 0 until event.gamepadsLength) {
val gamepad = event.gamepads[gamepadIndex]
val oldGamepad = this.oldGamepads.gamepads[gamepadIndex]
var updateCount = 0
GameButton.BUTTONS.fastForEach { button ->
if (gamepad[button] != oldGamepad[button]) {
updateCount++
button(buttonEvent.apply {
this.gamepad = gamepad.index
this.type = if (gamepad[button] != 0.0) GamePadButtonEvent.Type.DOWN else GamePadButtonEvent.Type.UP
Expand All @@ -105,6 +100,7 @@ class GamePadEvents(override val view: View) : GamepadComponent {
GameStick.STICKS.fastForEach { stick ->
val vector = gamepad[stick]
if (vector != oldGamepad[stick]) {
updateCount++
stick(stickEvent.apply {
this.gamepad = gamepad.index
this.stick = stick
Expand All @@ -113,10 +109,13 @@ class GamePadEvents(override val view: View) : GamepadComponent {
})
}
}
updatedGamepad(gamepad)
if (updateCount > 0) {
updatedGamepad(gamepad)
gamepadsUpdated = true
}
}
oldGamepads.copyFrom(event)
updated(event)
if (gamepadsUpdated) updated(event)
}

override fun onGamepadEvent(views: Views, event: GamePadConnectionEvent) {
Expand Down
10 changes: 5 additions & 5 deletions korgw/src/commonMain/kotlin/com/soywiz/korev/Input.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.soywiz.korev

import com.soywiz.kmem.arraycopy
import com.soywiz.kmem.extract
import com.soywiz.korma.geom.Point
import kotlin.math.min
import com.soywiz.kmem.*
import com.soywiz.korio.util.*
import com.soywiz.korma.geom.*
import kotlin.math.*

enum class MouseButton(val id: Int, val bits: Int = 1 shl id) {
LEFT(0), MIDDLE(1), RIGHT(2), BUTTON3(3),
Expand Down Expand Up @@ -291,7 +291,7 @@ abstract class GamepadMapping {
}

fun toString(info: GamepadInfo) = "$id(" + GameButton.values().joinToString(", ") {
"${it.name}=${get(it, info)}"
"${it.name}=${get(it, info).niceStr(2)}"
} + ")"
}

Expand Down
25 changes: 23 additions & 2 deletions korgw/src/jvmMain/kotlin/com/soywiz/korgw/osx/Cocoa.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.soywiz.korgw.osx
import com.sun.jna.*
import java.util.concurrent.*

//inline class ID(val id: Long)
typealias ID = Long
Expand Down Expand Up @@ -51,6 +52,8 @@ interface ObjectiveC : Library {
@NativeName("objc_msgSend")
fun objc_msgSendCGFloat(vararg args: Any?): CGFloat
@NativeName("objc_msgSend")
fun objc_msgSendFloat(vararg args: Any?): Float
@NativeName("objc_msgSend")
fun objc_msgSendNSPoint(vararg args: Any?): NSPointRes
@NativeName("objc_msgSend")
fun objc_msgSendNSRect(vararg args: Any?): NSRectRes
Expand Down Expand Up @@ -153,13 +156,31 @@ typealias NSRectRes = MyNativeNSRect.ByValue
private val isArm64 = System.getProperty("os.arch") == "aarch64"

// @TODO: Move Long to ObjcRef to not pollute Long scope
inline class ObjcRef(val id: Long) {
open class ObjcRef(val id: Long) {
}

fun sel(name: String) = ObjectiveC.sel_registerName(name)
inline class ObjcSel(val id: Long) {
companion object {
private val selectors = ConcurrentHashMap<String, ObjcSel>()

operator fun invoke(name: String): ObjcSel =
selectors.getOrPut(name) { ObjcSel(ObjectiveC.sel_registerName(name)) }
}
}

fun sel(name: String): Long = ObjectiveC.sel_registerName(name)
fun sel(name: ObjcSel): Long = name.id
fun Long.msgSend(sel: ObjcSel, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args)
fun Long.msgSend(sel: String, vararg args: Any?): Long = ObjectiveC.objc_msgSend(this, sel(sel), *args)
fun Long.msgSendInt(sel: ObjcSel, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(this, sel(sel), *args)
fun Long.msgSendInt(sel: String, vararg args: Any?): Int = ObjectiveC.objc_msgSendInt(this, sel(sel), *args)

fun Long.msgSendFloat(sel: ObjcSel, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(this, sel(sel), *args)
fun Long.msgSendFloat(sel: String, vararg args: Any?): Float = ObjectiveC.objc_msgSendFloat(this, sel(sel), *args)

fun Long.msgSendCGFloat(sel: ObjcSel, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(this, sel(sel), *args)
fun Long.msgSendCGFloat(sel: String, vararg args: Any?): CGFloat = ObjectiveC.objc_msgSendCGFloat(this, sel(sel), *args)

fun Long.msgSendNSPoint(sel: String, vararg args: Any?): NSPointRes = ObjectiveC.objc_msgSendNSPoint(this, sel(sel), *args)
fun Long.msgSendNSRect(sel: String, vararg args: Any?): NSRectRes {
if (isArm64) {
Expand Down
189 changes: 186 additions & 3 deletions korgw/src/jvmMain/kotlin/com/soywiz/korgw/osx/MacosGameController.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.soywiz.korgw.osx

import com.soywiz.kmem.*
import com.soywiz.korev.*
import com.soywiz.korio.annotations.*
import com.soywiz.korio.util.*
import com.soywiz.korma.geom.*
import com.sun.jna.*
import kotlin.reflect.*

//fun main() {
//}
Expand All @@ -10,18 +15,196 @@ interface FrameworkInt : Library {

}

inline class GCControllerButtonInput(val id: Long) {
val analog: Boolean get() = id.msgSendInt(sel_isAnalog) != 0
val touched: Boolean get() = id.msgSendInt(sel_isTouched) != 0
val pressed: Boolean get() = id.msgSendInt(sel_isPressed) != 0
val value: Double get() = id.msgSendFloat(sel_value).toDouble()
val sfSymbolsName: String get() = NSString(id.msgSend("sfSymbolsName")).toString()
val localizedName: String get() = NSString(id.msgSend("localizedName")).toString()
val unmappedLocalizedName: String get() = NSString(id.msgSend("unmappedLocalizedName")).toString()

override fun toString(): String = "GCControllerButtonInput($localizedName, $touched, $pressed, $value)"
val nice: String get() = value.niceStr(2)

companion object {
val sel_isAnalog = ObjcSel("isAnalog")
val sel_isTouched = ObjcSel("isTouched")
val sel_isPressed = ObjcSel("isPressed")
val sel_value = ObjcSel("value")

inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCControllerButtonInput = GCControllerButtonInput(obj.id.msgSend(property.name))
}
}

class GCControllerAxisInput(id: Long) : ObjcRef(id) {
val value: Double get() = id.msgSendFloat("value").toDouble()
companion object {
inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCControllerAxisInput = GCControllerAxisInput(obj.id.msgSend(property.name))
}

override fun toString(): String = value.niceStr(2)
}

class GCControllerDirectionPad(id: Long) : ObjcRef(id) {
@Keep val right by GCControllerButtonInput
@Keep val left by GCControllerButtonInput
@Keep val up by GCControllerButtonInput
@Keep val down by GCControllerButtonInput

@Keep val xAxis by GCControllerAxisInput
@Keep val yAxis by GCControllerAxisInput

val x: Double get() = xAxis.value
val y: Double get() = yAxis.value

private val _point: Point = Point()
val point: IPoint get() = _point.setTo(x, y)

companion object {
inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCControllerDirectionPad = GCControllerDirectionPad(obj.id.msgSend(property.name))
}

override fun toString(): String = "DPad(${up.nice}, ${right.nice}, ${down.nice}, ${left.nice})"
}

class GCExtendedGamepad(id: Long) : ObjcRef(id) {
@Keep val leftShoulder by GCControllerButtonInput
@Keep val rightShoulder by GCControllerButtonInput
@Keep val leftTrigger by GCControllerButtonInput
@Keep val rightTrigger by GCControllerButtonInput
@Keep val buttonMenu by GCControllerButtonInput
@Keep val buttonOptions by GCControllerButtonInput
@Keep val buttonHome by GCControllerButtonInput
@Keep val buttonA by GCControllerButtonInput
@Keep val buttonB by GCControllerButtonInput
@Keep val buttonX by GCControllerButtonInput
@Keep val buttonY by GCControllerButtonInput
@Keep val dpad by GCControllerDirectionPad

@Keep val leftThumbstick by GCControllerDirectionPad
@Keep val rightThumbstick by GCControllerDirectionPad

@Keep val leftThumbstickButton by GCControllerButtonInput
@Keep val rightThumbstickButton by GCControllerButtonInput


companion object {
inline operator fun getValue(obj: ObjcRef, property: KProperty<*>): GCExtendedGamepad = GCExtendedGamepad(obj.id.msgSend(property.name))
}

override fun toString(): String = "GCExtendedGamepad(dpad=$dpad, LR=[${leftThumbstick.point.niceStr(2)}, ${rightThumbstick.point.niceStr(2)}] [A=${buttonA.nice}, B=${buttonB.nice}, X=${buttonX.nice}, Y=${buttonY.nice}], L=[${leftShoulder.nice}, ${leftTrigger.nice}, ${leftThumbstickButton.nice}], R=[${rightShoulder.nice}, ${rightTrigger.nice}, ${rightThumbstickButton.nice}], SYS=[${buttonMenu.nice}, ${buttonOptions.nice}, ${buttonHome.nice}])"
}

class GCController(id: Long) : ObjcRef(id) {
val extendedGamepad: GCExtendedGamepad by GCExtendedGamepad

companion object {
fun controllers(): NSArray = NSArray(NSClass("GCController").msgSend("controllers"))
}
}

class NSArray(val id: Long) : AbstractList<Long>() {
val count: Int get() = id.msgSendInt("count")
override val size: Int get() = count
override operator fun get(index: Int): Long = id.msgSend("objectAtIndex:", index)
override fun toString(): String = "NSArray(${toList()})"
}

class MacosGameController {
companion object {
@JvmStatic
fun main(args: Array<String>) {
val lib = Native.load("/System/Library/Frameworks/GameController.framework/Versions/A/GameController", FrameworkInt::class.java)
println(NSClass("GCController").msgSend("controllers"))
val gamepad = MacosGamepadEventAdapter()
val events = EventDispatcher.Mixin()
events.addEventListener<GamePadUpdateEvent> { print("$it\r") }
events.addEventListener<GamePadConnectionEvent> { println(it) }
while (true) {
gamepad.updateGamepads(events)
Thread.sleep(10L)
}
}
}
}

internal class MacosGamepadEventAdapter {
val lib by lazy { Native.load("/System/Library/Frameworks/GameController.framework/Versions/A/GameController", FrameworkInt::class.java) }

private val gamepadsConnected = BooleanArray(MAX_GAMEPADS)
private val gamePadUpdateEvent = GamePadUpdateEvent()
private val gamePadConnectionEvent = GamePadConnectionEvent()

fun updateGamepads(dispatcher: EventDispatcher) {
// @TODO:
try {
lib
val controllers = GCController.controllers().toList().map { GCController(it) }
var connectedCount = 0

for (n in 0 until MAX_GAMEPADS) {
val ctrl = controllers.getOrNull(n)?.extendedGamepad
val prevConnected = gamepadsConnected[n]
val connected = ctrl != null
val gamepad = gamePadUpdateEvent.gamepads[n]

gamepad.connected = connected
if (connected && ctrl != null) {
gamepad.mapping = StandardGamepadMapping
//println("ctrl=$ctrl")

gamepad.rawButtonsPressed = 0
.insert(ctrl.dpad.left.pressed, GameButton.LEFT.index)
.insert(ctrl.dpad.right.pressed, GameButton.RIGHT.index)
.insert(ctrl.dpad.up.pressed, GameButton.UP.index)
.insert(ctrl.dpad.down.pressed, GameButton.DOWN.index)
.insert(ctrl.buttonA.pressed, GameButton.XBOX_A.index)
.insert(ctrl.buttonB.pressed, GameButton.XBOX_B.index)
.insert(ctrl.buttonX.pressed, GameButton.XBOX_X.index)
.insert(ctrl.buttonY.pressed, GameButton.XBOX_Y.index)
.insert(ctrl.buttonHome.pressed, GameButton.SYSTEM.index)
.insert(ctrl.buttonMenu.pressed, GameButton.START.index)
.insert(ctrl.buttonOptions.pressed, GameButton.SELECT.index)
.insert(ctrl.leftShoulder.pressed, GameButton.L1.index)
.insert(ctrl.rightShoulder.pressed, GameButton.R1.index)
.insert(ctrl.leftThumbstickButton.pressed, GameButton.L3.index)
.insert(ctrl.rightThumbstickButton.pressed, GameButton.R3.index)
gamepad.rawButtonsPressure[GameButton.L2.index] = ctrl.leftTrigger.value
gamepad.rawButtonsPressure[GameButton.R2.index] = ctrl.rightTrigger.value
gamepad.rawButtonsPressure[GameButton.L3.index] = ctrl.leftThumbstickButton.value
gamepad.rawButtonsPressure[GameButton.R3.index] = ctrl.rightThumbstickButton.value


gamepad.rawAxes[0] = ctrl.leftThumbstick.x
gamepad.rawAxes[1] = ctrl.leftThumbstick.y
gamepad.rawAxes[2] = ctrl.rightThumbstick.x
gamepad.rawAxes[3] = ctrl.rightThumbstick.y
connectedCount++
}
if (prevConnected != connected) {
if (connected && ctrl != null) {
gamepad.name = "Standard Gamepad"
}

//println("n=$n, prevConnected=$prevConnected, connected=$connected")

gamepadsConnected[n] = connected
dispatcher.dispatch(gamePadConnectionEvent.also {
it.gamepad = n
it.type = if (connected) GamePadConnectionEvent.Type.CONNECTED else GamePadConnectionEvent.Type.DISCONNECTED
})
}
}

gamePadUpdateEvent.gamepadsLength = connectedCount

if (connectedCount > 0) {
dispatcher.dispatch(gamePadUpdateEvent)
}
} catch (e: Throwable) {
e.printStackTrace()
}
}

companion object {
const val MAX_GAMEPADS = 4
}
}
Loading