前置知识
ScriptableObject
ScriptableObject是一个可独立于类实例来保存大量数据的数据容器
。ScriptableObject的一个主要用例就是通过避免重复值来减少项目的内存使用量。如果使用Editor,可以在编辑时和运行时将数据保存到ScriptableObject
ScriptableObject的主要用例为:
- 在Editor会话期间保存和存储数据
- 将数据保存为项目中的资源,以便在运行时使用
官方教程:https://docs.unity.cn/cn/2019.4/Manual/class-ScriptableObject.html
C#结构体与类
在C#中,结构体是值类型,他使得单一变量可以存储各种数据类型的相关数据。struct 关键字用于创建结构体
类的对象是存储在堆内存中,结构体存储在栈中。对空间大,但访问速度较慢,栈较小,访问速度相对更快。所以,当我们秒速一个轻量级对象的时候,结构体可以提高效率,成本更低。但也要从需求出发,假如我们在传值的时候希望传递的是引用而不是值,就应该使用类了
特点:
- 与类不同,结构不能继承其他的结构或者类
- 结构可以实现一个或者多个接口
- 当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化
- 如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用
代码分类
配置数据
ScriptableItem
定义道具的ScriptableObject脚本
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
[CreateAssetMenu(menuName = "Create Scriptable/General", order = 999)]
public partial class ScriptableItem : ScriptableObject
{
/// <summary>
/// 道具最大堆叠数
/// </summary>
public int maxStack;
/// <summary>
/// 道具名
/// </summary>
public string itemName;
/// <summary>
/// 道具编号,不可重复
/// </summary>
public int itemID;
[Tooltip("耐久性仅适用于不可堆叠的项目(如MaxStack为1)")]
public int maxDurability = 0; // disabled by default
public long buyPrice;
public long sellPrice;
public long itemMallPrice;
public bool sellable;
public bool tradable;
public bool destroyable;
[SerializeField, TextArea(1, 30)] protected string toolTip;
public Sprite image;
public virtual string ToolTip()
{
StringBuilder tip = new StringBuilder(toolTip);
tip.Replace("{NAME}", name);
tip.Replace("{ItemName}", itemName);
tip.Replace("{DESTROYABLE}", (destroyable ? "Yes" : "No"));
tip.Replace("{SELLABLE}", (sellable ? "Yes" : "No"));
tip.Replace("{TRADABLE}", (tradable ? "Yes" : "No"));
tip.Replace("{BUYPRICE}", buyPrice.ToString());
tip.Replace("{SELLPRICE}", sellPrice.ToString());
return tip.ToString();
}
}
点击菜单Assets/Create/Create Scriptable/General
,会生成ScriptableObject资源,此脚本就像是一个模板工厂,会创建具有相同属性的资源,然后可以根据需求配置不同的数据,生成的资源如下:
定义数据获取数据的管理器ScriptableItemMgr,此脚本可以将生成的ScriptableObject资源转成Dictionary
,方便其他代码调用
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
public class ScriptableItemMgr
{
static Dictionary<int, ScriptableItem> cache;
public static Dictionary<int, ScriptableItem> dict
{
get
{
if (cache == null)
{
ScriptableItem[] items = Resources.LoadAll<ScriptableItem>("Items");
var duplicates = items.ToList().FindDuplicates(item => item.name);
if (duplicates.Count == 0)
{
cache = items.ToDictionary(item => item.itemID, item => item);
}
else
{
foreach (string duplicate in duplicates)
Debug.LogError("资源文件夹包含多个名为" + duplicate + "的ScriptableItem");
}
}
return cache;
}
}
}
ItemInfo
道具信息结构体,一般在创建道具的时候使用,可以根据此ItemInfo提供的信息来创建对应Item道具,使用方式可以参考PlayerInventory
// 道具信息
[Serializable]
public struct ItemInfo
{
public int id { get; set; }
public string name { get; set; }
public int amount { get; set; }
public int durability { get; set; }
public int index { get; set; }
}
游戏类
Item
是道具条目,Item是对ScriptableItem数据的封装,提供了一些对外的方法
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
[SerializeField]
public partial struct Item
{
public int id;
/// <summary>
/// 耐久
/// </summary>
public int durability;
public Item(ScriptableItem data)
{
id = data.itemID;
durability = data.maxDurability;
}
public ScriptableItem data
{
get
{
if (!ScriptableItem.dict.ContainsKey(id))
throw new KeyNotFoundException("没有hash=" + id + "的ScriptableItem。");
return ScriptableItem.dict[id];
}
}
/// <summary>
/// 耐久百分比
/// </summary>
/// <returns></returns>
public float DurabilityPercent()
{
return (durability != 0 && MaxDurability != 0) ? (float)durability / (float)MaxDurability : 0;
}
/// <summary>
/// 检查耐久度是否有效
/// </summary>
public bool CheckDurability()
{
return MaxDurability == 0 || durability > 0;
}
public string ToolTip()
{
// 执行ScriptableItem资源对象的ToolTip()方法
StringBuilder tip = new StringBuilder(data.ToolTip());
// 只有当物品具有耐久性时才显示耐久性
if (MaxDurability > 0)
tip.Replace("{DURABILITY}", (DurabilityPercent() * 100).ToString("F0"));
return tip.ToString();
}
#region 属性
public string Name => data.name;
/// <summary>
/// 数量的上限
/// </summary>
public int MaxStack => data.maxStack;
public int MaxDurability => data.maxDurability;
public long BuyPrice => data.buyPrice;
public long SellPrice => data.sellPrice;
/// <summary>
/// 商城价格
/// </summary>
public long ItemMallPrice => data.itemMallPrice;
/// <summary>
/// 可出售
/// </summary>
public bool Sellable => data.sellable;
/// <summary>
/// 可交易
/// </summary>
public bool Tradable => data.tradable;
/// <summary>
/// 可销毁
/// </summary>
public bool Destroyable => data.destroyable;
public Sprite Image => data.image;
# endregion
}
ItemSlot
是道具槽,主要负责对当前道具槽内Item道具进行维护和管理。持有Item的引用,并且维护了一个amount
属性,即为当前道具槽内的道具数量
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
[SerializeField]
public partial struct ItemSlot
{
/// <summary>
/// 格子中道具
/// </summary>
public Item item;
/// <summary>
/// 格子中道具的数量
/// </summary>
public int amount;
public ItemSlot(Item item, int amount = 1)
{
this.item = item;
this.amount = amount;
}
/// <summary>
/// 减少数量
/// </summary>
/// <param name="reduceBy"></param>
/// <returns></returns>
public int DecreaseAmount(int reduceBy)
{
int limit = Mathf.Clamp(reduceBy, 0, amount);
amount -= limit;
return limit;
}
/// <summary>
/// 增加数量
/// </summary>
/// <param name="increaseBy"></param>
/// <returns></returns>
public int IncreaseAmount(int increaseBy)
{
int limit = Mathf.Clamp(increaseBy, 0, item.MaxStack - amount);
amount += limit;
return limit;
}
public string ToolTip()
{
if (amount == 0) return "";
// 执行Item属性方法结构体的ToolTip()方法
StringBuilder tip = new StringBuilder(item.ToolTip());
tip.Replace("{AMOUNT}", amount.ToString());
return tip.ToString();
}
}
Inventory
背包管理,维护所有道具槽的正确运行,提供了对道具的增删改功能,方便道具能够正确放到指定道具槽中
using System;
using UnityEngine;
public class Inventory : ItemContainer
{
public List<ItemSlot> slots = new List<ItemSlot>();
public ItemSlot CreateSlot(int itemID, int amount = 1)
{
Item item = new Item(ScriptableItem.dict[itemID]);
return new ItemSlot(item, amount);
}
public Item CreateItem(int itemID)
{
return new Item(ScriptableItem.dict[itemID]);
}
public int FreeSlots()
{
int free = 0;
foreach (ItemSlot slot in slots)
if (slot.amount == 0)
++free;
return free;
}
/// <summary>
/// 计算背包有多少被道具占用的格子
/// </summary>
/// <returns></returns>
public int SlotsOccipied()
{
int occupied = 0;
foreach (ItemSlot slot in slots)
if (slot.amount > 0)
++occupied;
return occupied;
}
// 计算背包中某种道具的总数量
public int Count(Item item)
{
int amount = 0;
foreach (ItemSlot slot in slots)
if (slot.amount > 0 && slot.item.Equals(item))
amount += slot.amount;
return amount;
}
// 从背包中移除指定数量某种道具
public bool Remove(Item item, int amount)
{
for (int i = 0; i < slots.Count; ++i)
{
ItemSlot slot = slots[i];
if (slot.amount > 0 && slot.item.Equals(item))
{
// 从包含某种道具的格子尽量移除此种道具
amount -= slot.DecreaseAmount(amount);
slots[i] = slot;
// 达到指定数量
if (amount == 0) return true;
}
}
// 背包中没有更多此种道具,达不到指定数量
return false;
}
public bool CanAdd(Item item, int amount)
{
//TODO:优化返回索引
for (int i = 0; i < slots.Count; i++)
{
// 当前格子没有物品
if (slots[i].amount == 0)
{
amount -= item.MaxStack;
}
//相同类型的物品是否能够装下
else if (slots[i].item.Equals(item))
{
amount -= (item.MaxStack - slots[i].amount);
}
if (amount <= 0)
return true;
}
return false;
}
public bool Add(Item item, int amount)
{
if (CanAdd(item, amount))
{
// 格子中已有此物品
for (int i = 0; i < slots.Count; i++)
{
if (slots[i].amount > 0 && slots[i].item.Equals(item))
{
ItemSlot temp = slots[i];
amount -= temp.IncreaseAmount(amount);
slots[i] = temp;
}
if (amount < 0) return true;
}
// 未达到指定数量,寻找空格子加入此物品
for (int i = 0; i < slots.Count; i++)
{
if (slots[i].amount == 0)
{
int add = Mathf.Min(amount, item.MaxStack);
slots[i] = new ItemSlot(item, add);
amount -= add;
}
if (amount <= 0) return true;
}
if (amount != 0) Debug.LogError("背包空间不够!添加道具到背包失败: " + item.Name + " " + amount);
}
return false;
}
}
PlayerInventory
负责读取背包数据,并根据数据初始化背包
using UnityEngine;
public class PlayerInventory : Inventory
{
[Header("Components")]
public Player player;
[Header("Inventory")]
public int size = 30;
public KeyCode[] splitKeys = { KeyCode.LeftShift, KeyCode.RightShift };
void Awake()
{
// 测试数据
ItemInfo[] itemsDefault = new ItemInfo[4]{
new ItemInfo{id = 12001,amount = 30},
new ItemInfo{id = 12002,amount = 15},
new ItemInfo{id = 12004,amount = 10},
new ItemInfo{id = 12005,amount = 6}
};
LoadPlayerInventory(itemsDefault);
}
public void LoadPlayerInventory(ItemInfo[] itemInfos)
{
// 创建玩家背包的格子
for (int i = 0; i < player.Inventory.size; i++)
{
player.Inventory.slots.Add(new ItemSlot());
}
print(player.Inventory.slots.Count);
// 添加格子中的道具
for (int i = 0; i < itemInfos.Length; i++)
{
ItemInfo item = itemInfos[i];
if (item.index > 0) // 物品指定了格子位置
{
//用带道具的格子替换指定位置的空格子
Insert(CreateSlot(item.id, item.amount), item.index);
}
else
{
//没有指定位置,将道具添加到下一个空格子
Add(CreateItem(item.id), item.amount);
}
}
}
/// <summary>
/// 添加道具到背包的指定格子
/// </summary>
public void Insert(ItemSlot slot, int index)
{
if (player.Inventory.slots[index].item.id == 0)
player.Inventory.slots[index] = slot;
else
Debug.LogError("背包中有此物品:" + slot.item.Name);
}
}
总结
配置数据
- ScriptableItem
- ScriptableItemMgr
- ItemInfo 道具信息
游戏对象
- Item 道具条目
- ItemSlot 道具槽
- Inventory 背包管理