# 背包系统总结

前言:这一部分并不复杂,所以我将通过功能模块的区分来记录而不是细分每一个文件的功能与彼此之间的逻辑来记录

# 1. 存储模块

ItemData.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum ItemType
{
    Material,
    Equipment
}
[CreateAssetMenu(fileName = "New Item Data", menuName = "Data/Item")]
public class ItemData : ScriptableObject
{
    public ItemType itemType;
    public string itemName;
    public Sprite icon;
    [Range(0,100)]
    public float dropChance;
}

首先创建一个 itemdata 的基类,用来存储一些所有物品都会有的属性,其次在此基础上通过继承来创建一些其他的类,比如装备等

ItemData_Equipment.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

// 装备类型的枚举定义
public enum EquipmentType
{
Weapon, // 武器
Armor, // 防具
Amulet, // 护符
Flask // 药剂
}

// 创建一个新的可创建的脚本资源菜单项
[CreateAssetMenu(fileName = "New Item Data", menuName = "Data/Equipment")]
public class ItemData_Equipment : ItemData
{
// 装备的类型,例如武器、防具等
public EquipmentType equipmentType;

// 物品冷却时间
public float itemCooldown;
// 物品效果数组
public ItemEffect[] itemEffects;

[Header("Major stats")]
public int strength; // 力量
public int agility; // 敏捷
public int intelligence; // 智力
public int vitality; // 体力

[Header("Offensive stats")]
public int damage; // 攻击力
public int critChance; // 暴击几率
public int critPower; // 暴击伤害

[Header("Defensive stats")]
public int health; // 生命值
public int armor; // 护甲值
public int evasion; // 闪避值
public int magicResistance; // 魔法抗性

[Header("Magic stats")]
public int fireDamage; // 火焰伤害
public int iceDamage; // 冰霜伤害
public int lightingDamage; // 闪电伤害

[Header("Craft requirements")]
public List<InventoryItem> craftingMaterials; // 合成所需材料

// 执行物品效果
public void Effect(Transform _enemyPosition)
{
// 遍历所有物品效果并在指定敌人位置执行效果
foreach (var item in itemEffects)
{
item.ExecuteEffect(_enemyPosition);
}
}

// 添加物品的属性修饰符到玩家身上
public void AddModifiers()
{
PlayerStats playerStats = PlayerManager.instance.player.GetComponent<PlayerStats>();

// 添加主要属性的修饰符
playerStats.strength.AddModifier(strength);
playerStats.agility.AddModifier(agility);
playerStats.intelligence.AddModifier(intelligence);
playerStats.vitality.AddModifier(vitality);

// 添加攻击属性的修饰符
playerStats.damage.AddModifier(damage);
playerStats.critChance.AddModifier(critChance);
playerStats.critPower.AddModifier(critPower);

// 添加防御属性的修饰符
playerStats.maxHealth.AddModifier(health);
playerStats.armor.AddModifier(armor);
playerStats.evasion.AddModifier(evasion);
playerStats.magicResistance.AddModifier(magicResistance);

// 添加魔法属性的修饰符
playerStats.fireDamage.AddModifier(fireDamage);
playerStats.iceDamage.AddModifier(iceDamage);
playerStats.lightingDamage.AddModifier(lightingDamage);
}

// 移除物品的属性修饰符
public void RemoveModifiers()
{
PlayerStats playerStats = PlayerManager.instance.player.GetComponent<PlayerStats>();

// 移除主要属性的修饰符
playerStats.strength.RemoveModifier(strength);
playerStats.agility.RemoveModifier(agility);
playerStats.intelligence.RemoveModifier(intelligence);
playerStats.vitality.RemoveModifier(vitality);

// 移除攻击属性的修饰符
playerStats.damage.RemoveModifier(damage);
playerStats.critChance.RemoveModifier(critChance);
playerStats.critPower.RemoveModifier(critPower);

// 移除防御属性的修饰符
playerStats.maxHealth.RemoveModifier(health);
playerStats.armor.RemoveModifier(armor);
playerStats.evasion.RemoveModifier(evasion);
playerStats.magicResistance.RemoveModifier(magicResistance);

// 移除魔法属性的修饰符
playerStats.fireDamage.RemoveModifier(fireDamage);
playerStats.iceDamage.RemoveModifier(iceDamage);
playerStats.lightingDamage.RemoveModifier(lightingDamage);
}
}

在此基础上通过 InventoryItem 来作为某个种类的仓库 (?) 来进行入栈出栈的操作以存储数据

InventoryItem.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
[Serializable]
public class InventoryItem
{
    public ItemData data;
    public int stackSize;
    public InventoryItem(ItemData _newItemData)
    {
        data = _newItemData;
        AddStack();
    }
    public void AddStack() => stackSize++;
    public void RemoveStack() => stackSize--;
}

最后通过 Inventory 来联动仓库与 UI 的变动

# 2. 人物装备模块

主要通过这部分代码来实现装备与仓库之间的联动与单个装备只能装备一个的功能

Inventory.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void EquipItem(ItemData _item)
{
// 装备新物品并替换旧物品
ItemData_Equipment newEquipment = _item as ItemData_Equipment;
InventoryItem newItem = new InventoryItem(newEquipment);

ItemData_Equipment oldEquipment = null;

// 查找相同类型的已装备物品
foreach (KeyValuePair<ItemData_Equipment, InventoryItem> item in equipmentDictionary)
{
if (item.Key.equipmentType == newEquipment.equipmentType)
oldEquipment = item.Key;
}

// 如果存在旧装备,将其卸下并添加回背包
if (oldEquipment != null)
{
UnequipItem(oldEquipment);
AddItem(oldEquipment);
}

// 装备新物品并更新字典
equipment.Add(newItem);
equipmentDictionary.Add(newEquipment, newItem);
newEquipment.AddModifiers();

// 从背包移除新装备
RemoveItem(_item);

// 更新 UI
UpdateSlotUI();
}

UI 当中在仓库点击装备

UI_ItemSlot
1
2
3
4
5
6
7
8
9
10
public virtual void OnPointerDown(PointerEventData eventData)
    {
        if (Input.GetKey(KeyCode.LeftControl))
        {
            Inventory.instance.RemoveItem(item.data);
            return;
        }
        if (item.data.itemType == ItemType.Equipment)
            Inventory.instance.EquipItem(item.data);
    }

在 UI 当中装备栏点击卸下

UI_EquipmentSlot.cs
1
2
3
4
5
6
public override void OnPointerDown(PointerEventData eventData)
    {
        Inventory.instance.UnequipItem(item.data as ItemData_Equipment);
        Inventory.instance.AddItem(item.data as ItemData_Equipment);
        CleanUpSlot();
    }

# 3. 敌人 / 玩家死亡掉落系统

# 敌人掉落

挂载 ItemDrop 方法在敌人身上,控制掉落

ItemDrop.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public virtual void GenerateDrop()
    {
        // 遍历所有可能掉落的物品,按照掉落几率决定是否加入掉落列表
        for (int i = 0; i < possibleDrop.Length; i++)
        {
            if (Random.Range(0, 100) <= possibleDrop[i].dropChance) // 随机生成一个 0 到 100 的值,与掉落几率比较
                dropList.Add(possibleDrop[i]); // 将符合条件的物品加入掉落列表
        }
        // 根据掉落数量上限,从掉落列表中随机选择物品并掉落
        for (int i = 0; i < possibleItemDrop; i++)
        {
            ItemData randomItem = dropList[Random.Range(0, dropList.Count - 1)]; // 随机选择一个物品
            dropList.Remove(randomItem); // 从列表中移除已掉落的物品,避免重复
            DropItem(randomItem); // w调用方法生成掉落物体
        }
    }
    // 创建掉落物体的方法
protected void DropItem(ItemData _itemData)
    {
        // 在当前物体位置生成一个新的掉落物体
        GameObject newDrop = Instantiate(dropPrefab, transform.position, Quaternion.identity);
        // 为掉落物体赋予一个随机的初始速度
        Vector2 randomVelocity = new Vector2(Random.Range(-5, 5), Random.Range(15, 20));
        // 设置掉落物体的属性
        newDrop.GetComponent<ItemObject>().SetupItem(_itemData, randomVelocity);
    }

然后在 EnemyStatus 中敌人死去时下方挂上 DropItem 方法

EnemyStatus
1
2
3
4
5
6
protected override void Die()
    {
        base.Die();
        enemy.Die();
        myDropSystem.GenerateDrop();
    }

# 玩家掉落

玩家掉落这块重写了一个方法,因为玩家的掉落需要考虑是否有装备掉落或是材料掉落

PlayerItemDrop.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class PlayerItemDrop : ItemDrop
{
[Header("Player's drop")]
[SerializeField] private float chanceToLooseItems; // 玩家失去装备的几率
[SerializeField] private float chanceToLooseMaterials; // 玩家失去材料的几率

// 重写生成掉落物品的方法
public override void GenerateDrop()
{
Inventory inventory = Inventory.instance; // 获取玩家的物品管理实例

List<InventoryItem> itemsToUnequip = new List<InventoryItem>(); // 用于存储需要卸下的装备
List<InventoryItem> materialsToLoose = new List<InventoryItem>(); // 用于存储需要丢失的材料

// 遍历玩家的装备列表,根据几率决定是否掉落装备
foreach (InventoryItem item in inventory.GetEquipmentList())
{
if (Random.Range(0, 100) <= chanceToLooseItems)
{
DropItem(item.data); // 掉落该物品
itemsToUnequip.Add(item); // 将物品加入需要卸下的列表
}
}

// 卸下所有被选中的装备
for (int i = 0; i < itemsToUnequip.Count; i++)
{
inventory.UnequipItem(itemsToUnequip[i].data as ItemData_Equipment);
}

// 遍历玩家的仓库列表,根据几率决定是否丢失材料
foreach (InventoryItem item in inventory.GetStashList())
{
if (Random.Range(0, 100) <= chanceToLooseMaterials)
{
DropItem(item.data); // 掉落该材料
materialsToLoose.Add(item); // 将材料加入需要丢失的列表
}
}

// 从玩家仓库中移除所有被选中的材料
for (int i = 0; i < materialsToLoose.Count; i++)
{
inventory.RemoveItem(materialsToLoose[i].data);
}
}
}

# 4. 特殊装备系统

我们需要一些装备拥有一些特殊的性能,比如可以造成电击,抑或是可以恢复血量。
创建一个基类存储方法

ItemEffect.cs
1
2
3
4
5
6
7
public class ItemEffect : ScriptableObject
{
    public virtual void ExecuteEffect(Transform _enemyPosition)
    {
        Debug.Log("Effect executed!");
    }
}

在 Equipment 中创建数组储存 Effect 方法并创建一个方法用于调用这些方法
ItemData_Equipment.cs
1
2
3
4
5
6
7
8
9
10
11
12
// 物品效果数组
    public ItemEffect[] itemEffects;
   
// 执行物品效果
    public void Effect(Transform _enemyPosition)
    {
        // 遍历所有物品效果并在指定敌人位置执行效果
        foreach (var item in itemEffects)
        {
            item.ExecuteEffect(_enemyPosition);
        }
    }

同时在 Inventory 中创建获取指定装备的方法
Inventory.cs
1
2
3
4
5
6
7
8
9
10
11
public ItemData_Equipment GetEquipment(EquipmentType _type)
    {
        // 获取指定类型的装备
        ItemData_Equipment equipedItem = null;
        foreach (KeyValuePair<ItemData_Equipment, InventoryItem> item in equipmentDictionary)
        {
            if (item.Key.equipmentType == _type)
                equipedItem = item.Key;
        }
        return equipedItem;
    }

然后在 PlayerAnimationTriggers 中攻击时调用效果
PlayerAnimationTriggers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void AttackTrigger()
{
// 获取攻击范围内的所有碰撞体
Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius);

// 遍历所有碰撞体
foreach (var hit in colliders)
{
// 检查是否是敌人
if (hit.GetComponent<Enemy>() != null)
{
// 获取敌人的状态组件
EnemyStats _target = hit.GetComponent<EnemyStats>();

// 如果敌人状态存在,对敌人造成伤害
if(_target != null)
player.stats.DoDamage(_target);

// 获取玩家当前装备的武器数据
ItemData_Equipment weaponData = Inventory.instance.GetEquipment(EquipmentType.Weapon);

// 如果有装备武器,执行武器的特殊效果
if (weaponData != null)
weaponData.Effect(_target.transform);
}
}
}

# 5. 一些特殊能力的实现

这边以雷击效果举例,其他效果将不再赘述
将 controller 文件绑定在雷击动画上,并将其作为预制件

ThunderStrike_Controller.cs
1
2
3
4
5
6
7
8
9
10
11
12
public class ThunderStrike_Controller : MonoBehaviour
{
    protected virtual void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.GetComponent<Enemy>() != null)
        {
            PlayerStats playerStats = PlayerManager.instance.player.GetComponent<PlayerStats>();
            EnemyStats enemyTarget = collision.GetComponent<EnemyStats>();
            playerStats.DoMagicalDamage(enemyTarget);
        }
    }
}

然后,将预制件放在 effect 的 data 中,再将 effect 放在武器的 data 中,达成绑定效果。


然后是电击后的自动索敌效果,同理创建了一个 ShockStrike_Controller 控制类来实现逻辑

ShockStrike_Controller.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShockStrike_Controller : MonoBehaviour
{
[SerializeField] private CharacterStats targetStats; // 目标的状态信息
[SerializeField] private float speed; // 移动速度
private int damage; // 造成的伤害值

private Animator anim; // 动画控制器
private bool triggered; // 是否已经触发

void Start()
{
anim = GetComponentInChildren<Animator>(); // 获取子物体中的动画组件
}

// 初始化攻击的伤害值和目标状态
public void Setup(int _damage, CharacterStats _targetStats)
{
damage = _damage; // 设置伤害值
targetStats = _targetStats; // 设置目标状态
}

// 每帧更新
void Update()
{
if (!targetStats) // 如果没有目标,则退出
return;

if (triggered) // 如果已经触发,则退出
return;

// 移动到目标位置
transform.position = Vector2.MoveTowards(transform.position, targetStats.transform.position, speed * Time.deltaTime);
// 调整朝向,指向目标
transform.right = transform.position - targetStats.transform.position;

// 检查是否到达目标
if (Vector2.Distance(transform.position, targetStats.transform.position) < .1f)
{
anim.transform.localPosition = new Vector3(0, .5f); // 调整动画位置
anim.transform.localRotation = Quaternion.identity; // 重置动画旋转

transform.localRotation = Quaternion.identity; // 重置自身旋转
transform.localScale = new Vector3(3, 3); // 放大自身以展现效果

// 延迟执行伤害并销毁自身
Invoke("DamageAndSelfDestroy", .2f);
triggered = true; // 标记为已触发
anim.SetTrigger("Hit"); // 播放击中动画
}
}

// 对目标造成伤害并销毁自身
private void DamageAndSelfDestroy()
{
targetStats.ApplyShock(true); // 触发目标的震荡效果
targetStats.TakeDamage(1); // 对目标造成 1 点伤害
Destroy(gameObject, .4f); // 在 0.4 秒后销毁自身
}
}