mirror of
https://github.com/izzy2lost/Super3.git
synced 2026-07-05 15:18:38 -07:00
added gyro steering and sensitivity option
This commit is contained in:
@@ -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{};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user