U
章 02角色与战斗9 课 — 点击打开
06 / 09 · 12 分钟
角色与战斗

装备与属性

到目前为止的项目

Assets/

Scripts/

CharacterStats.csSkillCaster.csArtifactData.csEquipment.csfinal* stats

Data/

Artifact_ assets

你已经有的这一课新增

骑士有三个技能,但数值仍是开局那套。神器是一件装备上就能让这些数字变大的道具。

Crown of Ember:攻击 +34。戴上,攻击从 40 变 74。摘下,回到 40。整个功能就这样。

写代码之前,先说陷阱

最直观的写法是直接往角色的 attack 上加。第一次能跑,之后就悄悄地出毛病。

✖ Add and subtract

戴上时 attack += 34,摘下时 attack -= 34。只要漏掉一次减法 —— 换装、死亡、重载场景 —— 骑士就会永久变强。

✓ Never touch the base

让 attack = 40 永远保持不动。每次换装备 就把旧总值扔掉,从头加一遍。
动态图解

盯着虚线看:进度条的基础那部分从不动。出现又消失的只有琥珀色那部分。

每个属性有两个数值

所以 CharacterStats 需要多一套字段。基础那套在 Inspector 里填好后就不再改;最终那套由游戏每次重算,也是实际用上的。

add to CharacterStats.cs
[Header("Base — you set these, nothing else may")]
public float attack = 40f;
public float defense = 30f;
public float moveSpeed = 5f;
public float maxHealth = 200f;

[Header("Final — recalculated, use these in gameplay")]
public float finalAttack;
public float finalDefense;
public float finalSpeed;
public float finalMaxHp;

神器不过是一串加成列表

一个 asset,一个列表。Crown of Ember 里有三行:攻击 +34、防御 +18、速度 +12。文件里没有任何东西知道谁会戴它。

ArtifactData.cs
using UnityEngine;

public enum StatType { Attack, Defense, MoveSpeed, MaxHealth }
public enum ArtifactSlot { Crown, Chest, Boots, Ring }

[System.Serializable]
public struct StatBonus
{
    public StatType stat;
    public float flat;      // +34       
    public float percent;
}

[CreateAssetMenu(fileName = "Artifact_", menuName = "Game/Artifact")]
public class ArtifactData : ScriptableObject
{
    public string displayName = "Crown of Ember";
    public Sprite icon;
    [Range(1, 5)] public int rarity = 4;

    public ArtifactSlot slot = ArtifactSlot.Crown;   // one item per slot
    public StatBonus[] bonuses;
}
  • struct 上的 [System.Serializable] 才能让加成行在 Inspector 里编辑。没有它,数组显示为空。

  • 槽位决定了你戴不了两顶王冠。每槽一件、共四个槽 —— 背包规则就这么多。

  • 故意把 flat 和 percent 分开 —— 下一节解释为什么顺序很重要。

重算:推倒,重新累加

这是唯一能写 final 字段的方法,而且它每次都从基础值开始算。没有累加,就不会跑偏。

Equipment.cs — put it on the Knight
using System.Collections.Generic;
using UnityEngine;

public class Equipment : MonoBehaviour
{
    [SerializeField] private CharacterStats stats;

    private readonly Dictionary<ArtifactSlot, ArtifactData> worn = new();

    public System.Action OnChanged;     // the UI listens

    public void Equip(ArtifactData a)
    {
        worn[a.slot] = a;               // replaces whatever was in that slot
        Recalculate();
    }

    public void Unequip(ArtifactSlot slot)
    {
        worn.Remove(slot);
        Recalculate();
    }

    void Recalculate()
    {
        // one bucket per stat
        int n = System.Enum.GetValues(typeof(StatType)).Length;
        float[] flat = new float[n];
        float[] pct  = new float[n];

        foreach (ArtifactData a in worn.Values)
            foreach (StatBonus b in a.bonuses)
            {
                flat[(int)b.stat] += b.flat;
                pct[(int)b.stat]  += b.percent;
            }

        // base + all the flat, THEN multiply by the percent
        stats.finalAttack  = Apply(stats.attack,    flat, pct, StatType.Attack);
        stats.finalDefense = Apply(stats.defense,   flat, pct, StatType.Defense);
        stats.finalSpeed   = Apply(stats.moveSpeed, flat, pct, StatType.MoveSpeed);
        stats.finalMaxHp   = Apply(stats.maxHealth, flat, pct, StatType.MaxHealth);

        OnChanged?.Invoke();
    }

    static float Apply(float baseValue, float[] flat, float[] pct, StatType s)
        => (baseValue + flat[(int)s]) * (1f + pct[(int)s] / 100f);
}

为什么先算 flat、后算 percent

基础 40,一件 +34 flat、一件 +15%,两种顺序给出两个结果 —— 对得上 tooltip 承诺数字的只有一个。

C# 脚本
// flat first
// percent first

动手试试

  1. 1

    通过 Assets▸Create▸Game▸Artifact 创建两个 artifact:Crown of Ember(+34 flat Attack)和 Ring of Haste(+15% Attack,+12 flat MoveSpeed)。

  2. 2

    给 Knight 挂上 Equipment,并把它的 CharacterStats 拖进引用槽。

  3. 3

    把按键 7 和 8 绑到 Equip 和 Unequip,游戏运行时就能随时开关。

  4. 4

    按 Play,盯着 Inspector 里的 finalAttack:40 → 74 → 85.1,然后一分不差地退回 40。

真正能证明它对的测试

两个 artifact 连续戴上摘下二十次。finalAttack 若不能正好回到 40,就说明有地方在往基础值上写 —— 去把它找出来。

a throwaway test you delete afterwards
[ContextMenu("Stress test equip")]
void StressTest()
{
    float before = stats.finalAttack;

    for (int i = 0; i < 20; i++)
    {
        Equip(crown); Equip(ring);
        Unequip(ArtifactSlot.Crown); Unequip(ArtifactSlot.Ring);
    }

    Debug.Log(before == stats.finalAttack
        ? "OK — no drift"
        : $"DRIFT! {before} → {stats.finalAttack}");
}