added gyro steering and sensitivity option

This commit is contained in:
izzy2lost
2026-01-09 14:15:32 -05:00
parent 9a181d159c
commit d7b5b75b10
7 changed files with 220 additions and 11 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
minSdk = 26
targetSdk = 36
versionCode = 6
versionName = "1.0.5"
versionCode = 7
versionName = "1.0.6"
ndkVersion = "29.0.14206865"
@@ -56,7 +56,7 @@ void AndroidInputSystem::SetVirtualWheelEnabled(bool enabled)
m_virtualWheelEnabled = enabled;
m_wheelFingerActive = false;
m_wheelFinger = 0;
m_virtualJoyX = 0;
m_virtualJoyX.store(0, std::memory_order_relaxed);
m_virtualJoyY = 0;
if (enabled && !m_virtualAnalogGunEnabled)
{
@@ -86,7 +86,7 @@ void AndroidInputSystem::SetVirtualAnalogGunEnabled(bool enabled)
else
std::strncpy(m_virtualJoyDetails.name, "Touch Controls", MAX_NAME_LENGTH);
m_virtualJoyDetails.name[MAX_NAME_LENGTH] = '\0';
m_virtualJoyX = 0;
m_virtualJoyX.store(0, std::memory_order_relaxed);
m_virtualJoyY = 0;
}
@@ -103,24 +103,37 @@ bool AndroidInputSystem::UseVirtualJoystick() const
void AndroidInputSystem::SetVirtualSteerFromEncoded(float encodedX)
{
// encodedX comes from Java as [(steer+1)/2], where steer is in [-1,1].
const float steer = std::clamp((encodedX - 0.5f) * 2.0f, -1.0f, 1.0f);
SetVirtualSteerFromSteer((encodedX - 0.5f) * 2.0f);
}
void AndroidInputSystem::SetVirtualSteerFromSteer(float steer)
{
steer = std::clamp(steer, -1.0f, 1.0f);
constexpr float deadzone = 0.08f;
if (std::abs(steer) < deadzone)
{
m_virtualJoyX = 0;
m_virtualJoyX.store(0, std::memory_order_relaxed);
return;
}
const float scaled = (std::abs(steer) - deadzone) / (1.0f - deadzone);
const float signedScaled = (steer < 0.0f) ? -scaled : scaled;
m_virtualJoyX = (int)std::lround(std::clamp(signedScaled, -1.0f, 1.0f) * 32767.0f);
m_virtualJoyX.store(
(int)std::lround(std::clamp(signedScaled, -1.0f, 1.0f) * 32767.0f),
std::memory_order_relaxed
);
}
void AndroidInputSystem::SetGyroSteer(float steer)
{
SetVirtualSteerFromSteer(steer);
}
void AndroidInputSystem::SetVirtualJoyFromNormalized(float x, float y)
{
const float sx = std::clamp((x - 0.5f) * 2.0f, -1.0f, 1.0f);
const float sy = std::clamp((y - 0.5f) * 2.0f, -1.0f, 1.0f);
m_virtualJoyX = (int)std::lround(sx * 32767.0f);
m_virtualJoyX.store((int)std::lround(sx * 32767.0f), std::memory_order_relaxed);
m_virtualJoyY = (int)std::lround(sy * 32767.0f);
}
@@ -217,7 +230,7 @@ bool AndroidInputSystem::InitializeSystem()
m_gunFinger = 0;
m_wheelFingerActive = false;
m_wheelFinger = 0;
m_virtualJoyX = 0;
m_virtualJoyX.store(0, std::memory_order_relaxed);
m_virtualJoyY = 0;
m_lastVirtualGear = -1;
@@ -989,7 +1002,7 @@ int AndroidInputSystem::GetJoyAxisValue(int joyNum, int axisNum)
if (UseVirtualJoystick())
{
if (axisNum == AXIS_X)
return m_virtualJoyX;
return m_virtualJoyX.load(std::memory_order_relaxed);
if (axisNum == AXIS_Y && m_virtualJoyDetails.hasAxis[AXIS_Y])
return m_virtualJoyY;
return 0;
@@ -2,6 +2,7 @@
#include <SDL.h>
#include <atomic>
#include <cstdint>
#include <unordered_map>
#include <string>
@@ -24,6 +25,7 @@ public:
void SetVirtualWheelEnabled(bool enabled);
void SetVirtualShifterMode(bool shift4, bool shiftUpDown);
void SetVirtualAnalogGunEnabled(bool enabled);
void SetGyroSteer(float steer); // [-1..1]
// Called from the SDL event loop (main thread).
void HandleEvent(const SDL_Event& ev);
@@ -84,6 +86,7 @@ private:
bool UseVirtualWheel() const;
bool UseVirtualJoystick() const;
void SetVirtualSteerFromEncoded(float encodedX);
void SetVirtualSteerFromSteer(float steer);
void SetVirtualJoyFromNormalized(float x, float y);
void SetKey(SDL_Scancode sc, bool down);
@@ -172,7 +175,7 @@ private:
bool m_virtualWheelEnabled = false;
SDL_FingerID m_wheelFinger = 0;
bool m_wheelFingerActive = false;
int m_virtualJoyX = 0;
std::atomic<int> m_virtualJoyX{0};
int m_virtualJoyY = 0;
JoyDetails m_virtualJoyDetails{};
+10
View File
@@ -986,3 +986,13 @@ Java_com_izzy2lost_super3_Super3Activity_nativeGetLoadedGameName(JNIEnv* env, jo
return nullptr;
return env->NewStringUTF(g_host->game.name.c_str());
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_izzy2lost_super3_Super3Activity_nativeSetGyroSteer(JNIEnv*, jobject, jfloat steer)
{
if (!g_host)
return JNI_FALSE;
const float clamped = std::clamp((float)steer, -1.0f, 1.0f);
g_host->inputSystem.SetGyroSteer(clamped);
return JNI_TRUE;
}
@@ -169,6 +169,8 @@ class MainActivity : AppCompatActivity() {
btnEnhancedReal3d = headerView.findViewById(R.id.btn_enhanced_real3d)
val btnShowTouchControls: MaterialButton = headerView.findViewById(R.id.btn_show_touch_controls)
val btnShowShifterOverlay: MaterialButton = headerView.findViewById(R.id.btn_show_shifter_overlay)
val btnGyroSteering: MaterialButton = headerView.findViewById(R.id.btn_gyro_steering)
val btnGyroSensitivity: MaterialButton = headerView.findViewById(R.id.btn_gyro_sensitivity)
gamesAdapter = GamesAdapter { item ->
if (!item.launchable) {
@@ -217,6 +219,38 @@ class MainActivity : AppCompatActivity() {
).show()
}
btnGyroSteering.isChecked = prefs.getBoolean("gyro_steering_enabled", false)
btnGyroSteering.setOnClickListener {
val enabled = btnGyroSteering.isChecked
prefs.edit().putBoolean("gyro_steering_enabled", enabled).apply()
Toast.makeText(
this,
if (enabled) "Gyro steering enabled (restart game to apply)" else "Gyro steering disabled",
Toast.LENGTH_SHORT,
).show()
}
data class GyroPreset(val label: String, val scale: Float)
val gyroPresets = listOf(
GyroPreset("Less", 0.65f),
GyroPreset("Normal", 1.0f),
GyroPreset("More", 1.35f),
)
fun updateGyroSensitivityLabel() {
val cur = prefs.getFloat("gyro_steering_sensitivity", 1.0f)
val match = gyroPresets.minByOrNull { kotlin.math.abs(it.scale - cur) } ?: gyroPresets[1]
btnGyroSensitivity.text = "Gyro sensitivity: ${match.label}"
}
updateGyroSensitivityLabel()
btnGyroSensitivity.setOnClickListener {
val cur = prefs.getFloat("gyro_steering_sensitivity", 1.0f)
val idx = gyroPresets.indexOfFirst { kotlin.math.abs(it.scale - cur) < 0.01f }.let { if (it < 0) 1 else it }
val next = gyroPresets[(idx + 1) % gyroPresets.size]
prefs.edit().putFloat("gyro_steering_sensitivity", next.scale).apply()
updateGyroSensitivityLabel()
Toast.makeText(this, "Gyro sensitivity: ${next.label} (restart game to apply)", Toast.LENGTH_SHORT).show()
}
runCatching { searchView.setupWithSearchBar(searchBar) }
styleSearchBarText()
searchBar.setOnClickListener {
@@ -7,10 +7,15 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Build
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.view.PixelCopy
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@@ -40,6 +45,7 @@ class Super3Activity : SDLActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private var overlayView: View? = null
private var overlayControlsEnabled: Boolean = true
private var gyroSteeringEnabled: Boolean = false
private var menuPaused = false
private var saveDialogOpen = false
private var exitDialogOpen = false
@@ -49,6 +55,12 @@ class Super3Activity : SDLActivity() {
private var gameName: String = ""
private var userDataRoot: File? = null
private var gyroSensorManager: SensorManager? = null
private var gyroSensor: Sensor? = null
private var gyroListener: SensorEventListener? = null
private var gyroZeroRollRad: Float? = null
private var gyroFilteredSteer: Float = 0f
private data class SaveSlot(
val slotIndex: Int,
val title: String,
@@ -80,6 +92,7 @@ class Super3Activity : SDLActivity() {
super.onCreate(savedInstanceState)
overlayControlsEnabled = prefs.getBoolean("overlay_controls_enabled", true)
gyroSteeringEnabled = prefs.getBoolean("gyro_steering_enabled", false)
saveStateSlot = prefs.getInt("save_state_slot", 0).coerceIn(0, 9)
gameName = intent.getStringExtra("gameName").orEmpty()
val userDataRootPath = intent.getStringExtra("userDataRoot").orEmpty()
@@ -502,6 +515,116 @@ class Super3Activity : SDLActivity() {
}
}
private fun angleDiffRad(a: Float, b: Float): Float {
var d = a - b
val twoPi = (Math.PI * 2.0).toFloat()
val pi = Math.PI.toFloat()
while (d > pi) d -= twoPi
while (d < -pi) d += twoPi
return d
}
private fun startGyroSteering() {
if (!gyroSteeringEnabled) return
val game = gameName
val gamesXml = intent.getStringExtra("gamesXmlPath").orEmpty()
val isRacing =
game.isNotBlank() &&
gamesXml.isNotBlank() &&
GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("vehicle", "harley"))
if (!isRacing) return
val sensitivity = prefs.getFloat("gyro_steering_sensitivity", 1.0f).coerceIn(0.25f, 2.0f)
if (gyroSensorManager == null) {
gyroSensorManager = getSystemService(SENSOR_SERVICE) as? SensorManager
}
val mgr = gyroSensorManager ?: return
val sensor =
mgr.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
?: mgr.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
?: return
gyroSensor = sensor
gyroZeroRollRad = null
gyroFilteredSteer = 0f
val listener =
object : SensorEventListener {
private val rotationMatrix = FloatArray(9)
private val adjusted = FloatArray(9)
private val orientation = FloatArray(3)
private val maxRollRad = Math.toRadians(28.0).toFloat()
override fun onSensorChanged(event: SensorEvent) {
if (!gyroSteeringEnabled) return
if (menuPaused || userPaused) return
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
val rotation = display?.rotation ?: Surface.ROTATION_0
when (rotation) {
Surface.ROTATION_0 -> rotationMatrix.copyInto(adjusted)
Surface.ROTATION_90 ->
SensorManager.remapCoordinateSystem(
rotationMatrix,
SensorManager.AXIS_Y,
SensorManager.AXIS_MINUS_X,
adjusted,
)
Surface.ROTATION_180 ->
SensorManager.remapCoordinateSystem(
rotationMatrix,
SensorManager.AXIS_MINUS_X,
SensorManager.AXIS_MINUS_Y,
adjusted,
)
Surface.ROTATION_270 ->
SensorManager.remapCoordinateSystem(
rotationMatrix,
SensorManager.AXIS_MINUS_Y,
SensorManager.AXIS_X,
adjusted,
)
}
SensorManager.getOrientation(adjusted, orientation)
val roll = orientation[2]
val zero = gyroZeroRollRad
if (zero == null) {
gyroZeroRollRad = roll
nativeSetGyroSteer(0f)
return
}
val delta = angleDiffRad(roll, zero)
val raw = ((delta / maxRollRad) * sensitivity).coerceIn(-1f, 1f)
gyroFilteredSteer = (gyroFilteredSteer * 0.85f) + (raw * 0.15f)
nativeSetGyroSteer(gyroFilteredSteer)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
gyroListener = listener
mgr.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME)
}
private fun stopGyroSteering() {
val mgr = gyroSensorManager
val listener = gyroListener
if (mgr != null && listener != null) {
mgr.unregisterListener(listener)
}
gyroListener = null
gyroSensor = null
gyroZeroRollRad = null
gyroFilteredSteer = 0f
nativeSetGyroSteer(0f)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
if (event.action == KeyEvent.ACTION_UP) {
@@ -517,6 +640,7 @@ class Super3Activity : SDLActivity() {
}
override fun onDestroy() {
stopGyroSteering()
overlayView?.let { v ->
(v.parent as? ViewGroup)?.removeView(v)
}
@@ -527,6 +651,13 @@ class Super3Activity : SDLActivity() {
override fun onResume() {
super.onResume()
applyImmersiveMode()
gyroSteeringEnabled = prefs.getBoolean("gyro_steering_enabled", false)
startGyroSteering()
}
override fun onPause() {
stopGyroSteering()
super.onPause()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -758,6 +889,7 @@ class Super3Activity : SDLActivity() {
private external fun nativeRequestSaveState(slot: Int): Boolean
private external fun nativeRequestLoadState(slot: Int): Boolean
private external fun nativeGetLoadedGameName(): String?
private external fun nativeSetGyroSteer(steer: Float): Boolean
private class SaveSlotAdapter(
private val slots: List<SaveSlot>,
@@ -137,6 +137,23 @@
android:text="Show shifter overlay"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_gyro_steering"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checkable="true"
android:text="Gyro steering (tilt to steer)"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_gyro_sensitivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Gyro sensitivity: Normal"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/games_folder_text"
android:layout_width="match_parent"