88 lines
4.4 KiB
C#
88 lines
4.4 KiB
C#
using System;
|
||
|
||
namespace XP.Common.Controls
|
||
{
|
||
/// <summary>
|
||
/// 虚拟摇杆核心计算逻辑(纯函数,无副作用)| Virtual joystick core calculation logic (pure functions, no side effects)
|
||
/// </summary>
|
||
public static class JoystickCalculator
|
||
{
|
||
/// <summary>
|
||
/// 将位移限制在圆形区域内 | Clamp displacement within circular radius
|
||
/// </summary>
|
||
/// <param name="dx">X 轴偏移量 | X axis displacement</param>
|
||
/// <param name="dy">Y 轴偏移量 | Y axis displacement</param>
|
||
/// <param name="radius">最大半径(必须大于 0)| Maximum radius (must be greater than 0)</param>
|
||
/// <returns>限制后的 (clampedDx, clampedDy) 元组 | Clamped (clampedDx, clampedDy) tuple</returns>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算归一化输出,包含死区映射 | Calculate normalized output with dead zone mapping
|
||
/// </summary>
|
||
/// <param name="dx">X 轴偏移量 | X axis displacement</param>
|
||
/// <param name="dy">Y 轴偏移量 | Y axis displacement</param>
|
||
/// <param name="radius">最大半径(必须大于 0)| Maximum radius (must be greater than 0)</param>
|
||
/// <param name="deadZone">死区比例,范围 [0.0, 1.0) | Dead zone ratio, range [0.0, 1.0)</param>
|
||
/// <param name="mode">摇杆模式 | Joystick mode</param>
|
||
/// <returns>归一化输出 (outputX, outputY) 元组,范围 [-1.0, 1.0] | Normalized output tuple, range [-1.0, 1.0]</returns>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对单轴比例值应用死区映射 | Apply dead zone mapping to a single axis ratio
|
||
/// </summary>
|
||
/// <param name="ratio">轴比例值,范围 [-1.0, 1.0] | Axis ratio, range [-1.0, 1.0]</param>
|
||
/// <param name="deadZone">死区比例,范围 [0.0, 1.0) | Dead zone ratio, range [0.0, 1.0)</param>
|
||
/// <returns>死区映射后的输出值 | Output value after dead zone mapping</returns>
|
||
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);
|
||
}
|
||
}
|
||
}
|