Siki_Unity_3-3_背包系统
Posted FudgeBear
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Siki_Unity_3-3_背包系统相关的知识,希望对你有一定的参考价值。
Unity 3-3 背包系统(基于UGUI)
任务1&2&3:演示、介绍、类图分析
背包面板、箱子面板、锻造合成面板、装备佩戴面板、商店面板等
面板的显示和隐藏、保存和加载、拾起物品、物品移动、物品出售和购买等
导入素材UI.unitypackage
UML图设计:
物品Item分为几类:消耗品Consumable、装备Equipment、武器Weapon、材料Material
消耗品影响HP/MP
装备影响strength/ intelligence/ agility/ stamina等
装备类型有:head/ neck/ chest/ ring/ leg/ bracer/ boots/ shoulder/ belt/ offHand
武器影响damage
武器类型有:offHand/ mainHand
材料用于合成装备和武器
物品共有变量:
id/ name/ type/ quality/ description/ capacity/ buyprice/ sellprice
消耗品变量:
hp/ mp
装备变量:
strength/ intelligence/ agility/ stamina等/ 还有equipmentType
武器变量:
damage/ 还有weaponType
材料变量:无
任务5&6:开发Item类(根据类图创建类)
使用get;set;的方式,可以很灵活地控制变量的访问权限
public class Item { public int ID { get; set; } public string Name { get; set; } public ItemType Type { get; set; } public ItemQuality Quality { get; set; } public string Description { get; set; } public int Capacity { get; set; } public int buyprice { get; set; } public int sellprice { get; set; } public Item(int id, string name, ItemType type, ItemQuality quality, string desc, int capacity, int buyprice, int sellprice){ this.ID = id; this.Name = name; ... this.buyprice = buyprice; this.sellprice = sellprice; } public enum ItemType { Consumable, Equipment, Weapon, Material } public enum ItemQuality { Common, Uncommon, Rare, Epic, Legendary, Artifact }}
-- 注意:两个枚举类型ItemType和ItemQuality是在类内部声明的,在外部使用时需要通过类名,比如Item.ItemType来使用
而且声明的时候需要为public的
-- 改进:每个Item都有自己的UI图标
public string SpritePath { get; set; }
并在Project中创建Resources文件夹,将所有Item图标的Sprite移入该文件夹
其他类的构造函数里也得加上spritePath
public class Consumable : Item { public int HP { get; set; } public int MP { get; set; } public Consumable(int id, string name, ItemType type, ItemQuality quality, string desc, int capacity, int buyprice, int sellprice, int hp, int mp) : base(id, name, type, quality, desc, capacity, buyprice, sellprice) { this.HP = hp; this.MP = mp; }}
public class Equipment : Item { public int Strength { get; set; } public int Intelligence { get; set; } public int Agility { get; set; } public int Stamina { get; set; } public EquipmentType EquipType { get; set; } public Equipment(int id, string name, ItemType type, ItemQuality quality, string desc, int capacity, int buyprice, int sellprice, int strength, int intelligence, int agility, int stamina, EquipmentType equipType) : base(id, name, type, quality, desc, capacity, buyprice, sellprice) { this.Strength = strength; this.Intelligence = intelligence; this.Agility = agility; this.Stamina = stamina; this.EquipType = equipType; } public enum EquipmentType { Head, Neck, Chest, Ring, Leg, Bracer, Boots, Shoulder, Belt, OffHand }}
public class Weapon : Item { public int Damage { get; set; } public WeaponType WeapType { get; set; } public Weapon(int id, string name, ItemType type, ItemQuality quality, string desc, int capacity, int buyprice, int sellprice, int damage, WeaponType weapType) : base(id, name, type, quality, desc, capacity, buyprice, sellprice) { this.Damage = damage; this.WeapType = weapType; } public enum WeaponType { OffHand, MainHand }}
-- 注意,这里因为Weapon不是继承与Equipment,因此这里使用的EquipmentType需要写成Equipment.EquipmentType
public class Material : Item { public Material(int id, string name, ItemType type, ItemQuality quality, string desc, int capacity, int buyprice, int sellprice) : base(id, name, type, quality, desc, capacity, buyprice, sellprice) { }}
-- 因为子类必须提供一个构造方法去构造父类,而父类没有空的构造方法,所以Material必须写对应的构造方法去构造父类
否则需要在Item中写一个空的构造方法
任务7:Item类的Json文件 -- 策划
https://www.bejson.com/jsoneditoronline -- 在线Json编辑器
有很多种物品,在Json文件中保存成一个数组
属性根据类中成员变量来确定
[ { "id": 1, "name": "血瓶", "type": "Consumable", "quality": "Common", "description": "这个是用来加血的", "capacity": 10, "buyprice": 10, "sellprice": 5, "hp": 10, "mp": 0, "spritePath": "Sprites/Items/hp" } ]
暂时先写一个物品,用于测试
在Project->Items下保存一个记事本Items.Json文件,编码格式改为UTF-8
任务8:InventoryManager物品管理器
&& 任务14:改进Knapsack和Chest的设计
创建空物体InventoryManager,添加脚本InventoryManager.cs -- 用于管理所有物品
之后还有两个分管理器:背包Knapsack,箱子Chest
Knapsack和Chest不是继承于InventoryManager的,只是功能结构关系而已
背包和箱子之间有一些交互,比如移动物品等,这些交互方法就在InventoryManager中实现
注意:InventoryManager和这些一般都为单例模式
InventoryManager.cs中
单例模式的实现
1. _instance为private,因为不能在外界访问
2. Instance为public,作为在外界访问的接口
3. 构造函数为private,不能在外界直接调用,而必须通过Instance进行调用
private static InventoryManager _instance;
public static InventoryManager Instance {
get {
if(_instance == null) {
// 第一次想要得到的时候,未赋值,给它赋值
_instance = GameObject.Find("InventoryManager").GetComponent<InventoryManager>();
}
return _instance;
}}
任务14:改进Knapsack和Chest的设计
因为Knapsack和Chest是有共有功能的,因此可以创建一个类Inventory作为他俩的父类
任务9&10&11:Json解析 -- LitJSON 和 JsonObject
InventoryManager需要进行Items.Json数据的解析
在Json官网 www.json.org中找到c#的 LitJSON
或前往 https://litjson.net/
额。。。下载失败,我直接在csdn下载了
https://download.csdn.net/download/blackbord/10016032
下载dll文件,导入unity中就可以使用dll中的相关类了
在Project文件夹下创建Plugins文件夹,这个文件夹下的文件会被预编译,一般用于放置插件
在InventoryManager中创建解析Json文件的方法:
ParseItemJson()
解析出来的结果为很多Item,新建一个List列表来存储
private List<Item> itemList;
itemList = new List<Item>();
取得Json文件的内容
TextAsset jsonTextAsset = Resources.Load<TextAsset>("Items");
string jsonString = jsonTextAsset.text; // 得到了文本文件中的字符串
解析
using LitJson;
LitJson的教程 -- https://www.cnblogs.com/Firepad-magic/p/5532650.html
// Siki老师下载失败后,从AssetStore上import了JsonObject
-- 会和LitJson有所区别
思路:
1. 通过API得到存储数据的对象(该对象为一个集合)
2. 通过遍历该对象,得到每一个数据对象
3. 通过"type"字段的值,判断Item的类型
4. 声明对应类型的对象,并通过构造函数新建对象
5. 将新建的对象添加到list中
LitJson版本:
// 得到的jsonData为一个集合,每一个元素也是JsonData类型 JsonData jsonData = JsonMapper.ToObject(jsonString); foreach (JsonData data in jsonData) { // 将JsonData对象中存储的值,通过Item或子类的构造函数,新建一个对应的Item对象 // 先得到共有的属性 int id = int.Parse(data["id"].ToString()); string name = data["name"].ToString(); string type = data["type"].ToString(); Item.ItemType itemType = (Item.ItemType)System.Enum.Parse(typeof(Item.ItemType), type); Item.ItemQuality itemQuality = (Item.ItemQuality)System.Enum.Parse(typeof(Item.ItemQuality), data["quality"].ToString()); string description = data["description"].ToString(); int capacity = int.Parse(data["capacity"].ToString()); int buyprice = int.Parse(data["buyprice"].ToString()); int sellprice = int.Parse(data["sellprice"].ToString()); string spritePath = data["spritePath"].ToString(); Item item = null; // 首先需要通过"type"的值,确认该Item是什么类型的 switch (itemType) { case Item.ItemType.Consumable: int hp = int.Parse(data["hp"].ToString()); int mp = int.Parse(data["mp"].ToString()); // 通过JsonData的数据,新建一个Consumable对象 item = new Consumable(id, name, itemType, itemQuality, description, capacity, buyprice, sellprice, spritePath, hp, mp); break; case Item.ItemType.Equipment: break; case Item.ItemType.Weapon: break; case Item.ItemType.Material: break; default: break; } // 将新建的Item对象添加到list中 itemList.Add(item); }
JsonObject版本:
JsonObject讲解:readme.txt
直接通过JSONObject的构造函数进行Json数据的解析
得到的多个JsonObject对象会存储在list中
事实上Json数据中的任何一个整体都是一个JsonObject类的对象
比如一个键值对,或一个对象,或一个数组
对于每个对象,通过jsonObject["key"]访问对应的value,根据value类型
通过.n表示float,.b表示bool,.str表示string等等,还有Object、数组等类型
// 得到的jsonObject为一个list集合,每一个元素也是JsonObject类型 JSONObject jsonObject = new JSONObject(jsonString); // 遍历JSONObject.list,得到每一个对象 foreach(JSONObject elem in jsonObject.list) { // 将对象转换为Item类 // 通过索引器得到的为JsonObject类型 // ToString()后发现,数据带有引号"" // 不能使用 elementObject["name"].ToString()); int id = (int)elem["id"].n; string name = elem["name"].str; Item.ItemType type=(Item.ItemType)System.Enum.Parse(typeof(Item.ItemType),elem["type"].str); ... Item item = null; switch (type) { case Item.ItemType.Consumable: int hp = (int)elem["hp"].n; int mp = (int)elem["mp"].n; item = new Consumable(id, name, type, quality, description, capacity, buyprice, sellprice, spritePath, hp, mp); break; ... default: break; } itemList.Add(item); }
任务12&13:背包的UI
所有物品的信息都保存在了InventoryManager.itemList中,
现在开发数据和UI之间的连通,将item显示在UI上
开发背包的UI
新建UI->Panel,命名KnapsackPanel,SourceImage: panel,调节颜色
屏幕自适应
Canvas--CanvasScaler--UI Scale Mode = Scale With Screen Size
表示按控件占屏幕的比例来显示,而不是按像素来显示
Match = Width,表示按宽度的比例来,而高度的确定按照控件的宽高比而定
显示效果不好 -- 去掉天空盒子,Window->Lighting->Skybox选择None
新建子物体UI->Panel,命名ItemsContainer,作为所有物品的容器
调整大小
因为不需要显示,所以alpha=0;
新建子物体UI->Image,命名Slot,作为一个物品的容器
SourceImage: button_square
因为需要很多个Slot,因此在ItemsContainer中添加组件Grid Layout Group,用于排序
调整Cell大小,调整Spacing
新建Knapsack的子物体,UI->Image,命名TitleBg,SourceImage: button_long
新建子物体, UI->Text,背包,字体等微调
因为不需要交互,取消勾选Knapsack、TitleBg、Text、ItemsContainer的Raycast Target
只有Slot需要交互
Slot的完善:
实现鼠标移入的颜色变化效果
在Slot上添加组件Button
制作成prefab
在Slot下创建子物体UI->Image,命名Item,作为Slot中存储的物品
调整大小,SourceImage: 先随便选一个,因为最后是动态赋值的
在Item下创建子物体UI->Text,命名Amount,用于显示物品数量,微调颜色等
因为在Slot中做了交互,所以Item和Amount中的Raycast Target取消勾选
将Item制作成prefab
给Slot添加脚本Slot.cs,用于管理自身
给Item添加脚本ItemUI.cs,用于管理Item自身的显示等功能
因为ItemUI表示的为存在该Slot中的物体,因此需要保存该Item和数量
public Item Item {get; set; }
public int Amount {get; set; }
任务14~18:Inventory的实现 -- 物品存储功能
&任务19:物品存储后的UI更新显示
&任务25:Bugfixing
&任务39:添加物品时的动画显示
Inventory.cs脚本 -- 管理自身中所有的Slot
在Knapsack中添加脚本Knapsack 继承自 Inventory
// 存储所有的Slot
private Slot[] slotList;
// 在Start()中获取所有的Slot
public virtual void Start() {
slotList = GetComponentsInChildren<Slot>();
}
-- 因为在Knapsack等子类中也需要用到这个Start(),因此设置为virtural,方便子类访问
拾起物品并存储进背包的功能:
public bool StoreItem(int id)
public bool StoreItem(Item itemToStore)
// 返回bool表示是否存储成功,因为一些原因比如背包满了
-- InventoryManager 中根据item id返回Item对象的方法
public Item GetItemById(int id) {
foreach(Item item in itemList) {
if(item.ID == id) {
return item;
}}
return null;
}
public bool StoreItem(int id) {
// 先进行转换
Item item = InventoryManager.Instance.GetItemById(id);
return StoreItem(item);
}
public bool StoreItem(Item item) {
// 安全判断
if(item == null) { Debug.LogWarning("要存储的物品id不存在"); }
// 存储
// 两种情况
// 1. 之前背包没有该类物品
实例化一个该类物体,将其放入一个Slot
2. 背包已有该类物品
找到该Slot
若物品个数小于Capacity,Amount+1 (装备等capacity为1)
若放满了,则实例化另一个Item,并放入另一个Slot
if(item.capacity == 1) {
Slot slotToStore = FindEmptySlot();
if(slotToStore == null) { Debug.LogWarning("没有空位"); return false; }
else { // 将物品放入该slot
slotToStore.StoreItem(item);
}} else {
// 判断当前是否已经存在该类物体
Slot slotToStore = FindSlotWithSameItemType(item);
if(slotToStore != null) { // 找到已存在同类未满Slot
slotToStore.StoreItem(item);
} else { // 未找到
// 新建一个slot存储
slotToStore = FindEmptySlot();
if(slotToStore == null) { ... 警告已满; return false; }
else {
slotToStore.StoreItem(item);
}}}
如何找到空格子呢?
private Slot FindEmptySlot()
foreach(Slot slot in slotList) {
if slot.transform.childCount == 0) {
return Slot;
}}
return null;
}
如何找到类型相同的物品槽呢?
private Slot FindSlotWithSameItemType(Item item) {
foreach(Slot slot in slotList) {
if(slot.transform.childCount == 1) { // 有一个子物体
if(slot.GetItemId() == item.ID && !slot.IsSlotFilled()) { // 符合类型且数量未满
// ------- 在Slot中实现GetItemType()方法
// public Item.ItemType GetItemType() {
// return transfrom.GetChild(0).GetComponent<ItemUI>().Item.Type;
// }
// ------- 任务25中发现:不应该判断GetItemType()
// 这样如果血瓶和蓝瓶都是Consumable的Type,就会互相叠加了
// public int GetItemId() {
// return transfrom.GetChild(0).GetComponent<ItemUI>().Item.ID;
// }
// ------- 在Slot中实现IsSlotFilled()方法
// public bool IsSlotFilled() {
// ItemUI itemUI = transform.GetChild(0).GetComponent<ItemUI>();
// return itemUI.Amount >= itemUI.Item.Capacity;
// }
return slot;
}}}
return null;
}
如何将物品存入Slot呢?
在Slot.cs中
public void StoreItem(Item item) {
if(transform.ChildCount == 0) { // 空slot
// 实例化Item,并存入Slot
-- public GameObject itemPrefab;
GameObject itemObject = Instantiate(itemPrefab);
itemObject.transform.SetParent(transform);
itemObject.transform.localPosition = Vector3.zero;
// 这里的Scale显示会出现Bug,在任务39中(即本节最后)会详细说明
// 给实例化出来的空Item进行赋值
// ---------在ItemUI中实现Item的赋值 public void SetItem(Item item, int amount = 1) {
// this.Item = item;
// this.Amout = amount;
// // 更新UI
// }
itemObject.GetComponent<ItemUI>().SetItem(item);
} else { // 本身已经存储了物体
Item itemInSlot = transform.GetChild(0);
ItemUI itemUI = itemInSlot.GetComponent<ItemUI>();
// 这里不必判断Slot满的情况,因为在外界判断完了
// -------- 在ItemUI中实现数量+1的方法 public void AddAmount(int num = 1) {
// this.Amount += num;
// // 更新UI
// }
itemUI.AddAmount();
}
如何更新UI呢?
// 显示有两个部分,一个部分是Sprite,一个部分是Amount.text
private Image itemImage;
private Text amountText;
// 如果将初始化写在Start中,会报空指针,因为在一开始的时候就执行了赋值初始化
// 所以写成get的形式
public Image ItemImage {
get{
if(itemImage == null) {
itemImage = GetComponent<Image>();
}
return itemImage;
}
public Text AmountText {
// 相似
amountText = GetComponentInChildren<Text>();
}
public void UpdateUISprite() {
ItemImage.sprite = Resources.Load<Sprite>(Item.SpritePath);
public void UpdateUIText() {
AmountText.text = Amount.ToString();
测试:
新建脚本Player.cs
-- 因为操作物品的来源一般为Player(随意啦,一个解释而已)
-- 通过键盘按键G,随机得到一个物品放到背包中
在 Update()中
if(Input.GetKeyDown(KeyCode.G) {
// 随机生成一个id
int id = Random.Range(1, 2);
// 调用Knapsack (即Inventory中)的StoreItem(id)进行存储
// ---------- 将Inventory做成单例模式
// 但是不能在Inventory中实现,应该在Knapsack和Chest中实现
// 因为如果在Inventory中实现,那么Knapsack和Chest就会共用了
// 将Knapsack做成单例模式
// private static Knapsack _instance;
// public static Knapsack Instance {
// get{
// if(_instance == null) {
// _instance = GameObject.Find("KnapsackPanel").GetComponent<Knapsack>();
// }
// return _instance;
// }}
Knapsack.Instance.StoreItem(id);
代码:
Player.cs
public class Player : MonoBehaviour { void Update () { if(Input.GetKeyDown(KeyCode.G)) { // 随机生成一个id int id = Random.Range(1, 2); Knapsack.Instance.StoreItem(id); }}}
Knapsack.cs中只有单例模式的实现代码
Inventory.cs
public class Inventory : MonoBehaviour { private Slot[] slotList; public virtual void Start () { slotList = GetComponentsInChildren<Slot>(); } public bool StoreItem(Item itemToStore) { // 存储一个Item,有两种情况 // 1. 在Inventory中没有此类Item,则寻找空Slot存储 // 2. 在Inventory中已有此类Item // 若数量已满,则寻找空Slot存储;若数量未满,则增加数量即可 // 另一种判断: // 1. 若Item.Capacity为1,则需要寻找空Slot存储 // 2. 若不为1,寻找是否已经存在该类物品 // 已存在,则数量增加;没有存在,寻找空Slot Slot slotToStore = FindSlotWithSameItemType(itemToStore); if(slotToStore == null) { // 没有找到相同类型且未满的Slot -- 故寻找空slot存储 slotToStore = FindEmptySlot(); if(slotToStore == null) { Debug.LogWarning("空间已满,不可进行存储"); return false; } else { //找到空slot,进行存储 slotToStore.StoreItem(itemToStore); } } else { // 找到相同类型且未满的Slot,存储 slotToStore.StoreItem(itemToStore); } return true; } public bool StoreItem(int itemId) { Item itemToStore = InventoryManager.Instance.GetItemById(itemId); if(itemToStore == null) { // 未找到该Item return false; } return StoreItem(itemToStore); } private Slot FindSlotWithSameItemType(Item item) { foreach(Slot slot in slotList) { if(slot.transform.childCount == 1) { // 不是空slot if(slot.GetItemType() == item.Type && !slot.IsSlotFilled()) { // 相同类型的slot,且未满 return slot; }}} return null; } private Slot FindEmptySlot() { foreach(Slot slot in slotList) { if(slot.transform.childCount == 0) { // 找到空slot return slot; }} return null; }}
Slot.cs
public class Slot : MonoBehaviour { public GameObject itemPrefab; public Item.ItemType GetItemType() { return transform.GetChild(0).GetComponent<ItemUI>().Item.Type; } public bool IsSlotFilled() { ItemUI itemUI = transform.GetChild(0).GetComponent<ItemUI>(); return itemUI.Amount >= itemUI.Item.Capacity; } public void StoreItem(Item itemToStore) { // 两种情况下调用该方法: // 1. 本Slot为空,需要实例化Item进行存储 // 2. 本Slot不为空,只需要增加数量即可 if(transform.childCount == 0) { // 实例化Item GameObject itemObject = GameObject.Instantiate(itemPrefab) as GameObject; itemObject.transform.SetParent(transform); itemObject.transform.localPosition = Vector3.zero; // 给该Item赋值 itemObject.GetComponent<ItemUI>().SetItem(itemToStore); } else { // 数量增加 transform.GetChild(0).GetComponent<ItemUI>().AddAmount(); }}}
ItemUI.cs
public class ItemUI : MonoBehaviour { public Item Item { get; set; } public int Amount { get; set; } private Text amountText; public Text AmountText { get { ... } } private Image itemImage; public Image ItemImage { get { ... } } public void SetItem(Item item, int amount = 1) { // amount默认为1,因为该方法意为被空Slot存储item时调用 this.Item = item; this.Amount = amount; // UI更新 UpdateUISprite(); UpdateUIText(); } public void AddAmount(int num = 1) { // 默认+1,因为该方法意为存储item时调用,通常存储为1个 this.Amount += num; // UI更新 UpdateUIText(); } private void UpdateUIText() { AmountText.text = Amount.ToString(); } private void UpdateUISprite() { ItemImage.sprite = Resources.Load<Sprite>(Item.SpritePath); }}
任务39:添加物品时的动画显示
-- 物品添加到Slot中时,会先放大一下物品表示强调,再缩小到应有大小
在ItemUI中控制动画的播放
流程解释:Player.Update() -> Knapsack.StoreItem(id/item) -> Slot.StoreItem(Item) -> ItemUI.SetItem(item)
ItemUI.SetItem(item)中,传递设置了item和amount,并更新了sprite和text的UI显示
因此在ItemUI.UpdateUISprite()中
添加
直接在UpdateUISprite()中完成动画效果吗?
不行,需要在Update()中不断调用Lerp来实现
定义属性
private float targetScale = 1;
Update() {
if(Mathf.Abs(transform.localScale.x - targetScale) > 0.05f) {
// 进行动画播放
transform.localScale = Vector3.one * Mathf.Lerp(transform.localScale.x, targetScale, Time.deltaTime*smooth);
} else {
transform.localScale = Vector3.one * targetScale; // 节约性能
if(targetScale != 1) {
// 每当添加物品时,会将targetScale设大,播放动画
// 结束动画后localScale=targetScale>1,此时自动将targetScale设为1,开始变小动画
targetScale = 1;
}}}
Bug修复:在Slot.cs的StoreItem()里有一个scale自动变化的问题
public void StoreItem(Item itemToStore) { if(transform.childCount == 0) { // 实例化Item GameObject itemObject = GameObject.Instantiate(itemPrefab) as GameObject; // 大小显示一直有问题,在这里手动设置 SIKI_Unity_2_初级案例_贪吃蛇SIKI_Unity_2_入门_通过实例学习游戏的存档和读档