From 77bfb53b0dd22c50d2bb00623ea5791c6d4b29ec Mon Sep 17 00:00:00 2001 From: seedds Date: Thu, 15 Oct 2026 09:36:39 +0800 Subject: [PATCH] =?UTF-8?q?iOS=20gamepad:=20=E2=88=92=20/=20+=20at=20the?= =?UTF-8?q?=20top=20centre,=20one=20large=20left=20stick,=20no=20D-pad=20o?= =?UTF-8?q?r=20right=20stick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - − and + sit side by side at the top centre of the screen - The on-screen D-pad and right stick are gone; physical controllers keep every button - The left stick ring is as large as the A/B/X/Y diamond and mirrors it; full tilt at 0.8 of the ring radius, axes rounded so full is exactly 32767 - Both bottom-corner controls drop to the bottom edge and reach halfway into the side safe-area inset, closer to the screen corners - README describes the new layout --- README.md | 6 +- dm/ios/Sources/DmGamepad.m | 167 +++++++++++++++---------------------- 2 files changed, 72 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 63c12aa..dc97d68 100644 --- a/README.md +++ b/README.md @@ -121,10 +121,10 @@ Sign and install the IPA with your own tool (AltStore, Sideloadly, ...). The too - `front_ios.m`: the `front.h` functions and a centred 16:9 Metal view. - `DmGamepad.m`: a translucent touch gamepad, drawn over the picture and kept close to the edges: - ZL above L in the top-left corner, ZR above R in the top-right; - - − and + in the bottom corners, with the D-pad and A/B/X/Y (Nintendo positions) just above them; - - left and right sticks (fixed rings) just inside those. + - − and + side by side at the top centre; + - the left stick (a fixed ring) in the bottom-left corner and A/B/X/Y (Nintendo positions) in the bottom-right, the same size and mirrored. - The gamepad is hidden while a physical controller is connected. Physical buttons map by position. + There is no on-screen D-pad or right stick. The gamepad is hidden while a physical controller is connected; physical controllers keep every button and map by position. The host log goes to the app's Documents (`log.txt`, with the previous run in `log.prev.txt`), visible in the Files app. Game files are also accepted at `Documents/romfs`. Environment switches can also go in `Documents/env.txt`, in the same format as `DM_ENV_FILE`. It takes precedence over the bundled file, so a sideloaded app can be switched without rebuilding. diff --git a/dm/ios/Sources/DmGamepad.m b/dm/ios/Sources/DmGamepad.m index 5b2f3b5..74d2db4 100644 --- a/dm/ios/Sources/DmGamepad.m +++ b/dm/ios/Sources/DmGamepad.m @@ -1,11 +1,15 @@ -// 虚拟手柄(常见串流手柄的布局,任天堂键位)+ 实体手柄(GameController)。两路合并后经 dm_input_set 交给宿主(hle_hid.c,键位见 dm_input.h)。 -// 上角:ZL 在上、L 在下(同一列),ZR / R 同,水滴形,尖朝屏角;下角:− / +,紧贴其上是十字键(左,四个分开的箭头键)与 A(右)B(下)X(上)Y(左)菱形,手指在键间滑动会换键; -// 紧挨在它们内侧、贴底:左、右摇杆(环固定,推量按环心算,带小死区)。控件都往边角靠,给画面多留地方;按键叠在画面上,每个控件一个 CAShapeLayer:按下只换填色,摇杆只挪手柄头,不整屏重画。 -// 接上实体手柄时虚拟手柄隐藏,拔掉后重现。实体手柄按位置对应:下 = B、右 = A、左 = Y、上 = X(与 Switch 手柄的键位位置一致)。 -// Virtual gamepad (the usual streaming-pad layout, Nintendo button positions) + physical controllers (GameController). Both are merged and handed to the host through dm_input_set (hle_hid.c, bit layout in dm_input.h). -// Top corners: ZL above L (one column), ZR above R likewise, teardrops with the tip toward the screen corner; bottom corners: − / +, with the D-pad (left, four separate arrow pads) and the A (right) B (bottom) X (top) Y (left) diamond right above them, sliding a finger between keys switches keys; -// just inside those, along the bottom edge: left and right sticks (fixed rings, tilt measured from the ring centre, small dead zone). Everything hugs the edges and corners to leave the picture room; controls sit over the picture, one CAShapeLayer each: a press only swaps the fill, a stick only moves its knob, nothing redraws the whole screen. -// The virtual gamepad hides while a physical controller is connected and comes back when it is removed. Physical controllers map by position: bottom = B, right = A, left = Y, top = X (same positions as on a Switch controller). +// 虚拟手柄(任天堂键位)+ 实体手柄(GameController)。两路合并后经 dm_input_set 交给宿主(hle_hid.c,键位见 dm_input.h)。 +// 上角:ZL 在上、L 在下(同一列),ZR / R 同,水滴形,尖朝屏角;顶边正中:− / + 并排; +// 左下角:左摇杆(环固定,推量按环心算,带小死区);右下角:A(右)B(下)X(上)Y(左)菱形,手指在键间滑动会换键。 +// 虚拟手柄没有十字键和右摇杆,下角只各剩一个控件,所以贴屏角放(伸进侧边安全区一半)。控件都往边角靠,给画面多留地方; +// 按键叠在画面上,每个控件一个 CAShapeLayer:按下只换填色,摇杆只挪手柄头,不整屏重画。 +// 接上实体手柄时虚拟手柄隐藏,拔掉后重现。实体手柄全键都在(含十字键、右摇杆),按位置对应:下 = B、右 = A、左 = Y、上 = X(与 Switch 手柄的键位位置一致)。 +// Virtual gamepad (Nintendo button positions) + physical controllers (GameController). Both are merged and handed to the host through dm_input_set (hle_hid.c, bit layout in dm_input.h). +// Top corners: ZL above L (one column), ZR above R likewise, teardrops with the tip toward the screen corner; top edge, centred: − and + side by side; +// bottom-left corner: the left stick (fixed ring, tilt measured from the ring centre, small dead zone); bottom-right corner: the A (right) B (bottom) X (top) Y (left) diamond, sliding a finger between keys switches keys. +// The virtual gamepad has no D-pad and no right stick, so each bottom corner holds a single control and it sits close in (reaching halfway into the side safe-area inset). Everything hugs the edges and corners to leave the picture room; +// controls sit over the picture, one CAShapeLayer each: a press only swaps the fill, the stick only moves its knob, nothing redraws the whole screen. +// The virtual gamepad hides while a physical controller is connected and comes back when it is removed. Physical controllers keep every button (D-pad and right stick included) and map by position: bottom = B, right = A, left = Y, top = X (same positions as on a Switch controller). #import #import #import @@ -13,32 +17,33 @@ #include "dm_input.h" enum { B_A = 0, B_B, B_X, B_Y, B_LS, B_RS, B_L, B_R, B_ZL, B_ZR, B_PLUS, B_MINUS, B_LEFT, B_UP, B_RIGHT, B_DOWN, B_N }; -enum { C_NONE = 0, C_STICK, C_DPAD, C_FACE, C_BTN }; // 触点占用的控件 | which control a touch holds +enum { C_NONE = 0, C_STICK, C_FACE, C_BTN }; // 触点占用的控件 | which control a touch holds static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static uint64_t g_touch_btn, g_pad_btn; -static int32_t g_touch_lx, g_touch_ly, g_touch_rx, g_touch_ry, g_pad_lx, g_pad_ly, g_pad_rx, g_pad_ry; +static int32_t g_touch_lx, g_touch_ly, g_pad_lx, g_pad_ly, g_pad_rx, g_pad_ry; static int tilted(int32_t x, int32_t y) { return (int64_t)x * x + (int64_t)y * y > (int64_t)4000 * 4000; } static void publish(void) { pthread_mutex_lock(&g_mu); - int32_t lx = g_touch_lx, ly = g_touch_ly, rx = g_touch_rx, ry = g_touch_ry; + int32_t lx = g_touch_lx, ly = g_touch_ly; if (tilted(g_pad_lx, g_pad_ly)) { lx = g_pad_lx; ly = g_pad_ly; } // 实体摇杆推着时用实体的 | a tilted physical stick wins - if (tilted(g_pad_rx, g_pad_ry)) { rx = g_pad_rx; ry = g_pad_ry; } - dm_input_set(g_touch_btn | g_pad_btn, lx, ly, rx, ry); + dm_input_set(g_touch_btn | g_pad_btn, lx, ly, g_pad_rx, g_pad_ry); // 右摇杆只有实体手柄有 | only a physical controller has a right stick pthread_mutex_unlock(&g_mu); } -// ---- 布局常数(单位 u;按左半边给,x 量自外侧边,右半边镜像) | Layout constants (in u; given for the left half, x measured from the outer edge, the right half mirrors) ---- +// ---- 布局常数(单位 u;x 量自外侧边,右半边镜像) | Layout constants (in u; x measured from the outer edge, the right half mirrors) ---- static const CGFloat K_ZL_X = 44, K_ZL_Y = 40, K_ZL_R = 26, // ZL / ZR:中心离外侧、离顶,半径 | ZL / ZR: centre from the outer edge and the top, radius K_L_X = 44, K_L_Y = 108, K_L_R = 24, // L / R:与 ZL / ZR 同一列 | L / R: same column as ZL / ZR K_DROP_TIP = 1.45, // 水滴尖离圆心(半径的倍数) | teardrop tip distance from the centre (times the radius) - K_DPAD_X = 64, K_FACE_X = 81, K_PAD_Y = 108, // 十字键、菱形中心离外侧,二者离底 | D-pad and diamond centre from the outer edge, both from the bottom - K_DPAD_R = 54, K_ARM_W = 18, K_ARM_IN = 12, // 十字键:箭头外沿离中心、半宽、尖离中心 | D-pad: arm outer edge from the centre, half-width, tip from the centre + K_CORNER_IN = 0.5, // 下角控件伸进侧边安全区的比例(横屏时那里只有屏幕圆角,灵动岛在边的正中) | how far the bottom-corner controls reach into the side safe-area inset (in landscape there is only the rounded screen corner there; the Dynamic Island is mid-edge) + K_FACE_X = 81, K_PAD_Y = 82, // 菱形中心离外侧;菱形、摇杆中心离底(B 键下沿离底约 5) | diamond centre from the outer edge; diamond and stick centre from the bottom (B's lower edge about 5 above it) K_FACE_GAP = 50, K_FACE_R = 27, // 菱形:键离中心、键半径 | diamond: key distance from the centre, key radius - K_PILL_X = 32, K_PILL_Y = 16, K_PILL_W = 50, K_PILL_H = 28, // − / +:中心离外侧、离底,宽高 | − / +: centre from the outer edge and the bottom, size - K_STICK_LX = 176, K_STICK_RX = 214, K_STICK_Y = 64, K_STICK_R = 50, K_KNOB_R = 22, // 摇杆:左 / 右环心离外侧(菱形比十字键宽)、离底,环半径、手柄头半径 | sticks: left / right ring centre from the outer edge (the diamond is wider than the D-pad), from the bottom, ring radius, knob radius - K_DEAD = 0.12; // 摇杆死区(环半径的倍数) | stick dead zone (times the ring radius) + K_PILL_DX = 40, K_PILL_Y = 22, K_PILL_W = 50, K_PILL_H = 28, // − / +:中心离屏幕中线、离顶,宽高 | − / +: centre from the screen's midline and from the top, size + K_STICK_X = 81, K_STICK_R = 77, K_KNOB_R = 30, // 左摇杆:与菱形左右对称,环和整个菱形一样大(键距 + 键半径),手柄头比菱形的键略大 | left stick: mirrors the diamond, the ring as large as the whole diamond (key distance + key radius), the knob a little larger than a diamond key + K_STICK_GRAB = 1.4, // 按下点离环心多近算抓住摇杆(环半径的倍数;左下角没有别的控件和它抢) | how near the ring centre a touch-down grabs the stick (times the ring radius; nothing else in the bottom-left corner competes) + K_FULL = 0.8, // 推到环半径的这么多就算推满(约 62,原先小环是 50;手柄头仍能走到环边) | full tilt at this fraction of the ring radius (about 62; the former small ring had 50; the knob still travels to the ring edge) + K_DEAD = 0.1; // 摇杆死区(环半径的倍数) | stick dead zone (times the ring radius) static const int k_btns[] = {B_ZL, B_L, B_ZR, B_R, B_MINUS, B_PLUS}; @interface DmGamepad : UIView @@ -46,13 +51,13 @@ static const int k_btns[] = {B_ZL, B_L, B_ZR, B_R, B_MINUS, B_PLUS}; static __weak DmGamepad *g_view; @implementation DmGamepad { - NSMapTable *_owner; // 触点 → 控件(C_BTN 时附带键号、C_STICK 时附带摇杆号,<< 8) | touch → control (with the key for C_BTN or the stick for C_STICK, << 8) - UITouch *_stick[2]; CGPoint _stickNow[2]; // 0 左 1 右 | 0 left, 1 right - uint64_t _faceBits, _dpadBits, _btnBits; + NSMapTable *_owner; // 触点 → 控件(C_BTN 时附带键号,<< 8) | touch → control (with the key for C_BTN, << 8) + UITouch *_stick; CGPoint _stickNow; // 握着左摇杆的触点、它现在的位置 | the touch holding the left stick and where it is now + uint64_t _faceBits, _btnBits; NSMapTable *_faceOf; // 每个落在菱形区的触点各自按着哪些键 | which keys each touch on the diamond currently holds - CAShapeLayer *_key[B_N]; CATextLayer *_label[B_N]; // 每个键一层(摇杆按键位空着) | one layer per key (the stick-click bits stay empty) - CAShapeLayer *_ring[2], *_knob[2]; - BOOL _sticksShown; // 上次刷新时有摇杆按着 | a stick was held at the last refresh + CAShapeLayer *_key[B_N]; CATextLayer *_label[B_N]; // 每个键一层(摇杆按下、十字键只有实体手柄有,位空着) | one layer per key (stick clicks and the D-pad exist only on physical controllers; those stay empty) + CAShapeLayer *_ring, *_knob; + BOOL _stickShown; // 上次刷新时摇杆按着 | the stick was held at the last refresh } static UIColor *k_idle, *k_on, *k_line, *k_ringFill, *k_text, *k_textOn; @@ -78,15 +83,13 @@ static CATextLayer *label(CALayer *parent, NSString *s) { k_idle = [UIColor colorWithWhite:1 alpha:0.14]; k_on = [UIColor colorWithWhite:1 alpha:0.45]; k_line = [UIColor colorWithWhite:1 alpha:0.38]; k_ringFill = [UIColor colorWithWhite:1 alpha:0.06]; k_text = [UIColor colorWithWhite:1 alpha:0.6]; k_textOn = [UIColor colorWithWhite:0 alpha:0.7]; }); - for (int i = 0; i < 2; i++) { - _ring[i] = shape(self.layer); _ring[i].fillColor = k_ringFill.CGColor; - _knob[i] = shape(self.layer); - CAShapeLayer *in = [CAShapeLayer layer]; in.fillColor = nil; in.strokeColor = k_line.CGColor; in.lineWidth = 1.5; // 手柄头内圈 | inner ring of the knob - [_knob[i] addSublayer:in]; - } + _ring = shape(self.layer); _ring.fillColor = k_ringFill.CGColor; + _knob = shape(self.layer); + CAShapeLayer *in = [CAShapeLayer layer]; in.fillColor = nil; in.strokeColor = k_line.CGColor; in.lineWidth = 1.5; // 手柄头内圈 | inner ring of the knob + [_knob addSublayer:in]; static NSString *const names[B_N] = {@"A", @"B", @"X", @"Y", nil, nil, @"L", @"R", @"ZL", @"ZR", @"+", @"−"}; for (int b = 0; b < B_N; b++) { - if (b == B_LS || b == B_RS) continue; + if (b == B_LS || b == B_RS || b >= B_LEFT) continue; _key[b] = shape(self.layer); if (names[b]) _label[b] = label(_key[b], names[b]); } @@ -100,23 +103,22 @@ static CATextLayer *label(CALayer *parent, NSString *s) { // ---- 布局(点;按安全区内的矩形算) | Layout (points; computed inside the safe area) ---- - (CGRect)safe { return UIEdgeInsetsInsetRect(self.bounds, self.safeAreaInsets); } +- (CGRect)corner { UIEdgeInsets i = self.safeAreaInsets; i.left *= K_CORNER_IN; i.right *= K_CORNER_IN; return UIEdgeInsetsInsetRect(self.bounds, i); } // 下角控件用的矩形 | the rectangle the bottom-corner controls use - (CGFloat)u { return MIN(self.bounds.size.height / 430.0, 1.3); } // 以 430 点高(iPhone Pro Max 横屏)为 1 | 1 = 430 points tall (iPhone Pro Max landscape) static CGPoint side(CGRect s, int right, CGFloat dx, CGFloat y) { return CGPointMake(right ? CGRectGetMaxX(s) - dx : CGRectGetMinX(s) + dx, y); } -- (CGPoint)faceCenter { CGRect s = self.safe; CGFloat u = self.u; return side(s, 1, K_FACE_X * u, CGRectGetMaxY(s) - K_PAD_Y * u); } +- (CGPoint)faceCenter { CGRect s = self.corner; CGFloat u = self.u; return side(s, 1, K_FACE_X * u, CGRectGetMaxY(s) - K_PAD_Y * u); } - (CGFloat)faceGap { return K_FACE_GAP * self.u; } - (CGFloat)faceR { return K_FACE_R * self.u; } - (CGPoint)facePos:(int)b { CGPoint c = self.faceCenter; CGFloat g = self.faceGap; switch (b) { case B_A: return CGPointMake(c.x + g, c.y); case B_B: return CGPointMake(c.x, c.y + g); case B_X: return CGPointMake(c.x, c.y - g); default: return CGPointMake(c.x - g, c.y); } } -- (CGPoint)dpadCenter { CGRect s = self.safe; CGFloat u = self.u; return side(s, 0, K_DPAD_X * u, CGRectGetMaxY(s) - K_PAD_Y * u); } -- (CGFloat)dpadR { return K_DPAD_R * self.u; } -- (CGPoint)stickHome:(int)i { CGRect s = self.safe; CGFloat u = self.u; return side(s, i, (i ? K_STICK_RX : K_STICK_LX) * u, CGRectGetMaxY(s) - K_STICK_Y * u); } +- (CGPoint)stickHome { CGRect s = self.corner; CGFloat u = self.u; return side(s, 0, K_STICK_X * u, CGRectGetMaxY(s) - K_PAD_Y * u); } - (CGFloat)stickR { return K_STICK_R * self.u; } static BOOL isDrop(int b) { return b == B_ZL || b == B_L || b == B_ZR || b == B_R; } - (CGPoint)dropCenter:(int)b { CGRect s = self.safe; CGFloat u = self.u; BOOL z = b == B_ZL || b == B_ZR; return side(s, b == B_ZR || b == B_R, (z ? K_ZL_X : K_L_X) * u, CGRectGetMinY(s) + (z ? K_ZL_Y : K_L_Y) * u); } - (CGFloat)dropR:(int)b { return (b == B_ZL || b == B_ZR ? K_ZL_R : K_L_R) * self.u; } - (CGRect)pillRect:(int)b { CGRect s = self.safe; CGFloat u = self.u, w = K_PILL_W * u, h = K_PILL_H * u; - CGPoint c = side(s, b == B_PLUS, K_PILL_X * u, CGRectGetMaxY(s) - K_PILL_Y * u); + CGPoint c = CGPointMake(CGRectGetMidX(s) + (b == B_PLUS ? K_PILL_DX : -K_PILL_DX) * u, CGRectGetMinY(s) + K_PILL_Y * u); // − 在左、+ 在右 | − on the left, + on the right return CGRectMake(c.x - w / 2, c.y - h / 2, w, h); } static CGFloat dist(CGPoint a, CGPoint b) { return hypot(a.x - b.x, a.y - b.y); } @@ -132,21 +134,10 @@ static CGFloat dist(CGPoint a, CGPoint b) { return hypot(a.x - b.x, a.y - b.y); return m; } - (BOOL)inFace:(CGPoint)p { return dist(p, self.faceCenter) <= self.faceGap + self.faceR * 1.6; } -- (uint64_t)dpadAt:(CGPoint)p { - CGPoint c = self.dpadCenter; CGFloat dx = p.x - c.x, dy = p.y - c.y; - if (hypot(dx, dy) < self.dpadR * 0.25) return 0; - double a = atan2(-dy, dx) * 180 / M_PI; uint64_t m = 0; // 8 向:每向 45°,斜向按两键 | 8 directions: 45° each, diagonals press two keys - if (a > -67.5 && a < 67.5) m |= 1ull << B_RIGHT; - if (a > 22.5 && a < 157.5) m |= 1ull << B_UP; - if (a > 112.5 || a < -112.5) m |= 1ull << B_LEFT; - if (a < -22.5 && a > -157.5) m |= 1ull << B_DOWN; - return m; -} -- (BOOL)inDpad:(CGPoint)p { return dist(p, self.dpadCenter) <= self.dpadR * 1.3; } -- (BOOL)inStick:(int)i at:(CGPoint)p { return dist(p, [self stickHome:i]) <= self.stickR * 1.5; } -- (CGPoint)knobOffset:(int)i { // 手柄头偏离环心(限在环内) | knob offset from the ring centre (clamped to the ring) - if (!_stick[i]) return CGPointZero; - CGPoint h = [self stickHome:i]; CGFloat dx = _stickNow[i].x - h.x, dy = _stickNow[i].y - h.y, d = hypot(dx, dy), R = self.stickR; +- (BOOL)inStick:(CGPoint)p { return dist(p, self.stickHome) <= self.stickR * K_STICK_GRAB; } +- (CGPoint)knobOffset { // 手柄头偏离环心(限在环内) | knob offset from the ring centre (clamped to the ring) + if (!_stick) return CGPointZero; + CGPoint h = self.stickHome; CGFloat dx = _stickNow.x - h.x, dy = _stickNow.y - h.y, d = hypot(dx, dy), R = self.stickR; if (d > R) { dx *= R / d; dy *= R / d; } return CGPointMake(dx, dy); } @@ -154,53 +145,48 @@ static CGFloat dist(CGPoint a, CGPoint b) { return hypot(a.x - b.x, a.y - b.y); // ---- 触摸 | Touch ---- - (void)recompute { uint64_t face = 0; for (UITouch *t in _faceOf) face |= [_faceOf objectForKey:t].unsignedLongLongValue; - uint64_t btn = 0, dpad = 0; - for (UITouch *t in _owner) { uint64_t v = [_owner objectForKey:t].unsignedLongLongValue; int c = (int)(v & 0xff); - if (c == C_BTN) btn |= 1ull << (v >> 8); - else if (c == C_DPAD) dpad |= [self dpadAt:[t locationInView:self]]; } - int32_t ax[4] = {0}; - for (int i = 0; i < 2; i++) { - CGPoint o = [self knobOffset:i]; CGFloat R = self.stickR, d = hypot(o.x, o.y), dz = R * K_DEAD; - if (d <= dz) continue; - CGFloat m = (d - dz) / (R - dz) / d; // 死区外从 0 起算到环边为满 | outside the dead zone: 0 rising to full at the ring edge - ax[i * 2] = (int32_t)(o.x * m * 32767); ax[i * 2 + 1] = (int32_t)(-o.y * m * 32767); // 宿主:上为正 | host: up is positive + uint64_t btn = 0; + for (UITouch *t in _owner) { uint64_t v = [_owner objectForKey:t].unsignedLongLongValue; if ((v & 0xff) == C_BTN) btn |= 1ull << (v >> 8); } + int32_t ax = 0, ay = 0; + CGPoint o = self.knobOffset; CGFloat R = self.stickR, d = hypot(o.x, o.y), dz = R * K_DEAD; + if (d > dz) { + CGFloat m = fmin((d - dz) / (R * K_FULL - dz), 1) / d; // 死区外从 0 起算,到推满处为满 | outside the dead zone: 0 rising to full at the full-tilt radius + ax = (int32_t)lrint(o.x * m * 32767); ay = (int32_t)lrint(-o.y * m * 32767); // 宿主:上为正;四舍五入,推满正好是 32767 | host: up is positive; rounded so full tilt is exactly 32767 } - BOOL changed = face != _faceBits || dpad != _dpadBits || btn != _btnBits; - _faceBits = face; _dpadBits = dpad; _btnBits = btn; + BOOL changed = face != _faceBits || btn != _btnBits; + _faceBits = face; _btnBits = btn; pthread_mutex_lock(&g_mu); - g_touch_btn = face | dpad | btn; g_touch_lx = ax[0]; g_touch_ly = ax[1]; g_touch_rx = ax[2]; g_touch_ry = ax[3]; + g_touch_btn = face | btn; g_touch_lx = ax; g_touch_ly = ay; pthread_mutex_unlock(&g_mu); publish(); - BOOL sticks = _stick[0] || _stick[1]; - if (changed || sticks || _sticksShown) [self refresh]; // 松开摇杆那次也要刷,手柄头才回中 | also refresh on the stick release so the knob returns to the centre - _sticksShown = sticks; + BOOL held = _stick != nil; + if (changed || held || _stickShown) [self refresh]; // 松开摇杆那次也要刷,手柄头才回中 | also refresh on the stick release so the knob returns to the centre + _stickShown = held; } - (void)touchesBegan:(NSSet *)ts withEvent:(UIEvent *)e { for (UITouch *t in ts) { CGPoint p = [t locationInView:self]; int hit = [self btnAt:p]; if (hit >= 0) [_owner setObject:@(C_BTN | (uint64_t)hit << 8) forKey:t]; else if ([self inFace:p]) { [_owner setObject:@(C_FACE) forKey:t]; [_faceOf setObject:@([self faceAt:p]) forKey:t]; } - else if ([self inDpad:p]) [_owner setObject:@(C_DPAD) forKey:t]; - else for (int i = 0; i < 2; i++) if (!_stick[i] && [self inStick:i at:p]) { - [_owner setObject:@(C_STICK | (uint64_t)i << 8) forKey:t]; _stick[i] = t; _stickNow[i] = p; break; } + else if (!_stick && [self inStick:p]) { [_owner setObject:@(C_STICK) forKey:t]; _stick = t; _stickNow = p; } } [self recompute]; } - (void)touchesMoved:(NSSet *)ts withEvent:(UIEvent *)e { for (UITouch *t in ts) { - uint64_t v = [_owner objectForKey:t].unsignedLongLongValue; int c = (int)(v & 0xff); CGPoint p = [t locationInView:self]; + int c = (int)([_owner objectForKey:t].unsignedLongLongValue & 0xff); CGPoint p = [t locationInView:self]; if (c == C_FACE) [_faceOf setObject:@([self faceAt:p]) forKey:t]; - else if (c == C_STICK) _stickNow[v >> 8] = p; + else if (c == C_STICK) _stickNow = p; } [self recompute]; } - (void)endTouches:(NSSet *)ts { - for (UITouch *t in ts) { [_owner removeObjectForKey:t]; [_faceOf removeObjectForKey:t]; for (int i = 0; i < 2; i++) if (t == _stick[i]) _stick[i] = nil; } + for (UITouch *t in ts) { [_owner removeObjectForKey:t]; [_faceOf removeObjectForKey:t]; if (t == _stick) _stick = nil; } [self recompute]; } - (void)touchesEnded:(NSSet *)ts withEvent:(UIEvent *)e { [self endTouches:ts]; } - (void)touchesCancelled:(NSSet *)ts withEvent:(UIEvent *)e { [self endTouches:ts]; } -- (void)reset { [_owner removeAllObjects]; [_faceOf removeAllObjects]; _stick[0] = _stick[1] = nil; [self recompute]; } +- (void)reset { [_owner removeAllObjects]; [_faceOf removeAllObjects]; _stick = nil; [self recompute]; } // ---- 形状 | Shapes ---- static CGPathRef circlePath(CGPoint c, CGFloat r) { return CGPathCreateWithEllipseInRect(CGRectMake(c.x - r, c.y - r, 2 * r, 2 * r), NULL); } @@ -214,15 +200,6 @@ static CGPathRef dropPath(CGPoint c, CGFloat r, CGFloat tipAngle, CGFloat round) CGPathAddArc(p, NULL, c.x, c.y, r, tipAngle + th, tipAngle - th + 2 * M_PI, false); CGPathCloseSubpath(p); return p; } -static CGPathRef armPath(CGPoint c, CGFloat R, CGFloat w, CGFloat in, CGFloat angle, CGFloat round) { // 十字键一臂:朝上的五边形(尖朝中心)绕中心转 angle | one D-pad arm: an upward pentagon (tip toward the centre) rotated by angle around the centre - CGPoint v[5] = {{-w, -R}, {w, -R}, {w, -(in + w)}, {0, -in}, {-w, -(in + w)}}; - CGFloat cs = cos(angle), sn = sin(angle); - for (int i = 0; i < 5; i++) v[i] = CGPointMake(c.x + v[i].x * cs - v[i].y * sn, c.y + v[i].x * sn + v[i].y * cs); - CGMutablePathRef p = CGPathCreateMutable(); - CGPathMoveToPoint(p, NULL, (v[4].x + v[0].x) / 2, (v[4].y + v[0].y) / 2); - for (int i = 0; i < 5; i++) CGPathAddArcToPoint(p, NULL, v[i].x, v[i].y, v[(i + 1) % 5].x, v[(i + 1) % 5].y, round); - CGPathCloseSubpath(p); return p; -} static void setPath(CAShapeLayer *l, CGPathRef p) { l.path = p; l.shadowPath = p; CGPathRelease(p); } static void placeLabel(CATextLayer *t, CGPoint c, CGFloat fs) { CTFontRef f = CTFontCreateUIFontForLanguage(kCTFontUIFontEmphasizedSystem, fs, NULL); @@ -237,8 +214,6 @@ static void placeLabel(CATextLayer *t, CGPoint c, CGFloat fs) { CGFloat u = self.u; [CATransaction begin]; [CATransaction setDisableActions:YES]; for (int b = B_A; b <= B_Y; b++) { CGPoint c = [self facePos:b]; setPath(_key[b], circlePath(c, self.faceR)); placeLabel(_label[b], c, 20 * u); } - static const struct { int b; CGFloat angle; } arms[] = {{B_UP, 0}, {B_RIGHT, M_PI_2}, {B_DOWN, M_PI}, {B_LEFT, -M_PI_2}}; // 同一臂转 90° 一步 | the same arm in 90° steps - for (int i = 0; i < 4; i++) setPath(_key[arms[i].b], armPath(self.dpadCenter, self.dpadR, K_ARM_W * u, K_ARM_IN * u, arms[i].angle, 6 * u)); for (int i = 0; i < 6; i++) { int b = k_btns[i]; if (isDrop(b)) { BOOL right = b == B_ZR || b == B_R; CGPoint c = [self dropCenter:b]; @@ -248,26 +223,22 @@ static void placeLabel(CATextLayer *t, CGPoint c, CGFloat fs) { setPath(_key[b], CGPathCreateWithRoundedRect(rc, rc.size.height / 2, rc.size.height / 2, NULL)); placeLabel(_label[b], CGPointMake(CGRectGetMidX(rc), CGRectGetMidY(rc)), 18 * u); } } - for (int i = 0; i < 2; i++) { - setPath(_ring[i], circlePath([self stickHome:i], self.stickR)); - CGFloat k = K_KNOB_R * u; _knob[i].bounds = CGRectMake(0, 0, 2 * k, 2 * k); - setPath(_knob[i], circlePath(CGPointMake(k, k), k)); - CAShapeLayer *in = (CAShapeLayer *)_knob[i].sublayers.firstObject; CGPathRef ip = circlePath(CGPointMake(k, k), k * 0.62); - in.frame = _knob[i].bounds; in.path = ip; CGPathRelease(ip); - } + setPath(_ring, circlePath(self.stickHome, self.stickR)); + CGFloat k = K_KNOB_R * u; _knob.bounds = CGRectMake(0, 0, 2 * k, 2 * k); + setPath(_knob, circlePath(CGPointMake(k, k), k)); + CAShapeLayer *in = (CAShapeLayer *)_knob.sublayers.firstObject; CGPathRef ip = circlePath(CGPointMake(k, k), k * 0.62); + in.frame = _knob.bounds; in.path = ip; CGPathRelease(ip); [CATransaction commit]; [self refresh]; } - (void)safeAreaInsetsDidChange { [super safeAreaInsetsDidChange]; [self setNeedsLayout]; } - (void)refresh { // 按下状态 → 填色,摇杆 → 手柄头位置 | pressed state → fill, sticks → knob position - uint64_t all = _faceBits | _dpadBits | _btnBits; + uint64_t all = _faceBits | _btnBits; [CATransaction begin]; [CATransaction setDisableActions:YES]; for (int b = 0; b < B_N; b++) if (_key[b]) { BOOL on = (all >> b) & 1; _key[b].fillColor = (on ? k_on : k_idle).CGColor; _label[b].foregroundColor = (on ? k_textOn : k_text).CGColor; } - for (int i = 0; i < 2; i++) { - CGPoint h = [self stickHome:i], o = [self knobOffset:i]; - _knob[i].position = CGPointMake(h.x + o.x, h.y + o.y); _knob[i].fillColor = (_stick[i] ? k_on : k_idle).CGColor; - } + CGPoint h = self.stickHome, o = self.knobOffset; + _knob.position = CGPointMake(h.x + o.x, h.y + o.y); _knob.fillColor = (_stick ? k_on : k_idle).CGColor; [CATransaction commit]; }