using System; namespace XP.Common.Controls { /// /// 虚拟摇杆核心计算逻辑(纯函数,无副作用)| Virtual joystick core calculation logic (pure functions, no side effects) /// public static class JoystickCalculator { /// /// 将位移限制在圆形区域内 | Clamp displacement within circular radius /// /// X 轴偏移量 | X axis displacement /// Y 轴偏移量 | Y axis displacement /// 最大半径(必须大于 0)| Maximum radius (must be greater than 0) /// 限制后的 (clampedDx, clampedDy) 元组 | Clamped (clampedDx, clampedDy) tuple public static (double clampedDx, double clampedDy) ClampToRadius(double dx, double dy, double radius) { if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "半径必须大于 0 | Radius must be greater than 0"); var distance = Math.Sqrt(dx * dx + dy * dy); // 如果距离在半径内,直接返回原值 | If within radius, return original values if (distance <= radius) return (dx, dy); // 归一化到半径上 | Normalize to radius boundary var scale = radius / distance; return (dx * scale, dy * scale); } /// /// 计算归一化输出,包含死区映射 | Calculate normalized output with dead zone mapping /// /// X 轴偏移量 | X axis displacement /// Y 轴偏移量 | Y axis displacement /// 最大半径(必须大于 0)| Maximum radius (must be greater than 0) /// 死区比例,范围 [0.0, 1.0) | Dead zone ratio, range [0.0, 1.0) /// 摇杆模式 | Joystick mode /// 归一化输出 (outputX, outputY) 元组,范围 [-1.0, 1.0] | Normalized output tuple, range [-1.0, 1.0] public static (double outputX, double outputY) CalculateOutput( double dx, double dy, double radius, double deadZone, JoystickMode mode) { if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "半径必须大于 0 | Radius must be greater than 0"); if (deadZone < 0.0 || deadZone >= 1.0) throw new ArgumentOutOfRangeException(nameof(deadZone), "死区比例必须在 [0.0, 1.0) 范围内 | Dead zone must be in range [0.0, 1.0)"); // 步骤 1-2:将位移限制在半径内 | Step 1-2: Clamp displacement within radius var (clampedDx, clampedDy) = ClampToRadius(dx, dy, radius); // 步骤 3:计算比例值 | Step 3: Calculate ratio var ratioX = clampedDx / radius; var ratioY = clampedDy / radius; // 步骤 4:对每个轴分别应用死区 | Step 4: Apply dead zone to each axis independently var outputX = ApplyDeadZone(ratioX, deadZone); var outputY = ApplyDeadZone(ratioY, deadZone); // 步骤 5:SingleAxisY 模式下强制 OutputX = 0 | Step 5: Force OutputX = 0 in SingleAxisY mode if (mode == JoystickMode.SingleAxisY) outputX = 0.0; return (outputX, outputY); } /// /// 对单轴比例值应用死区映射 | Apply dead zone mapping to a single axis ratio /// /// 轴比例值,范围 [-1.0, 1.0] | Axis ratio, range [-1.0, 1.0] /// 死区比例,范围 [0.0, 1.0) | Dead zone ratio, range [0.0, 1.0) /// 死区映射后的输出值 | Output value after dead zone mapping internal static double ApplyDeadZone(double ratio, double deadZone) { var absRatio = Math.Abs(ratio); // 在死区内,输出为 0 | Within dead zone, output is 0 if (absRatio < deadZone) return 0.0; // 死区外线性映射:sign(ratio) × (absRatio - D) / (1 - D) | Linear mapping outside dead zone return Math.Sign(ratio) * (absRatio - deadZone) / (1.0 - deadZone); } } }