Skip to content

Commit d64d1d1

Browse files
committed
feat(android): on-screen touch controls in the letterbox bars
A 640×480 canvas pillarboxes on a wide phone, wasting the side bars. Fill them with a semi-transparent virtual joystick (left → drives the cursor precisely) plus a big left-click and a smaller right-click button (right), for players who find tapping the small canvas imprecise. Direct tapping on the canvas still works (drag to aim, lift to click). - src/platform/android/touch_overlay.c (+ android_touch.h): geometry from the renderer output size, alpha-blended draw, finger routing, stick→cursor integration. All sizes/alpha/speed are tunable constants (feel needs on-device tuning). - video_sdl.c: draw the overlay after the game texture (toggles logical size off to paint into the bars), under #ifdef __ANDROID__. - platform_sdl.c: on Android disable SDL touch→mouse synthesis and route all SDL_FINGER* events to the overlay + tick the stick each frame; the desktop two-finger handlers are compiled out there. Inert when the bars are too narrow (≈4:3 screens) — falls back to direct tap only.
1 parent 89a17a8 commit d64d1d1

5 files changed

Lines changed: 364 additions & 1 deletion

File tree

android/app/jni/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ set(WACKI_SRCS
155155
${WACKI_ROOT}/src/platform/android/hooks_android.c
156156
${WACKI_ROOT}/src/platform/android/data_root_android.c
157157
${WACKI_ROOT}/src/platform/android/saf.c
158+
${WACKI_ROOT}/src/platform/android/touch_overlay.c
158159
)
159160

160161
# SDLActivity loads "SDL2" then "main" → this must be libmain.so.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* SPDX-License-Identifier: GPL-3.0-or-later
2+
* Copyright (C) 2026 Mateusz Szuła
3+
*
4+
* include/wacki/platform/android_touch.h — Android on-screen touch overlay.
5+
*
6+
* The engine renders a 640×480 (4:3) canvas; on a wide phone held landscape
7+
* that letterboxes to big black pillarbox bars left and right. This overlay
8+
* fills that wasted space with semi-transparent controls for players who find
9+
* tapping the tiny canvas imprecise:
10+
*
11+
* - left bar: a virtual joystick that drives the cursor (precise, analog)
12+
* - right bar: a big button = left click (walk / use), a smaller one = right
13+
* click (switch actor)
14+
*
15+
* Direct tapping on the game canvas still works (drag to aim, lift to click).
16+
* On Android we own ALL touch (SDL touch→mouse synthesis is disabled), so the
17+
* canvas tap path lives here too — it needs the letterbox geometry the draw
18+
* step caches anyway.
19+
*
20+
* Implementation: src/platform/android/touch_overlay.c. The shared SDL layer
21+
* calls these under #ifdef __ANDROID__: video_sdl.c draws, platform_sdl.c feeds
22+
* SDL_FINGER* events + ticks the stick each frame. */
23+
#ifndef WACKI_PLATFORM_ANDROID_TOUCH_H
24+
#define WACKI_PLATFORM_ANDROID_TOUCH_H
25+
26+
#include <SDL.h>
27+
28+
/* Draw the overlay (called after the game texture, before present). Also
29+
* (re)caches the renderer output size + control geometry for the event
30+
* handlers. No-op visual when the bars are too narrow to hold controls. */
31+
void wacki_overlay_draw(SDL_Renderer *ren);
32+
33+
/* SDL_FINGER* routing. Coordinates are SDL's window-normalized 0..1. Each
34+
* returns 1 when the touch was handled here (always 1 on Android — we own
35+
* touch). */
36+
void wacki_overlay_finger_down(SDL_FingerID id, float nx, float ny);
37+
void wacki_overlay_finger_motion(SDL_FingerID id, float nx, float ny);
38+
void wacki_overlay_finger_up(SDL_FingerID id, float nx, float ny);
39+
40+
/* Per-frame: integrate the virtual stick deflection into the cursor. */
41+
void wacki_overlay_tick(void);
42+
43+
#endif /* WACKI_PLATFORM_ANDROID_TOUCH_H */
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/* SPDX-License-Identifier: GPL-3.0-or-later
2+
* Copyright (C) 2026 Mateusz Szuła
3+
*
4+
* src/platform/android/touch_overlay.c — Android on-screen touch overlay
5+
* (see include/wacki/platform/android_touch.h).
6+
*
7+
* Fills the letterbox pillarbox bars with a virtual joystick (left, drives the
8+
* cursor) + left/right click buttons (right). Semi-transparent so it doesn't
9+
* fight the artwork. Owns all touch on Android: controls in the bars, plus
10+
* drag-to-aim / tap-to-click on the game canvas (with the letterbox→logical
11+
* coordinate conversion the draw step's cached geometry provides).
12+
*
13+
* All tunables (sizes, alpha, cursor speed, deadzone) are constants below —
14+
* touch feel wants on-device tuning. */
15+
16+
#include "wacki.h" /* g_mouse_x/y, g_lmb_clicked, g_rmb_clicked, WACKI_SCREEN_* */
17+
#include "wacki/platform/android_touch.h"
18+
19+
#include <SDL.h>
20+
#include <math.h>
21+
22+
/* ---- tunables ---------------------------------------------------- */
23+
#define BAR_MIN_PX 96 /* hide controls if a side bar is narrower */
24+
#define STICK_BAR_FRAC 0.42f /* stick radius vs bar width … */
25+
#define STICK_H_FRAC 0.17f /* … and vs output height (min wins) */
26+
#define STICK_CY_FRAC 0.60f /* stick vertical centre (thumb reach) */
27+
#define KNOB_FRAC 0.50f /* knob radius vs stick radius */
28+
#define LMB_BAR_FRAC 0.42f
29+
#define LMB_H_FRAC 0.16f
30+
#define LMB_CY_FRAC 0.62f
31+
#define RMB_FRAC 0.64f /* RMB radius vs LMB radius */
32+
#define STICK_DEADZONE 0.18f
33+
#define STICK_SPEED 7.0f /* cursor px/frame at full deflection */
34+
#define TAP_MS 350u /* canvas press↔release under this = a tap */
35+
#define TAP_SLOP_PX 18 /* … with movement under this */
36+
37+
/* overlay paint (RGBA, low alpha = subtle over the black bars) */
38+
#define A_STICK_BASE 40
39+
#define A_STICK_KNOB 95
40+
#define A_LMB 64
41+
#define A_RMB 48
42+
43+
/* ---- geometry (renderer output pixels), cached on draw ----------- */
44+
static int s_ow = 0, s_oh = 0;
45+
static int s_have_geom = 0;
46+
static float s_scale = 1.0f;
47+
static int s_gx0, s_gy0, s_gw, s_gh; /* game canvas rect */
48+
static int s_controls_on = 0;
49+
static int s_stick_cx, s_stick_cy, s_stick_r;
50+
static int s_lmb_cx, s_lmb_cy, s_lmb_r;
51+
static int s_rmb_cx, s_rmb_cy, s_rmb_r;
52+
53+
/* ---- virtual stick + per-finger state --------------------------- */
54+
static float s_def_x = 0.0f, s_def_y = 0.0f; /* deflection -1..1 */
55+
static int s_stick_on = 0;
56+
static float s_cur_x = 0.0f, s_cur_y = 0.0f; /* sub-pixel cursor mirror */
57+
58+
enum { ROLE_NONE = 0, ROLE_STICK, ROLE_LMB, ROLE_RMB, ROLE_GAME };
59+
typedef struct {
60+
SDL_FingerID id;
61+
int used;
62+
int role;
63+
uint32_t t0; /* ROLE_GAME: press time */
64+
int sx, sy; /* ROLE_GAME: press point (output px) */
65+
int moved;
66+
} Finger;
67+
#define MAX_FINGERS 8
68+
static Finger s_fingers[MAX_FINGERS];
69+
70+
/* ---- helpers ----------------------------------------------------- */
71+
static int clampi(int v, int lo, int hi){ return v < lo ? lo : (v > hi ? hi : v); }
72+
73+
static Finger *finger_get(SDL_FingerID id, int create)
74+
{
75+
Finger *free_slot = NULL;
76+
for (int i = 0; i < MAX_FINGERS; ++i) {
77+
if (s_fingers[i].used && s_fingers[i].id == id) return &s_fingers[i];
78+
if (!s_fingers[i].used && !free_slot) free_slot = &s_fingers[i];
79+
}
80+
if (create && free_slot) {
81+
free_slot->used = 1; free_slot->id = id; free_slot->role = ROLE_NONE;
82+
free_slot->moved = 0;
83+
return free_slot;
84+
}
85+
return NULL;
86+
}
87+
static void finger_release(SDL_FingerID id)
88+
{
89+
for (int i = 0; i < MAX_FINGERS; ++i)
90+
if (s_fingers[i].used && s_fingers[i].id == id) { s_fingers[i].used = 0; return; }
91+
}
92+
93+
static int in_circle(int px, int py, int cx, int cy, int r)
94+
{
95+
long dx = px - cx, dy = py - cy;
96+
return dx * dx + dy * dy <= (long)r * r;
97+
}
98+
99+
/* Map an output-pixel point inside the game canvas to logical 640×480. Returns
100+
* 0 if the point is outside the canvas (i.e. in a bar). */
101+
static int win_to_logical(int px, int py, int *lx, int *ly)
102+
{
103+
if (s_scale <= 0.0f) return 0;
104+
int x = (int)((px - s_gx0) / s_scale);
105+
int y = (int)((py - s_gy0) / s_scale);
106+
if (x < 0 || y < 0 || x >= WACKI_SCREEN_W || y >= WACKI_SCREEN_H) return 0;
107+
*lx = x; *ly = y;
108+
return 1;
109+
}
110+
111+
static void recompute(int ow, int oh)
112+
{
113+
s_ow = ow; s_oh = oh;
114+
const float lw = (float)WACKI_SCREEN_W, lh = (float)WACKI_SCREEN_H;
115+
float sx = ow / lw, sy = oh / lh;
116+
s_scale = sx < sy ? sx : sy;
117+
s_gw = (int)(lw * s_scale); s_gh = (int)(lh * s_scale);
118+
s_gx0 = (ow - s_gw) / 2; s_gy0 = (oh - s_gh) / 2;
119+
120+
int left_bar = s_gx0;
121+
int right_bar = ow - (s_gx0 + s_gw);
122+
s_controls_on = (left_bar >= BAR_MIN_PX && right_bar >= BAR_MIN_PX);
123+
124+
/* left bar → joystick */
125+
int sr = (int)((left_bar * STICK_BAR_FRAC < oh * STICK_H_FRAC)
126+
? left_bar * STICK_BAR_FRAC : oh * STICK_H_FRAC);
127+
s_stick_r = sr;
128+
s_stick_cx = left_bar / 2;
129+
s_stick_cy = (int)(oh * STICK_CY_FRAC);
130+
131+
/* right bar → LMB (big) + RMB (small, above it) */
132+
int rcx = s_gx0 + s_gw + right_bar / 2;
133+
int lr = (int)((right_bar * LMB_BAR_FRAC < oh * LMB_H_FRAC)
134+
? right_bar * LMB_BAR_FRAC : oh * LMB_H_FRAC);
135+
s_lmb_r = lr;
136+
s_lmb_cx = rcx;
137+
s_lmb_cy = (int)(oh * LMB_CY_FRAC);
138+
s_rmb_r = (int)(lr * RMB_FRAC);
139+
s_rmb_cx = rcx;
140+
s_rmb_cy = s_lmb_cy - s_lmb_r - s_rmb_r - (int)(oh * 0.03f);
141+
142+
s_have_geom = 1;
143+
}
144+
145+
static void stick_set(int px, int py)
146+
{
147+
if (s_stick_r <= 0) return;
148+
float dx = (float)(px - s_stick_cx) / s_stick_r;
149+
float dy = (float)(py - s_stick_cy) / s_stick_r;
150+
float m = sqrtf(dx * dx + dy * dy);
151+
if (m > 1.0f) { dx /= m; dy /= m; } /* clamp to the rim */
152+
s_def_x = dx; s_def_y = dy;
153+
}
154+
155+
/* ---- drawing ----------------------------------------------------- */
156+
static void fill_circle(SDL_Renderer *ren, int cx, int cy, int r,
157+
Uint8 R, Uint8 G, Uint8 B, Uint8 A)
158+
{
159+
if (r <= 0) return;
160+
SDL_SetRenderDrawColor(ren, R, G, B, A);
161+
for (int dy = -r; dy <= r; ++dy) {
162+
int dx = (int)floor(sqrt((double)r * r - (double)dy * dy));
163+
SDL_RenderDrawLine(ren, cx - dx, cy + dy, cx + dx, cy + dy);
164+
}
165+
}
166+
167+
void wacki_overlay_draw(SDL_Renderer *ren)
168+
{
169+
if (!ren) return;
170+
int ow = 0, oh = 0;
171+
SDL_GetRendererOutputSize(ren, &ow, &oh);
172+
if (ow <= 0 || oh <= 0) return;
173+
recompute(ow, oh);
174+
if (!s_controls_on) return;
175+
176+
/* Draw in output-pixel space: drop the logical-size letterbox mapping so we
177+
* can paint into the bars (outside the 640×480 viewport), then restore. */
178+
int lw = 0, lh = 0;
179+
SDL_RenderGetLogicalSize(ren, &lw, &lh);
180+
SDL_RenderSetLogicalSize(ren, 0, 0);
181+
SDL_BlendMode prev_bm;
182+
SDL_GetRenderDrawBlendMode(ren, &prev_bm);
183+
SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
184+
185+
/* joystick: base + knob at current deflection */
186+
fill_circle(ren, s_stick_cx, s_stick_cy, s_stick_r, 255, 255, 255, A_STICK_BASE);
187+
int kx = s_stick_cx + (int)(s_def_x * s_stick_r);
188+
int ky = s_stick_cy + (int)(s_def_y * s_stick_r);
189+
fill_circle(ren, kx, ky, (int)(s_stick_r * KNOB_FRAC), 255, 255, 255, A_STICK_KNOB);
190+
191+
/* click buttons: big LMB, smaller RMB */
192+
fill_circle(ren, s_lmb_cx, s_lmb_cy, s_lmb_r, 255, 255, 255, A_LMB);
193+
fill_circle(ren, s_rmb_cx, s_rmb_cy, s_rmb_r, 255, 255, 255, A_RMB);
194+
195+
SDL_SetRenderDrawBlendMode(ren, prev_bm);
196+
SDL_RenderSetLogicalSize(ren, lw, lh);
197+
}
198+
199+
/* ---- input ------------------------------------------------------- */
200+
void wacki_overlay_finger_down(SDL_FingerID id, float nx, float ny)
201+
{
202+
if (!s_have_geom) return;
203+
int px = (int)(nx * s_ow), py = (int)(ny * s_oh);
204+
Finger *f = finger_get(id, 1);
205+
if (!f) return;
206+
207+
if (s_controls_on) {
208+
if (in_circle(px, py, s_stick_cx, s_stick_cy, s_stick_r)) {
209+
f->role = ROLE_STICK;
210+
if (!s_stick_on) { s_cur_x = g_mouse_x; s_cur_y = g_mouse_y; }
211+
s_stick_on = 1;
212+
stick_set(px, py);
213+
return;
214+
}
215+
if (in_circle(px, py, s_lmb_cx, s_lmb_cy, s_lmb_r)) {
216+
f->role = ROLE_LMB; g_lmb_clicked = 1; return;
217+
}
218+
if (in_circle(px, py, s_rmb_cx, s_rmb_cy, s_rmb_r)) {
219+
f->role = ROLE_RMB; g_rmb_clicked = 1; return;
220+
}
221+
}
222+
223+
/* Game canvas: move the cursor under the finger (drag to aim), arm a tap. */
224+
int lx, ly;
225+
if (win_to_logical(px, py, &lx, &ly)) {
226+
f->role = ROLE_GAME; f->t0 = SDL_GetTicks();
227+
f->sx = px; f->sy = py; f->moved = 0;
228+
g_mouse_x = (int16_t)lx; g_mouse_y = (int16_t)ly;
229+
} else {
230+
f->role = ROLE_NONE; /* empty bar space → swallow, do nothing */
231+
}
232+
}
233+
234+
void wacki_overlay_finger_motion(SDL_FingerID id, float nx, float ny)
235+
{
236+
if (!s_have_geom) return;
237+
Finger *f = finger_get(id, 0);
238+
if (!f) return;
239+
int px = (int)(nx * s_ow), py = (int)(ny * s_oh);
240+
241+
if (f->role == ROLE_STICK) {
242+
stick_set(px, py);
243+
} else if (f->role == ROLE_GAME) {
244+
int dx = px - f->sx, dy = py - f->sy;
245+
if (dx * dx + dy * dy > TAP_SLOP_PX * TAP_SLOP_PX) f->moved = 1;
246+
int lx, ly;
247+
if (win_to_logical(px, py, &lx, &ly)) {
248+
g_mouse_x = (int16_t)lx; g_mouse_y = (int16_t)ly;
249+
}
250+
}
251+
}
252+
253+
void wacki_overlay_finger_up(SDL_FingerID id, float nx, float ny)
254+
{
255+
(void)nx; (void)ny;
256+
Finger *f = finger_get(id, 0);
257+
if (f) {
258+
if (f->role == ROLE_STICK) {
259+
s_stick_on = 0; s_def_x = s_def_y = 0.0f;
260+
} else if (f->role == ROLE_GAME) {
261+
if (!f->moved && (SDL_GetTicks() - f->t0) <= TAP_MS) g_lmb_clicked = 1;
262+
}
263+
}
264+
finger_release(id);
265+
}
266+
267+
void wacki_overlay_tick(void)
268+
{
269+
if (!s_stick_on) return;
270+
float m = sqrtf(s_def_x * s_def_x + s_def_y * s_def_y);
271+
if (m < STICK_DEADZONE) return;
272+
s_cur_x += s_def_x * STICK_SPEED;
273+
s_cur_y += s_def_y * STICK_SPEED;
274+
s_cur_x = (float)clampi((int)s_cur_x, 0, WACKI_SCREEN_W - 1);
275+
s_cur_y = (float)clampi((int)s_cur_y, 0, WACKI_SCREEN_H - 1);
276+
g_mouse_x = (int16_t)s_cur_x;
277+
g_mouse_y = (int16_t)s_cur_y;
278+
}

0 commit comments

Comments
 (0)