主题
GameSystemUI 移动控件
GameSystemUI 提供一套平台自适应的移动输入控件:手机端显示虚拟摇杆,PC 端默认使用 WASD。它有两种使用模式:
- 单位控制模式:绑定
Player.LocalPlayer.MainUnit或指定Unit,控件自动发出单位移动指令。 - 输入事件模式:不绑定单位,只订阅移动输入事件,用于纯 GameGraph / SceneGraph、相机、载具、光标或其它自定义控制目标。
定义位置:GameSystemUI.MoveSystem
常用数据入口:gamesystemui.ScopeData.GameDataControl.MoveControl
快速开始
game_entry 模板默认引用 GameSystemUI.dll,并把 gamesystemui.ScopeData 配置为 GSUI 全局别名。通常直接创建平台自适应根控件即可:
csharp
#if CLIENT
using GameSystemUI.MoveSystem;
var moveControlRoot = GSUI.GameDataControl.MoveControl.Data!.CreateControl();
moveControlRoot.AddToVisualTree();
#endifMoveControl 是 GameDataControlPlatformAdaptivePanel,运行时会根据平台选择实际子控件:
| 平台 | 实际控件 | 行为 |
|---|---|---|
| 手机 / 移动调试 | MoveJoyStick | 虚拟摇杆 |
| Windows / PC | MoveKeyBoard | WASD |
默认模式:控制主控单位
默认情况下 UseMainUnit = true。控件会绑定本地玩家主控单位,并把输入转换为 VectorMove / VectorMoveAdjust / VectorMoveStop 指令。
服务端需要先设置主控单位:
csharp
#if SERVER
player.MainUnit = hero;
#endif客户端添加移动控件:
csharp
#if CLIENT
using GameSystemUI.MoveSystem;
var moveControlRoot = GSUI.GameDataControl.MoveControl.Data!.CreateControl();
moveControlRoot.AddToVisualTree();
#endif适合常规 2.5D / 3D 单位游戏。相机角度变化时,控件会重新按当前相机方向修正移动指令。
指定单位模式
如果不想使用本地玩家 MainUnit,可以手动绑定单位:
csharp
#if CLIENT
using GameSystemUI.MoveSystem;
var moveControlRoot = GSUI.GameDataControl.MoveControl.Data!.CreateControl();
moveControlRoot.AddToVisualTree();
moveControlRoot.SetUseMainUnit(false);
moveControlRoot.BindMoveUnit(myUnit);
#endif也可以先取得实际 MoveControlBase:
csharp
var moveControl = moveControlRoot.GetMoveControl();
if (moveControl != null)
{
moveControl.BindUnit = myUnit; // 手动设置 BindUnit 会自动关闭 UseMainUnit
}输入事件模式:不绑定单位
纯 GameGraph / SceneGraph 游戏没有 Unit 或 MainUnit 的概念。此时可以只使用 GameSystemUI 的跨平台输入 UI,不让它发单位移动指令:
csharp
#if CLIENT
using GameUI.Control;
using GameSystemUI.MoveSystem;
using System.Numerics;
private Control? moveControlRoot;
private MoveControlBase? moveControl;
private Vector2 moveDirection;
private void CreateMoveInput()
{
moveControlRoot = GSUI.GameDataControl.MoveControl.Data!.CreateControl();
moveControlRoot.AddToVisualTree();
moveControl = moveControlRoot.GetMoveControl();
if (moveControl == null)
{
Game.Logger.LogWarning("MoveControl was created, but no MoveControlBase child was found.");
return;
}
moveControl.UseMainUnit = false;
moveControl.BindUnit = null;
moveControl.OnMoveInput += OnMoveInput;
}
private void OnMoveInput(object? sender, MoveInputEventArgs e)
{
if (e.State == MoveInputState.Stopped)
{
moveDirection = Vector2.Zero;
return;
}
// 常规 SCE 世界平面控制使用镜头补偿方向;固定 XY 正交相机见下文。
moveDirection = e.WorldDirection;
}
private void DisposeMoveInput()
{
if (moveControl != null)
{
moveControl.OnMoveInput -= OnMoveInput;
moveControl = null;
}
moveControlRoot?.RemoveFromVisualTree();
moveControlRoot?.Dispose();
moveControlRoot = null;
}
#endif在 Think 或其它更新循环中,把 moveDirection 应用到自定义目标即可:
csharp
node.Position += new Vector3(moveDirection.X, moveDirection.Y, 0f) * speed * dt;项目应根据目标坐标系选择原始 Direction 或带镜头补偿的 WorldDirection,然后进行必要的轴映射。下节说明固定二维正交相机的转换方式。
输入坐标与镜头补偿
Direction 保留键盘/摇杆的屏幕输入约定:X 向右、Y 向下。W 与摇杆上推对应 (0, -1),S 与下推对应 (0, 1);A 向左、D 向右。WorldDirection 则按默认镜头偏移旋转该向量。
UI 屏幕位置通常从左上角开始,但这里的 Direction 不是屏幕位置:摇杆先计算相对于自身中心的偏移,再转换为方向;(0,0) 表示居中或停止。不能用地图高度或 UI 原点对移动方向做平移。
TMX 文件中的像素坐标同样是左上原点、Y 向下。GameGraph 加载正交地图后,提供左下原点、Y 向上的地图局部坐标;瓦片行列索引仍保留左上开始的行序。直接把原始输入加到加载后的位置会造成 W/S 反向,原因是 Y 正方向不同。完整对照、位置转换与方向转换见 TMX 文件、地图与输入坐标。
默认镜头补偿按以下顺序取方位:
- 优先读取主渲染视口相机的正前方向在 XY 平面的投影,求方位角后加
90°。 - 投影不能确定方位时,回退到 SCE 游戏镜头的
Yaw + 90°。 - 无法读取镜头或发生异常时,使用
0°偏移。
设置 UseMainUnit = false 和 BindUnit = null 只阻止发送单位移动指令;输入事件仍然提供带镜头补偿的 WorldDirection。移动期间镜头方位变化,也会触发 Changed 事件。
| 使用场景 | 方向选择 |
|---|---|
| 常规 SCE 单位或按镜头方位操控的世界平面目标 | WorldDirection,再按项目坐标约定映射 |
| 沿 Z 轴观察 XY 平面,画面右方为 +X、上方为 +Y 的固定正交相机 | Direction,映射为 (X, -Y) |
| 使用 Y 向下屏幕坐标的 Canvas/UI 对象 | Direction,保持屏幕坐标约定 |
| 旋转或采用其它平面的二维相机 | 根据实际相机的屏幕右/上方向转换原始输入 |
TmxMapSample 的固定相机沿 Z 轴观察,正前方向的 XY 投影为零。默认补偿会回退到 SCE 游戏镜头,不能据此保证该二维视图的上下左右正确。样例使用原始输入并翻转 Y:
这里的“镜头补偿”是输入方向计算,不是切换渲染相机。TMX 样例使用通用 CameraComponent 的正交投影显示二维地图;正交/透视与是否能从正前方向的 XY 投影求方位是两个独立条件。相机关系见TMX 地图、场景与相机。
csharp
private void OnMoveInput(object? sender, MoveInputEventArgs e)
{
moveDirection = e.State == MoveInputState.Stopped
? Vector2.Zero
: new Vector2(e.Direction.X, -e.Direction.Y);
}这一转换针对上述固定相机与世界坐标约定,不是所有二维游戏的通用规则。它只转换输入;TileMapObject2D.Position 等地图坐标已由加载器转换,不能再次翻转 Y。完整样例见 code_sample/src/TmxMapSample/。
MoveInputEventArgs
OnMoveInput 在有无绑定单位时都会触发。参数如下:
| 属性 | 类型 | 说明 |
|---|---|---|
State | MoveInputState | Started、Changed、Stopped |
Angle | Angle? | 原始输入角度;停止时为 null |
WorldAngle | Angle? | 应用相机角度偏移后的角度;停止时为 null |
Direction | Vector2 | 未应用镜头补偿的屏幕输入方向,X 向右、Y 向下;停止时为 Vector2.Zero |
WorldDirection | Vector2 | 应用相机角度偏移后的方向;停止时为 Vector2.Zero |
Started 通常对应一次移动输入开始,Changed 对应方向变化或相机角度变化,Stopped 对应键盘/摇杆释放。
扩展方法
MoveControlExtensions 可从平台自适应根控件递归找到实际移动控件:
csharp
MoveControlBase? GetMoveControl(this Control control);
bool TryGetMoveControl(this Control control, out MoveControlBase? moveControl);
bool SetUseMainUnit(this Control control, bool useMainUnit);
bool BindMoveUnit(this Control control, Unit? unit);这些扩展适合 GSUI.GameDataControl.MoveControl.Data!.CreateControl() 返回的是 PlatformAdaptivePanel 时使用。
数据模板字段
DefaultMoveJoyStick 和 DefaultMoveKeyBoard 都支持:
| 字段 | 说明 |
|---|---|
UseMainUnit | 是否自动绑定本地玩家主控单位 |
StopMoveOnUnitChange | 绑定单位变化时是否停止旧单位移动 |
MoveJoyStick 还支持 DeadBand,用于设置摇杆死区像素范围。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 移动 UI 出现但单位不动 | 服务端没有设置 player.MainUnit,或单位不可行走 | 设置主控单位并确认单位有移动能力 |
| 纯 GameGraph 项目没有 Unit | 使用了默认单位控制模式 | 设置 UseMainUnit = false,只订阅 OnMoveInput |
| PC 可输入,手机无摇杆或反之 | 直接创建了单个平台控件 | 优先创建 GSUI.GameDataControl.MoveControl 平台自适应控件 |
| 方向与游戏期望不一致 | 项目相机或坐标约定不同 | 按上文选择 Direction / WorldDirection,并在项目代码中映射轴 |
| 固定 XY 正交视图中 W/S 反向 | 把屏幕 Y 向下直接当成世界 Y 向上 | 使用 Direction 并转换为 (X, -Y);不要翻转已转换的 TMX 地图坐标 |
| 销毁界面后仍收到输入 | 未取消事件订阅或未移除控件 | 在 Dispose 中取消 OnMoveInput 并移除/释放控件 |