主题
指令系统(Order System)
指令系统管理玩家操作和 AI 行为,将移动、攻击、施法、物品使用等操作封装为指令对象,在服务端权威执行。
完整阶段流程、目标快照、重新接近及动画表现的配合,见指令、施法与技能动画的完整生命周期。
系统概述
客户端/AI输入 → Command(指令) → Order(命令) → OrderQueue(队列) → 执行- 服务端权威 — 所有指令在服务端验证和执行
- 权限控制 — 客户端只能操作自己的单位
- 队列管理 — 支持排队和抢占
- 可打断性 — 支持取消和覆盖
核心组件
Command(指令)
指令是用户意图的抽象表示:
csharp
public record struct Command
{
public IGameLink<GameDataAbility>? AbilityLink { get; set; } // 技能链接
public CommandIndex Index { get; set; } // 指令类型
public ComponentTag Type { get; set; } // 处理组件
public CommandFlag Flag { get; set; } // 指令标志
public ICommandTarget? Target { get; set; } // 目标
public Item? Item { get; set; } // 物品
public Player? Player { get; set; } // 发起玩家
public bool IsRequest { get; }
public bool IsAcquired { get; }
public CmdResult<Order> IssueOrder(Entity entity, bool testOnly = false); // 服务端
public CmdResult IssueOrder(Entity entity, bool testOnly = false); // 客户端
}Order(命令)
命令是指令的执行实例:
csharp
public abstract class Order : IDisposable
{
public Command Command { get; }
public Entity Owner { get; }
public OrderStage Stage { get; }
public OrderState State { get; }
public bool IsUnInterruptible { get; }
public bool IsAutoPreemptible { get; }
public void Cancel();
public void Destroy();
protected abstract CmdResult ExecuteEffect();
}OrderQueue(指令队列)
csharp
public class OrderQueue : TagComponent
{
public bool IsPaused { get; }
public bool IsEmpty { get; }
public bool HasSingleOrder { get; }
public void Add(Order order, CommandFlag flag);
public Order? Peek();
public void Pause();
public void Resume();
public IEnumerable<Order> GetOrders(Func<Order, bool> filter);
}权限控制
服务端权限
服务端逻辑对任何单位有完全控制权,不受客户端权限规则约束。可通过设置 Player 和 Flag = CommandFlag.IsRequest 模拟玩家请求,使指令受权限限制。
客户端权限限制
- 只能操作属于当前玩家或被共享控制权的单位
- 无法操作拥有
Uncommandable状态的单位 - 目标不能是不可见的单位
- 不能使用隐藏技能
csharp
#if SERVER
// 服务端直接下达指令(无权限限制)
var command = new Command
{
AbilityLink = myAbilityLink,
Index = CommandIndex.Execute,
Type = ComponentTagEx.AbilityManager,
Target = enemyUnit,
Flag = CommandFlag.None
};
// 模拟玩家请求(受权限限制)
var playerCommand = new Command
{
AbilityLink = myAbilityLink,
Index = CommandIndex.Execute,
Type = ComponentTagEx.AbilityManager,
Target = enemyUnit,
Player = specificPlayer,
Flag = CommandFlag.IsRequest
};
#endif指令类型
技能指令
csharp
#if SERVER
// 基本技能释放
var command = new Command
{
AbilityLink = myAbilityLink,
Index = CommandIndex.Execute,
Type = ComponentTagEx.AbilityManager,
Target = enemyUnit,
Player = player,
Flag = CommandFlag.IsUser
};
var result = command.IssueOrder(casterUnit);
if (result.IsSuccess)
{
Game.Logger.LogInformation("技能指令已排队: {Ability}", command.AbilityLink);
}
// 切换型技能
var toggleOn = new Command
{
AbilityLink = myToggleAbilityLink,
Index = CommandIndex.TurnOn, // 或 TurnOff、Toggle(智能切换)
Type = ComponentTagEx.AbilityManager,
Player = player,
Flag = CommandFlag.IsUser
};
#endif攻击指令
攻击指令是特殊的技能指令,自动查找标记了 IsAttack = true 的技能。
技能必须在 GameDataAbilityExecute 中设置 IsAttack = true 才能被攻击指令使用。
查找优先级:
- 如果指定了
AbilityLink,检查该技能的IsAttack标志 - 否则遍历单位所有技能,选择第一个
IsAttack && IsValid && IsEnabled的技能
csharp
#if SERVER
// 攻击指令 — 自动查找攻击技能,无需指定 AbilityLink
var attackCommand = new Command
{
Index = CommandIndex.Attack,
Type = ComponentTagEx.AbilityManager,
Target = enemyUnit,
Player = player,
Flag = CommandFlag.IsUser
};
var result = attackCommand.IssueOrder(attackerUnit);
// 也可以指定特定攻击技能
var specificAttack = new Command
{
Index = CommandIndex.Attack,
Type = ComponentTagEx.AbilityManager,
Target = enemyUnit,
AbilityLink = myMeleeAttackLink,
Player = player,
Flag = CommandFlag.IsUser
};
#endif| 指令类型 | AbilityLink | 技能查找方式 | 适用场景 |
|---|---|---|---|
| Attack 指令 | 可选 | 自动查找 IsAttack=true 的技能 | 通用攻击、AI 攻击 |
| Execute 指令 | 必填 | 使用指定的技能 | 特定技能释放 |
常见问题:给单位添加技能后 Attack 指令无反应 — 原因是技能的 IsAttack 标志未设置。在 GameDataAbilityExecute 中配置 AbilityExecuteFlags = new AbilityExecuteFlags { IsAttack = true }。
移动指令
移动指令受指令队列限制,不能与技能释放同时进行(瞬发 Transient 技能除外)。
> 如需在施法期间移动,使用 Unit.StartJoystickMove() / Unit.PathTo() 等直接移动方法绕过指令队列,详见 单位系统 — 直接移动方法。
csharp
#if SERVER
// 点到点移动
var moveCommand = new Command
{
Index = CommandIndex.Move,
Type = ComponentTag.Walkable,
Target = targetPosition, // ScenePoint 或 Entity
Player = player,
Flag = CommandFlag.IsUser
};
moveCommand.IssueOrder(unit);
// 摇杆方向移动
var vectorMove = new Command
{
Index = CommandIndex.VectorMove,
Type = ComponentTag.Walkable,
Target = new Angle(45), // 移动方向(度数)
Player = player,
Flag = CommandFlag.IsUser
};
vectorMove.IssueOrder(unit);
// 停止摇杆移动
var stopMove = new Command
{
Index = CommandIndex.VectorMoveStop,
Type = ComponentTag.Walkable,
Player = player,
Flag = CommandFlag.IsUser
};
stopMove.IssueOrder(unit);
#endif移动方法对比
| 方法类型 | 指令队列影响 | 技能期间可用 | 适用场景 |
|---|---|---|---|
Move 指令 | 受限制 | 不可用* | 标准单位移动 |
VectorMove 指令 | 受限制 | 不可用* | 玩家摇杆控制 |
Unit.StartJoystickMove() | 不受限制 | 可用 | 技能期间移动、AI 灵活控制 |
Unit.PathTo() | 不受限制 | 可用 | 直接寻路、AI 控制 |
> *瞬发(Transient)技能期间可以正常使用指令系统移动
物品指令
物品指令通过 ComponentTagEx.InventoryManager 处理:
csharp
#if SERVER
// 拾取物品
var pickup = new Command
{
Index = CommandIndexInventory.PickUp,
Type = ComponentTagEx.InventoryManager,
Target = droppedItem,
Player = player,
Flag = CommandFlag.IsUser
};
pickup.IssueOrder(unit);
// 使用物品
var use = new Command
{
Index = CommandIndexInventory.Use,
Type = ComponentTagEx.InventoryManager,
Item = potionItem,
Player = player,
Flag = CommandFlag.IsUser
};
use.IssueOrder(unit);
// 交换/丢弃/给予
// Index 分别为 CommandIndexInventory.Swap / .Drop / .Give
#endif停止指令
csharp
#if SERVER
var stop = new Command
{
Index = CommandIndex.Stop,
Player = player,
Flag = CommandFlag.IsUser
};
stop.IssueOrder(unit);
#endif指令生命周期
指令阶段(OrderStage)
普通排队指令的完整阶段为:
WaitToQueue → Queued → Approach → WaitForCooldown → Preswing → Cast → Channel → Backswing → Left
分别对应待入队、排队、接近、等待冷却、前摇、出手、引导、后摇和离开。普通移动复用这条管线,主要耗时在接近阶段;瞬态指令走专用的快速执行路径,不经历常规前摇等等待。
正常可执行技能在最终检查、扣费成功后进入 Channel,此时发动主 Effect。启用 Reapproachable 后可在完成 Cast、进入 Channel 前回到 Approach。各阶段可能被取消,Left 是统一清理阶段,不代表执行成功。详见完整生命周期指南。
指令状态(OrderState)
| 状态 | 说明 |
|---|---|
Normal | 尚未结束,包括排队、执行和重新接近 |
Canceled | 已取消 |
Aborted | 调用 Cancel() 时处于引导阶段,施法中止 |
Completed | 正常完成,或在后摇阶段调用 Cancel() |
OrderStage 表示阶段,OrderState 表示结果。队列覆盖或直接销毁不完全等同于调用 Cancel(),应读取最终状态。
指令队列管理
队列模式
csharp
#if SERVER
// 请求优先执行(仍受当前阶段的打断规则约束)
var urgent = new Command
{
AbilityLink = healAbilityLink,
Index = CommandIndex.Execute,
Flag = CommandFlag.IsUser | CommandFlag.Preempt,
Player = player
};
// 排队执行(等待当前指令完成)
var queued = new Command
{
AbilityLink = myAbilityLink,
Index = CommandIndex.Execute,
Flag = CommandFlag.IsUser | CommandFlag.Queued,
Player = player
};
// 暂停/恢复队列
var queue = unit.GetComponent<OrderQueue>();
queue?.Pause();
queue?.Resume();
#endifPause() 暂停队首调度,不冻结已经开始的移动、施法等待或游戏时间。
摇杆控件
WasiCore 提供三种摇杆控件(客户端 UI),可与移动指令配合使用:
| 类型 | 说明 |
|---|---|
JoystickNormal | 固定位置的传统摇杆 |
JoystickFloat | 在触摸位置动态出现 |
JoystickDynamic | 中心点跟随手指移动 |
csharp
#if CLIENT
var joystick = new JoystickNormal()
{
Radius = 80f,
KnobSize = 30f,
IsEnabled = true
};
joystick.ValueChanged += (sender, e) =>
{
var input = e.NewValue;
if (input.Length() < 0.1f)
{
// 死区内,停止移动
var stop = new Command
{
Index = CommandIndex.VectorMoveStop,
Type = ComponentTag.Walkable,
Player = Player.LocalPlayer,
Flag = CommandFlag.IsUser
};
stop.IssueOrder(controlledUnit);
return;
}
var angle = Math.Atan2(input.Y, input.X) * (180.0 / Math.PI);
var move = new Command
{
Index = CommandIndex.VectorMove,
Type = ComponentTag.Walkable,
Target = new Angle((float)angle),
Player = Player.LocalPlayer,
Flag = CommandFlag.IsUser
};
move.IssueOrder(controlledUnit);
};
#endif使用示例
AI 行为控制
csharp
#if SERVER
public class SimpleAIController
{
private Unit aiUnit;
public SimpleAIController(Unit unit) => aiUnit = unit;
public async Task RunAI()
{
while (aiUnit.IsValid && aiUnit.IsAlive)
{
var target = FindNearestEnemy();
if (target != null && target.IsAlive)
{
var attack = new Command
{
Index = CommandIndex.Attack,
Type = ComponentTagEx.AbilityManager,
Target = target,
Flag = CommandFlag.IsAI
};
var result = attack.IssueOrder(aiUnit);
if (!result.IsSuccess)
{
var move = new Command
{
Index = CommandIndex.Move,
Type = ComponentTag.Walkable,
Target = target,
Flag = CommandFlag.IsAI
};
move.IssueOrder(aiUnit);
}
}
await Game.Delay(TimeSpan.FromSeconds(1));
}
}
private Unit? FindNearestEnemy() => null; // 实现寻敌逻辑
}
#endifCommandIndex 枚举
csharp
public enum CommandIndex
{
Execute, // 执行技能
TurnOn, // 开启切换技能
TurnOff, // 关闭切换技能
Toggle, // 智能切换
Attack, // 攻击(自动查找 IsAttack 技能)
Stop, // 停止
VectorMove, // 方向移动
VectorMoveStop, // 停止方向移动
Move, // 移动到目标
}CommandFlag 标志
csharp
[Flags]
public enum CommandFlag
{
None = 0,
Queued = 1, // 排队执行
Preempt = 1 << 1, // 抢占执行
IsAcquired = 1 << 2, // 自动获得
IsAuto = (1 << 3) | IsAcquired, // 自动施法
IsRequest = 1 << 4, // 请求指令(受权限限制)
IsUser = (1 << 5) | IsRequest, // 用户请求
IsAI = (1 << 6) | IsRequest, // AI 请求
IsRecast = (1 << 7) | IsAcquired,
DoRecast = 1 << 8,
WasDoRecastAlive = (1 << 9) | IsRecast,
}AbilityExecuteFlags
csharp
public class AbilityExecuteFlags
{
public bool IsAttack { get; set; } // 标记为攻击技能
public bool AlwaysAcquireTarget { get; set; } // 无目标技能是否总是获取目标
public bool InfiniteCasting { get; set; } // 可无限施法(开发中)
public bool Reapproachable { get; set; } // 进入引导前出射程时重新接近
}重新接近目标
设置 AbilityExecuteFlags.Reapproachable = true 后,技能在完成前摇和出手、进入 Channel 前再次检查射程。目标超出射程时,同一条 Order 回到 Approach,接近后重新执行完整的 Preswing 和 Cast。关闭此标志时沿用原有施法行为。
- 重试期间不扣资源、不开始冷却,也不发动技能效果;仅最终进入
Channel时执行这些操作。进入Channel后不再回跳。 - 回到
Approach时停止本轮施法动画并清理仍受阶段生命周期管理的表现,重新起手时再次创建。普通瞬态表现仍按自己的生命周期结束。 - 每次重试都会重新发送
Preswing、Cast阶段事件。自定义阶段回调已经产生的游戏逻辑不会自动撤销,需要按可能重复起手来编写。 - 点、向量技能以实体辅助瞄准时,同一条 Order 始终保留第一次进入
Preswing时锁定的瞄准快照;Unit、UnitOrGround的实体目标和交互宿主继续实时追踪。新指令会重新确定快照。 - 无法接近、目标失效或其它执行条件不满足时取消指令。重新接近期间使用
Approach的打断规则,AI 和玩家仍可下达新的指令。