Article

Unity 入门实战 04:战斗输入与技能释放(把 C# 技能系统接入真实场景)

路线阶段:Unity 入门实战第 4 章。
本章目标:让玩家在场景里稳定完成“选目标 -> 施法 -> 结算 -> 反馈”完整链路。

学习目标

完成本章后,你应该能做到:

  1. 将键位输入转换为技能释放命令,而不是直接写死逻辑。
  2. 构建目标锁定与校验流程,避免“看起来放了,实际没生效”。
  3. 把施法结果反馈到 UI(冷却、失败原因、命中提示)。
  4. 保持输入、施法、结算三层解耦,便于后续接 WebGL 与 AI。

架构目标

本章链路固定为:

  1. CombatInputSampler 采样按键
  2. SkillCastController 生成 CastSkillCommand
  3. CommandBus 执行命令
  4. SkillComponent 完成校验与施法
  5. DamagePipeline 结算
  6. CombatHud 刷新反馈

输入映射(Q/W/E/R)

public struct SkillInput
{
    public bool CastQ;
    public bool CastW;
    public bool CastE;
    public bool CastR;
    public bool LockTarget;
}

public sealed class CombatInputSampler
{
    public SkillInput Sample()
    {
        SkillInput input;
        input.CastQ = Input.GetKeyDown(KeyCode.Q);
        input.CastW = Input.GetKeyDown(KeyCode.W);
        input.CastE = Input.GetKeyDown(KeyCode.E);
        input.CastR = Input.GetKeyDown(KeyCode.R);
        input.LockTarget = Input.GetMouseButtonDown(1);
        return input;
    }
}

目标锁定系统

public sealed class TargetLockSystem
{
    private readonly Camera _camera;
    private readonly LayerMask _enemyMask;
    private Actor _current;

    public TargetLockSystem(Camera camera, LayerMask enemyMask)
    {
        _camera = camera;
        _enemyMask = enemyMask;
    }

    public Actor CurrentTarget
    {
        get { return _current; }
    }

    public void TryLock(GameWorld world)
    {
        var ray = _camera.ScreenPointToRay(new Vector3(Screen.width * 0.5f, Screen.height * 0.5f, 0f));
        RaycastHit hit;
        if (!Physics.Raycast(ray, out hit, 30f, _enemyMask))
        {
            return;
        }

        var actorView = hit.collider.GetComponent<ActorView>();
        if (actorView == null)
        {
            return;
        }

        var actor = world.GetActor(actorView.ActorId);
        if (actor == null || actor.Hp <= 0f)
        {
            return;
        }

        _current = actor;
        FoundationLog.Info("Target", "locked id=" + actor.Id);
    }

    public void Validate()
    {
        if (_current == null)
        {
            return;
        }

        if (_current.Hp <= 0f)
        {
            _current = null;
        }
    }
}

施法控制器(命令驱动)

public sealed class SkillCastController : IUpdatable
{
    public int Order { get { return 120; } }

    private readonly CombatInputSampler _input;
    private readonly TargetLockSystem _targetLock;
    private readonly CommandBus _commandBus;
    private readonly CommandFactory _factory;
    private readonly GameWorld _world;

    public SkillCastController(
        CombatInputSampler input,
        TargetLockSystem targetLock,
        CommandBus commandBus,
        CommandFactory factory,
        GameWorld world)
    {
        _input = input;
        _targetLock = targetLock;
        _commandBus = commandBus;
        _factory = factory;
        _world = world;
    }

    public void Tick(float dt, float unscaledDt)
    {
        _targetLock.Validate();

        var sampled = _input.Sample();

        if (sampled.LockTarget)
        {
            _targetLock.TryLock(_world);
        }

        var target = _targetLock.CurrentTarget;
        var targetId = target == null ? 0 : target.Id;

        if (sampled.CastQ)
        {
            EnqueueCast(1001, targetId);
        }

        if (sampled.CastW)
        {
            EnqueueCast(1002, targetId);
        }

        if (sampled.CastE)
        {
            EnqueueCast(1003, targetId);
        }

        if (sampled.CastR)
        {
            EnqueueCast(1004, targetId);
        }
    }

    private void EnqueueCast(int skillId, int targetId)
    {
        var cmd = _factory.GetCastSkill(skillId, targetId);
        _commandBus.Enqueue(cmd, _world.Time.Now);
    }
}

CastSkillCommand(Unity 场景落地版)

public sealed class CastSkillCommand : ICommand
{
    public int Sequence { get; set; }
    public float Timestamp { get; set; }
    public string Name { get { return "CastSkill"; } }

    public int SkillId;
    public int TargetId;

    public bool Execute(CommandContext context)
    {
        var world = (GameWorld)context.Host;
        var caster = world.Player;
        var target = TargetId <= 0 ? null : world.GetActor(TargetId);

        string reason;
        var ok = caster.Skills.TryCast(caster, target, SkillId, Timestamp, out reason);

        if (!ok)
        {
            world.EventBus.Publish("SkillCastRejected", reason);
            FoundationLog.Warn("Skill", "cast reject skill=" + SkillId + " reason=" + reason);
        }
        else
        {
            world.EventBus.Publish("SkillCastAccepted", SkillId);
        }

        return ok;
    }

    public void Reset()
    {
        Sequence = 0;
        Timestamp = 0f;
        SkillId = 0;
        TargetId = 0;
    }
}

冷却与反馈 UI

public sealed class CombatHud : MonoBehaviour
{
    [SerializeField] private SkillSlotView _q;
    [SerializeField] private SkillSlotView _w;
    [SerializeField] private SkillSlotView _e;
    [SerializeField] private SkillSlotView _r;
    [SerializeField] private TMPro.TextMeshProUGUI _message;

    private SkillComponent _skills;

    public void Bind(SkillComponent skills, EventBus eventBus)
    {
        _skills = skills;

        eventBus.Subscribe("SkillCastRejected", OnCastRejected);
        eventBus.Subscribe("SkillCastAccepted", OnCastAccepted);
    }

    private void Update()
    {
        if (_skills == null)
        {
            return;
        }

        _q.SetCooldown(_skills.GetCooldownRemain(1001));
        _w.SetCooldown(_skills.GetCooldownRemain(1002));
        _e.SetCooldown(_skills.GetCooldownRemain(1003));
        _r.SetCooldown(_skills.GetCooldownRemain(1004));
    }

    private void OnCastRejected(object payload)
    {
        _message.text = "施法失败:" + payload;
    }

    private void OnCastAccepted(object payload)
    {
        _message.text = "施法成功";
    }
}

与前面系统联动

  1. 命令系统:所有施法都走命令队列,可回放可排障。
  2. 技能系统:统一处理冷却/蓝耗/距离,不在输入层重复写规则。
  3. 伤害管线:技能生效后交给结算链,保证公式统一。
  4. 仇恨系统:伤害结果反哺仇恨增长。
  5. 状态机Hit/Dead 状态可直接禁用施法输入。

WebGL 兼容要点

  1. 输入采样与施法执行解耦,方便接移动端虚拟按钮。
  2. HUD 更新避免每帧字符串拼接,失败消息可做节流。
  3. 技能图标资源统一预加载,避免 WebGL 首次施法卡顿。

验收清单

  1. Q/W/E/R 可稳定释放对应技能。
  2. 无目标或超距时给出明确失败原因。
  3. 冷却 UI 与逻辑层时间一致。
  4. 回放同一输入序列,技能命中与失败结果一致。

常见坑

坑 1:输入层直接扣蓝和改冷却

会出现 UI 成功但逻辑失败。所有规则必须在技能层。

坑 2:目标锁定不校验死亡状态

会导致对无效目标施法失败率异常高。

坑 3:命令执行异常未反馈到 UI

玩家只看到“按了没反应”。至少要给失败原因文本或图标闪烁。

本月作业

实现“智能施法提示”功能:

  1. 技能可释放时边框高亮,不可释放时显示原因图标(蓝量/冷却/超距)。
  2. 锁定目标超距时在地面显示范围圈。
  3. 回放模式下 HUD 同步重现释放与失败提示。

下一章进入 Unity 入门实战 05:敌人生成与波次系统(支撑可持续战斗场景)。