Skip to content

指令系统(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 才能被攻击指令使用。

查找优先级:

  1. 如果指定了 AbilityLink,检查该技能的 IsAttack 标志
  2. 否则遍历单位所有技能,选择第一个 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();
#endif

Pause() 暂停队首调度,不冻结已经开始的移动、施法等待或游戏时间。

摇杆控件 ​

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; // 实现寻敌逻辑
}
#endif

CommandIndex 枚举 ​

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 和玩家仍可下达新的指令。

相关文档 ​