added virtual joystick and buttons for games that didn't have controls

This commit is contained in:
izzy2lost
2025-12-25 00:53:41 -05:00
parent 62985edf4e
commit 2c018ea82f
7 changed files with 352 additions and 5 deletions
@@ -165,6 +165,16 @@ void AndroidInputSystem::ApplyConfig(const Util::Config::Node& config)
m_touchShift3.a = keySc(get("InputGearShift3", "KEY_9"), SDL_SCANCODE_9);
m_touchShift4.a = keySc(get("InputGearShift4", "KEY_0"), SDL_SCANCODE_0);
m_touchShiftN.a = keySc(get("InputGearShiftN", "KEY_6"), SDL_SCANCODE_6);
m_touchPunch.a = keySc(get("InputPunch", "KEY_A"), SDL_SCANCODE_A);
m_touchKick.a = keySc(get("InputKick", "KEY_S"), SDL_SCANCODE_S);
m_touchGuard.a = keySc(get("InputGuard", "KEY_D"), SDL_SCANCODE_D);
m_touchEscape.a = keySc(get("InputEscape", "KEY_F"), SDL_SCANCODE_F);
m_touchSpikeShift.a = keySc(get("InputShift", "KEY_A"), SDL_SCANCODE_A);
m_touchSpikeBeat.a = keySc(get("InputBeat", "KEY_S"), SDL_SCANCODE_S);
m_touchSpikeCharge.a = keySc(get("InputCharge", "KEY_D"), SDL_SCANCODE_D);
m_touchSpikeJump.a = keySc(get("InputJump", "KEY_F"), SDL_SCANCODE_F);
}
bool AndroidInputSystem::InitializeSystem()
@@ -319,6 +329,88 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down)
const float x = tf.x;
const float y = tf.y;
// Fighting game action buttons (fingerId encoded from Java).
switch (tf.fingerId)
{
case 1110: SetKeys(m_touchPunch, down); return;
case 1111: SetKeys(m_touchKick, down); return;
case 1112: SetKeys(m_touchGuard, down); return;
case 1113: SetKeys(m_touchEscape, down); return;
case 1115: SetKeys(m_touchSpikeShift, down); return;
case 1116: SetKeys(m_touchSpikeBeat, down); return;
case 1117: SetKeys(m_touchSpikeCharge, down); return;
case 1118: SetKeys(m_touchSpikeJump, down); return;
default: break;
}
// Virtual fighting stick: 8-way directional based on encoded x/y in [0..1], keyed by a fixed fingerId.
if (tf.fingerId == 1114)
{
auto releaseHeld = [&]() {
auto it = m_fingerHeldDir.find(tf.fingerId);
if (it != m_fingerHeldDir.end())
{
SetKeys(it->second.primary, false);
SetKeys(it->second.secondary, false);
m_fingerHeldDir.erase(it);
}
};
if (!down)
{
releaseHeld();
return;
}
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);
constexpr float deadzone = 0.25f;
if (std::abs(sx) < deadzone && std::abs(sy) < deadzone)
{
releaseHeld();
return;
}
constexpr float kPi = 3.14159265358979323846f;
float angle = std::atan2(-sy, sx); // y is down in normalized coords; invert for math-y up.
if (angle < 0.0f)
angle += 2.0f * kPi;
const int oct = (int)std::floor((angle + (kPi / 8.0f)) / (kPi / 4.0f)) & 7;
DualScancode h{};
DualScancode v{};
switch (oct)
{
case 0: h = m_touchJoyRight; break;
case 1: h = m_touchJoyRight; v = m_touchJoyUp; break;
case 2: v = m_touchJoyUp; break;
case 3: h = m_touchJoyLeft; v = m_touchJoyUp; break;
case 4: h = m_touchJoyLeft; break;
case 5: h = m_touchJoyLeft; v = m_touchJoyDown; break;
case 6: v = m_touchJoyDown; break;
case 7: h = m_touchJoyRight; v = m_touchJoyDown; break;
default: break;
}
releaseHeld();
HeldDirKeys held{};
if (h.a != SDL_SCANCODE_UNKNOWN || h.b != SDL_SCANCODE_UNKNOWN)
held.primary = h;
if (v.a != SDL_SCANCODE_UNKNOWN || v.b != SDL_SCANCODE_UNKNOWN)
{
if (held.primary.a == SDL_SCANCODE_UNKNOWN && held.primary.b == SDL_SCANCODE_UNKNOWN)
held.primary = v;
else
held.secondary = v;
}
if (held.primary.a != SDL_SCANCODE_UNKNOWN || held.primary.b != SDL_SCANCODE_UNKNOWN)
SetKeys(held.primary, true);
if (held.secondary.a != SDL_SCANCODE_UNKNOWN || held.secondary.b != SDL_SCANCODE_UNKNOWN)
SetKeys(held.secondary, true);
m_fingerHeldDir[tf.fingerId] = held;
return;
}
// Virtual manual shifter (racing games): encoded in tf.x/tf.y from Java and keyed by a fixed fingerId.
if ((m_virtualShifterShift4 || m_virtualShifterUpDown) && tf.fingerId == 1108)
{
@@ -487,7 +579,8 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down)
auto it = m_fingerHeldDir.find(tf.fingerId);
if (it != m_fingerHeldDir.end())
{
SetKeys(it->second, false);
SetKeys(it->second.primary, false);
SetKeys(it->second.secondary, false);
m_fingerHeldDir.erase(it);
}
return;
@@ -507,13 +600,21 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down)
if (dir.a != SDL_SCANCODE_UNKNOWN || dir.b != SDL_SCANCODE_UNKNOWN)
{
m_fingerHeldDir[tf.fingerId] = dir;
SetKeys(dir, true);
HeldDirKeys held{};
held.primary = dir;
m_fingerHeldDir[tf.fingerId] = held;
SetKeys(held.primary, true);
}
}
void AndroidInputSystem::HandleTouchMotion(const SDL_TouchFingerEvent& tf)
{
if (tf.fingerId == 1114)
{
HandleTouch(tf, true);
return;
}
if (m_virtualShifterShift4 && tf.fingerId == 1108)
{
// Avoid accidental neutral when passing through center during motion.
@@ -582,7 +683,8 @@ void AndroidInputSystem::HandleTouchMotion(const SDL_TouchFingerEvent& tf)
}
// Release previous direction and compute a new one.
SetKeys(it->second, false);
SetKeys(it->second.primary, false);
SetKeys(it->second.secondary, false);
m_fingerHeldDir.erase(it);
HandleTouch(tf, true);
}
@@ -73,6 +73,11 @@ private:
SDL_Scancode b = SDL_SCANCODE_UNKNOWN;
};
struct HeldDirKeys {
DualScancode primary{};
DualScancode secondary{};
};
void RefreshControllers();
void CloseControllers();
@@ -126,6 +131,16 @@ private:
DualScancode m_touchShift4{SDL_SCANCODE_0, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchShiftN{SDL_SCANCODE_6, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchPunch{SDL_SCANCODE_A, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchKick{SDL_SCANCODE_S, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchGuard{SDL_SCANCODE_D, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchEscape{SDL_SCANCODE_F, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchSpikeShift{SDL_SCANCODE_A, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchSpikeBeat{SDL_SCANCODE_S, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchSpikeCharge{SDL_SCANCODE_D, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchSpikeJump{SDL_SCANCODE_F, SDL_SCANCODE_UNKNOWN};
bool m_gunTouchEnabled = false;
SDL_FingerID m_gunFinger = 0;
bool m_gunFingerActive = false;
@@ -152,7 +167,7 @@ private:
std::vector<ControllerState> m_controllers;
std::vector<uint8_t> m_keys; // indexed by SDL_Scancode
std::unordered_map<SDL_FingerID, DualScancode> m_fingerHeldDir;
std::unordered_map<SDL_FingerID, HeldDirKeys> m_fingerHeldDir;
std::unordered_map<SDL_FingerID, DualScancode> m_fingerHeldKey;
std::unordered_map<SDL_Scancode, uint32_t> m_pulseUntilMs;
};
+8
View File
@@ -267,6 +267,14 @@ struct Super3Host {
ensureKeyboardFallback("InputSteeringRight", "KEY_RIGHT");
ensureKeyboardFallback("InputAccelerator", "KEY_W");
ensureKeyboardFallback("InputBrake", "KEY_S");
ensureKeyboardFallback("InputPunch", "KEY_A");
ensureKeyboardFallback("InputKick", "KEY_S");
ensureKeyboardFallback("InputGuard", "KEY_D");
ensureKeyboardFallback("InputEscape", "KEY_F");
ensureKeyboardFallback("InputShift", "KEY_A");
ensureKeyboardFallback("InputBeat", "KEY_S");
ensureKeyboardFallback("InputCharge", "KEY_D");
ensureKeyboardFallback("InputJump", "KEY_F");
inputSystem.ApplyConfig(config);
}
@@ -13,6 +13,7 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.RelativeLayout
@@ -139,6 +140,16 @@ class Super3Activity : SDLActivity() {
gamesXml.isNotBlank() &&
GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("gun1", "gun2", "analog_gun1", "analog_gun2"))
val isFighting =
game.isNotBlank() &&
gamesXml.isNotBlank() &&
GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("fighting"))
val isSpikeout =
game.isNotBlank() &&
gamesXml.isNotBlank() &&
GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("spikeout"))
val shifterEnabled = getSharedPreferences("super3_prefs", MODE_PRIVATE)
.getBoolean("overlay_shifter_enabled", false)
val showShifter = shifterEnabled && (hasShift4 || hasShiftUpDown)
@@ -155,6 +166,12 @@ class Super3Activity : SDLActivity() {
overlay.findViewById<MaterialButton>(R.id.overlay_reload)?.visibility =
if (isGunGame) View.VISIBLE else View.GONE
overlay.findViewById<View>(R.id.overlay_fight_stick)?.visibility =
if (isFighting || isSpikeout) View.VISIBLE else View.GONE
overlay.findViewById<View>(R.id.overlay_fight_buttons)?.visibility =
if (isFighting || isSpikeout) View.VISIBLE else View.GONE
fun nativeTouch(action: Int, fingerId: Int, x: Float, y: Float, p: Float = 1.0f) {
SDLActivity.onNativeTouch(0, fingerId, action, x, y, p)
}
@@ -216,6 +233,67 @@ class Super3Activity : SDLActivity() {
bindMomentary(R.id.overlay_reload, fingerId = 1109, x = 0.90f, y = 0.90f)
}
if (isFighting || isSpikeout) {
val baseId =
if (isSpikeout) {
1115
} else {
1110
}
bindHeld(R.id.overlay_fight_punch, fingerId = baseId + 0, x = 0.90f, y = 0.55f)
bindHeld(R.id.overlay_fight_kick, fingerId = baseId + 1, x = 0.82f, y = 0.65f)
bindHeld(R.id.overlay_fight_guard, fingerId = baseId + 2, x = 0.90f, y = 0.45f)
bindHeld(R.id.overlay_fight_escape, fingerId = baseId + 3, x = 0.82f, y = 0.35f)
val stick = overlay.findViewById<FrameLayout>(R.id.overlay_fight_stick)
val knob = overlay.findViewById<View>(R.id.overlay_fight_stick_knob)
stick?.setOnTouchListener { v, ev ->
val w = v.width.toFloat().coerceAtLeast(1f)
val h = v.height.toFloat().coerceAtLeast(1f)
val cx = w / 2f
val cy = h / 2f
val dx = ((ev.x - cx) / cx).coerceIn(-1f, 1f)
val dy = ((ev.y - cy) / cy).coerceIn(-1f, 1f)
val radiusPx = (w.coerceAtMost(h) * 0.32f).coerceAtLeast(1f)
knob?.translationX = dx * radiusPx
knob?.translationY = dy * radiusPx
val encodedX = ((dx + 1f) / 2f).coerceIn(0f, 1f)
val encodedY = ((dy + 1f) / 2f).coerceIn(0f, 1f)
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
v.alpha = 0.90f
nativeTouch(MotionEvent.ACTION_DOWN, 1114, encodedX, encodedY)
true
}
MotionEvent.ACTION_MOVE -> {
nativeTouch(MotionEvent.ACTION_MOVE, 1114, encodedX, encodedY)
true
}
MotionEvent.ACTION_UP -> {
v.alpha = 1.0f
knob?.translationX = 0f
knob?.translationY = 0f
nativeTouch(MotionEvent.ACTION_UP, 1114, 0.5f, 0.5f)
true
}
MotionEvent.ACTION_CANCEL -> {
v.alpha = 1.0f
knob?.translationX = 0f
knob?.translationY = 0f
nativeTouch(MotionEvent.ACTION_UP, 1114, 0.5f, 0.5f)
true
}
else -> true
}
}
} else {
overlay.findViewById<View>(R.id.overlay_fight_stick)?.setOnTouchListener(null)
}
if (isRacing) {
// Match the native pedal zone (right-middle), independent of UI placement.
bindHeld(R.id.overlay_gas, fingerId = 1103, x = 0.85f, y = 0.35f)
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#22000000" />
<stroke android:width="2dp" android:color="@color/overlay_ripple" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#33FFFFFF" />
<stroke android:width="2dp" android:color="#66FFFFFF" />
</shape>
@@ -197,4 +197,138 @@
android:src="@drawable/gaspedal" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/overlay_fight_stick"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="bottom|start"
android:layout_marginStart="32dp"
android:layout_marginBottom="104dp"
android:visibility="gone">
<View
android:id="@+id/overlay_fight_stick_base"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/overlay_joystick_base" />
<View
android:id="@+id/overlay_fight_stick_knob"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@drawable/overlay_joystick_knob" />
</FrameLayout>
<LinearLayout
android:id="@+id/overlay_fight_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="18dp"
android:layout_marginBottom="64dp"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_fight_escape"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="62dp"
android:layout_height="62dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="E"
android:textAllCaps="true"
android:textSize="16sp"
app:cornerRadius="31dp"
app:rippleColor="@color/overlay_ripple"
app:strokeWidth="2dp" />
<Space
android:layout_width="10dp"
android:layout_height="match_parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_fight_guard"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="62dp"
android:layout_height="62dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="G"
android:textAllCaps="true"
android:textSize="16sp"
app:cornerRadius="31dp"
app:rippleColor="@color/overlay_ripple"
app:strokeWidth="2dp" />
</LinearLayout>
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_fight_kick"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="62dp"
android:layout_height="62dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="K"
android:textAllCaps="true"
android:textSize="16sp"
app:cornerRadius="31dp"
app:rippleColor="@color/overlay_ripple"
app:strokeWidth="2dp" />
<Space
android:layout_width="10dp"
android:layout_height="match_parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_fight_punch"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="62dp"
android:layout_height="62dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="P"
android:textAllCaps="true"
android:textSize="16sp"
app:cornerRadius="31dp"
app:rippleColor="@color/overlay_ripple"
app:strokeWidth="2dp" />
</LinearLayout>
</LinearLayout>
</FrameLayout>