Unity3D RPG实现 2 —— 背包系统
Posted 晴夏。
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D RPG实现 2 —— 背包系统相关的知识,希望对你有一定的参考价值。
视频链接:使用unity实现的3D RPG游戏_网络游戏热门视频 (bilibili.com)
目录
背包系统
制作背包的基本UI
-
导入素材设置画布布局
-
使用 Grid Layout Group 进行网格化控制
-
保存 Slot Holder 为 Prefab
下载两个素材:
然后导入设置画布:
然后给图片设定长宽,注意到被拉伸后设置的不好看:
进入原图的sprite editor:
调整其border,这样其就只会有中心被拉伸而边框不拉伸了。
点击apply,回到原panel中:
image type选择slice前后对比:
创建title和字、以及按钮:
接下来是关键,添加格子的办法:
先添加一个panel,锚点居中调整大小:
阿尔法值设为0
然后添加这个便于布局:
然后为其设定大小、空隙和初始:
限定纵向列数为5
效果如图:
接下来在格子中间实现图片:
添加图片,选择覆盖全屏的锚点,然后设定边距
效果:
随便选中一个图片可看到效果:
然后添加字体并设定大小。
然后将其设为预制体:
然后复制30个:
快捷栏和人物信息面板的UI
-
创建快捷栏 Action Bar 的按钮 UI
-
创建人物信息面板 UI
添加画布,设定位置,然后添加这个layout Group
然后将item slot拖出来也作为预制体:
然后将item slot加到action button里去,将其设为预制体,添加脚本,和挂接组件
然后将其放进去actionbar的container脚本里:
修改下图片使其美观:
然后再创建一个画布如下:
上面那一格拿来做武器。
在此基础上创建一个武器图片:
再给那个武器图片添加一个item slot
添加武器和盾牌:
创建信息:
创建世界地图上的可拾取物品
这是人物身上的装备的地方
剑弄进来放在地上并导入材质
然后将剑作为预制体并添加刚体碰撞体。
然后创建一个SO:
[CreateAssetMenu(fileName ="New Item",menuName ="Inventory/Item Data")]
public class ItemData_SO : ScriptableObject
并创建文件:
此处需要将地图上的武器和玩家的武器区分开来,因为玩家身上的武器没有刚体组件,所以需要两个预制件。
public enum ItemType Useable,Weapon,Armor
[CreateAssetMenu(fileName ="New Item",menuName ="Inventory/Item Data")]
public class ItemData_SO : ScriptableObject
public ItemType itemType;
public string itemName;
public Sprite itemIcon;
public int itemAmount;//这个物品有多少数量
[TextArea]
public string description = "";
public bool stackable;//是否可堆叠
[Header("Weapon")]
public GameObject WeaponPrefab;
设定剑信息
接下来实现碰撞到世界地图上的剑然后装备。
public class ItemPickUp : MonoBehaviour
public ItemData_SO itemData;
private void OnTriggerEnter(Collider other)
if (other.CompareTag("Player"))
//TODO:添加物品到背包
//装备武器
Destroy(gameObject);
为实现给武器一个掉落到地面上的效果,添加重力和capsule collider:
为使得剑在人物身上时可以正常,调位置:
由于我们希望装备武器后,武器攻击力也会改变,所以换武器的操作在characterStats完成。
设定位置:
然后即可实现触碰武器就装备上了。
接下来实现人物的数据由武器的数据决定。
先在物品信息处添加武器信息。
添加武器数据
修改数据的函数:(在攻击数据的SO里进行修改)
装备武器时修改属性
创建背包的数据库
内容:
-
创建 InventoryData_SO 作为不同背包的数据库
-
创建单独的 class InventoryItem 用来记录背包里的物品和现有数量
-
添加 AddItem 方法将物品信息传递到 List 列表当中
-
修改 ItemPickUp 实现碰撞物体 拾取到背包里
创建背包的SO
[CreateAssetMenu(fileName ="New Inventory",menuName ="Inventory/Inventory Data")]
public class InventoryData_SO : ScriptableObject
再创建一个文件夹:
创建一个背包的inventory data,这个背包想要的无非就是一个列表,就是之前的itemData,但是如果创建一系列的itemData没有办法给它计数,因为此处的itemAmount指的是创建的物品本身的数量(比如打怪掉落苹果,掉落三个)
如果直接把这个数据放到data里面用的话,就无法使用这个数据的相加或者相减了,所以此处为它单独创建一个列表里的元素,单独创建一个class。
实现如下:
public class InventoryData_SO : ScriptableObject
public List<InventoryItem> items = new List<InventoryItem>();
[System.Serializable]
public class InventoryItem
public ItemData_SO itemData;
public int amount;
为使得类里面的变量能看见,添加Serializable使其序列化。
效果:
堆叠物品的代码:
[CreateAssetMenu(fileName ="New Inventory",menuName ="Inventory/Inventory Data")]
public class InventoryData_SO : ScriptableObject
public List<InventoryItem> items = new List<InventoryItem>();
public void AddItem(ItemData_SO newItemData,int amount)
//对于添加物体,如果是可以堆叠的,只需要在背包中找到同类型的,改变数量,如果不能堆叠,就新建一个格子
bool found = false;//是否找到同类物品
//
if (newItemData.stackable)
foreach(var item in items)
if (item.itemData == newItemData)
item.amount += amount;
found = true;
break;
//上面用foreach是为了找相同的物品,下面使用for循环是为了找到最近的空格
//如果捡到一个物体并且背包中不可堆叠只需要找到一个空格子放入即可
for(int i = 0; i < items.Count; i++)
if (items[i].itemData == null && !found)
items[i].itemData = newItemData;
items[i].amount = amount;
break;
这条添加物品的命令在哪里执行呢?我们创建了数据,接下来需要数据去管理,创建代码挂到inventory canvas管理。
添加背包数据:
拾取武器时不再是直接装备而是放入背包:
效果:
除此之外,有一点需要注意一下,
在itemData_SO中,其自带一个amount信息,是这个物品初始就具有的数量,比如打怪一次掉落三个苹果。这时候这三个苹果就是一个整体。
由于有些物品是可堆叠的,对于可堆叠的物品就需要有一个数量,意思是背包中,这种类型的物品一共有多少个。
所以需要物品信息在此基础上,设定一个背包内的物品信息,将物品的数量和背包中物品的数量区分开:
并将其作为一个新的类
实现背包根据数据库的信息显示物品
-
梳理逻辑并创建代码 ContainerUI / SlotHolder / ItemUI
-
修改 SlotHolder 的 Prefab 添加空物体 ItemSlot
-
逐层完成代码实现背包里正确显示物品图片和数量
创建三个脚本
为了实现代码的逻辑,调整下背包里物体的逻辑,背包里的格子的图片仅仅作为图片,改名为image,创建一个空物体用来挂接脚本,叫做item slot,并且把image赋值进去
此处实现将图片和数字更新进去
注意到初始时image为空,所以需要激活,为空时则关闭。
别忘了将image修改成它的子物体:
然后在此处写这个脚本
然后挂接上脚本;
Item UI用子物体去赋值,它会自动获得子物体的item ui这个脚本:
在item ui创建两个变量:
对不同情况进行分类,然后获取数据库中对应序号的物品,然后对其进行更新:
在父级物体上创建一个SlotHolder的容器:
然后在inspector窗口中赋值。
然后书写UI更新的代码:
再在总的管理器上添加一个获取的代码:
然后赋值:
接下来实现通过InventoryManager来对背包里的物品进行更新:
初始时更新一次:
捡到物品时再更新一次:
别忘了给剑赋予图片
运行后发现数字不在正中间,解决办法:
因为之前多添加了一个空物体作为item slot,我们将这个item slot布局为覆盖全部,然后留下5个空余:
将image设置为0:
然后拾取地上的剑即可看到剑出现在背包:
人物去捡剑时也会出现
背包、格子、数据库的逻辑关系总结
此处介绍一下从上到下的逻辑:
InventoryCanvas包含全部的背包系统,包括背包,人物属性和快捷栏。
InventoryBag包含背包的标题、关闭键和背包里的格子容器(InventoryContainer)。
格子容器(InventoryContainer)包含所有的子物体格子,代码上体现在:
并且由它执行子物体格子的刷新
子物体SlotHolder的赋值通过在inspector窗口中进行:并且格子的顺序和ContainerUI中的List列表里的顺序是一致的。
在SlotHolder中需要实现的代码如下:
SlotHolder是格子(由holder也可以理解),是不可移动的,是和容器里的List一一对应的。
而ItemSlot可以理解为格子里的物品,是可以移动,放到其他的格子里去的,并且里面的物品数量也可以改变。
但是其实ItemSlot里是item物品的UI信息,即图片和数量,并不是真正的物品。
真正的物品信息是放在这里的:
然后
ItemData_SO长这样:
实际例子比如剑:
在inspector窗口中为ContainerUI里的东西拖拽赋值后,接下来需要为格子里的每个物品UI进行序号上的赋值:
并且还要让格子里进行物品UI上的更新,即:slotHolders[i].UpdateItem();
对于每个物品的UI信息,我们需要建立三个变量,获取的是Inventory管理器中的三个InvetoryData的SO物体:
然后我们在itemUI的父级物体SlotHolder中根据类型来为其赋值:
这样的话,除了通过父物体去调用子物体,还实现了通过子物体获取父物体的方法。
上面这样给每个itemUI都赋予了不同种类的inventory栏,然后就可以通过该栏去获取该栏中与UI的Index对应的物品信息:
var item = itemUI.Bag.items[itemUI.Index];//找到数据库中对应序号的对应物品
获取了该物品信息后,就可以进行更新了:
itemUI.SetupItemUI(item.itemData, item.amount);
根据item信息更新itemUI的方法如下:
总结一下就是说,我们实际上获取物品或者移动物品,事实上是先直接修改背包里的数据库的物品信息,修改完毕后,我们让背包中的每个物品栏里的UI去通过背包里的数据库获取对应index的物品信息,然后让物品栏中的UI信息更新为数据库中对应序号的物品的图片信息和数量。
实现拖拽物品
-
创建 DragItem 实现拖拽功能
-
介绍 EventSystems 里的接口
-
实现拖拽跟随鼠标指针
-
创建 DragData 用来记录每一个 UI 物品原始数据
给信息栏里的containerUI也添加:
public class InventoryManager : Singleton<InventoryManager>
[Header("Inventory Data")]
//使用类似之前那样的模板,游戏新开始时复制一份的操作
//TODO:最后添加模板用于保存数据
public InventoryData_SO inventoryData;
public InventoryData_SO actionData;
public InventoryData_SO equipmentData;
[Header("Containers")]
public ContainerUI inventoryUI;
public ContainerUI actionUI;
public ContainerUI equipmentUI;
private void Start()
inventoryUI.RefreshUI();
接下来实现拖拽功能:
为ItemSlot预制体添加该功能。
想要实现拖拽的效果实现这几个接口即可
(交换数据往往需要一个第三方的东西变量来存储然后进行交换)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
[RequireComponent(typeof(ItemUI))]
public class DragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
ItemUI currentItemUI;
SlotHolder currentHolder;
SlotHolder targetHolder;
void Awake()
currentItemUI = GetComponent<ItemUI>();
currentHolder = GetComponentInParent<SlotHolder>();
public void OnBeginDrag(PointerEventData eventData)
//记录原始数据
public void OnDrag(PointerEventData eventData)
//跟随鼠标位置移动
transform.position = eventData.position;
public void OnEndDrag(PointerEventData eventData)
//放下物品 交换数据
然后即可实现拖拽移动物体,但是物体会被挡在背包里的格子之下。
解决方法:创建一个画布,当物体移动时,将该物体放到该画布上。
dragItem:
接下来需要用第三方的数据去保存那个holder,当拽完了之后需要还原到之前的位置
在InventoryManager设置:
并声明该变量:
然后在拖拽时候修改:
交换物品
-
使用 EventSystem 判断指针位置
-
利用 RectTransformUtility.RectangleContainsScreenPoint 判断鼠标位置是否包含在每一个格子范围内
-
交换图片的同时要交换真是的数据列表排序
-
调整 RectTransform 的 offset 确保图片在正确位置显示
参考代码手册:
此处要用到event system
需要要到event system的这个变量:
我们需要判断被拖拽的物品是否在三个栏里面
书写函数:
public bool CheckInInventoryUI(Vector3 position)//此处这个位置是要传输进来的位置
for(int i = 0; i < inventoryUI.slotHolders.Length; i++)
RectTransform t = inventoryUI.slotHolders[i].transform as RectTransform;//强制类型转换
if (RectTransformUtility.RectangleContainsScreenPoint(t, position))//判断当前位置是否物品栏里
return true;
return false;
使用如下:
public void OnEndDrag(PointerEventData eventData)
//放下物品 交换数据
if (EventSystem.current.IsPointerOverGameObject())//是否指向UI组件
if(InventoryManager.Instance.CheckInActionUI(eventData.position)
|| InventoryManager.Instance.CheckInInventoryUI(eventData.position)
|| InventoryManager.Instance.CheckInEquipmentUI(eventData.position))
//判断是否在三个栏里面的格子里
下面这个是判断是否在UI组件上
想要实现交换,在endDrag的函数里实现,首先看它是否指向UI组件,如果是的话看是否在设定的背包、快捷栏、人物信息里。
如果在的话,我们获取鼠标点击松开位置的该物品信息。
如果该物体和原来鼠标拖拽的物体一样且可堆叠,只需要增加数字即可。
如果不可堆叠,那就交换数据库中的物品信息,然后刷新UI,就实现了这个效果。
实现代码:
[RequireComponent(typeof(ItemUI))]
public class DragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
ItemUI currentItemUI;
//
SlotHolder currentHolder;
SlotHolder targetHolder;
void Awake()
currentItemUI = GetComponent<ItemUI>();
currentHolder = GetComponentInParent<SlotHolder>();//原先的格子的信息
public void OnBeginDrag(PointerEventData eventData)
InventoryManager.Instance.currentDrag = new InventoryManager.DragData();
InventoryManager.Instance.currentDrag.orginalHolder = GetComponentInParent<SlotHolder>();
InventoryManager.Instance.currentDrag.originalParent = (RectTransform)transform.parent;
//记录原始数据
transform.SetParent(InventoryManager.Instance.dragCanvas.transform,true);
public void OnDrag(PointerEventData eventData)
//跟随鼠标位置移动
transform.position = eventData.position;
public void OnEndDrag(PointerEventData eventData)
//放下物品 交换数据
if (EventSystem.current.IsPointerOverGameObject())//是否指向UI组件
if(InventoryManager.Instance.CheckInActionUI(eventData.position)
|| InventoryManager.Instance.CheckInInventoryUI(eventData.position)
|| InventoryManager.Instance.CheckInEquipmentUI(eventData.position))
//判断是否在三个栏里面的格子里
if (eventData.pointerEnter.gameObject.GetComponent<SlotHolder>())
targetHolder = eventData.pointerEnter.gameObject.GetComponent<SlotHolder>();
else
targetHolder = eventData.pointerEnter.gameObject.GetComponentInParent<SlotHolder>();
//如果没找到,此时是因为被图片所挡住了,那么就获取其父类的component
Debug.Log(eventData.pointerEnter.gameObject);
//判断鼠标选中的物体是否有slot holder,
//由于例如食物不能放在武器栏里,所以需要对其做区分
switch (targetHolder.slotType)
case SlotType.BAG:
SwapItem();
break;
case SlotType.WEAPON:
break;
case SlotType.ARMOR:
break;
case SlotType.ACTION:
break;
currentHolder.UpdateItem();//交换完毕后需要更新数据
targetHolder.UpdateItem();
transform.SetParent(InventoryManager.Instance.currentDrag.originalParent);
public void SwapItem()
//targetHolder是鼠标指向的位置的格子。
//获取目标格子上面显示的UI,UI图片对应它身上属于哪个背包的哪一个序号的物品
var targetItem = targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index];//实际上就是获取鼠标指向的物品item
var tempItem = currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index];//点击前的鼠标的格子的信息
//如果是相同的物品就进行合并
bool isSameItem = tempItem.itemData == targetItem.itemData;
if (isSameItem && targetItem.itemData.stackable)//并且还要可堆叠才行
targetItem.amount += tempItem.amount;
tempItem.itemData = null;
tempItem.amount = 0;
else
currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index] = targetItem;
//targetItem=tempItem;这样的写法不行因为targetItem只是我们获取的一个变量,应该直接用其本身去更换,即下一行的写法
targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index] = tempItem;
然后接下来如果我们移动物体,如果正好移到中心方块则可以正常实现,如果没有移到中心方块则会出现下面这样的情况。
观察可得,此时物体卡在中间:
item slot的位置也发生了变化:
所以我们要保证它能回到间距5个单位的中心格子上。
注意这两个变量:
offsetMax就是 这个里面的right和top
然后在代码中新增这两项:
接下来即可实现拖拽。
实现切换武器
DragItem 中添加物品属性和 SlotType 的匹配
对于非背包里的物品我们也可以交换但是要加以限制。
在 SlotHolder 中切换所属背包的数据库
当我们将Slot Holder放在不同的地方时,此时也要更改其所属的背包对应的数据库:
这样即可实现不同栏间的物品交换,并且数据库也会因此更新。
鼠标点击会使得人物移动的问题
换武器的方法
在characterStats中补充卸下武器的代码:
public void UnEquipWeapon()
if (weaponSlot.transform.childCount != 0)
for(int i = 0; i < weaponSlot.transform.childCount; i++)
Destroy(weaponSlot.transform.GetChild(i).gameObject);
卸下武器前就要还原成原来的攻击数据,因此需要有一个变量来记录初始时没武器的数据:
代码如下:
public void UnEquipWeapon()
if (weaponSlot.transform.childCount != 0)
for(int i = 0; i < weaponSlot.transform.childCount; i++)
Destroy(weaponSlot.transform.GetChild(i).gameObject);
attackData.ApplyWeanponData(baseAttackData);//卸下武器后还原之前的攻击力
//TODO:切换动画
public void ChangeWeapon(ItemData_SO weapon)
UnEquipWeapon();
EquipWeapon(weapon);
然后在SlotHolder更新一栏中更改武器:
可使用的物品
-
创建大蘑菇 并添加必要的组件
-
创建 UseableItemData_SO 制作可使用物品的属性模版
-
利用 IPointerClickHandler 接口实现双击使用物品
-
在 CharacterStats 中添加 ApplyHealth 实现血量变化
参考代码手册:
-
IPointerClickHandler :点击跳转
创建一个蘑菇的变量:
创建useable类型的SO:
然后在itemData中加入这种类型:
创建useable类型的SO:
并赋值:(为useable Item赋值)
然后在场景中创建一个mushroom并添加这些组件:
之后即可实现拾取蘑菇并且在格子和快捷栏里自由移动。
接下来实现使用物品的功能:
先在itemUI中写一个根据对应UI获取对应物品的方法:
在characterStats中书写一个函数:
使用需要实现一个接口,
在SlotHolder里实现该接口
public void OnPointerClick(PointerEventData eventData)
if (eventData.clickCount % 2 == 0)//代表是双击的话
UseItem();
public void UseItem()
if (itemUI.GetItem().itemType == ItemType.Useable&&itemUI.Bag.items[itemUI.Index].amount>0)
GameManager.Instance.playerStats.ApplyHealth(itemUI.GetItem().useableData.healthPoint);
itemUI.Bag.items[itemUI.Index].amount -= 1;
UpdateItem();
别忘了要进行数量减一,更新UI的操作。
但是之后发现一个bug,当蘑菇用完时图片仍然存在:
解决方法:
显示人物面板相关信息
设定一个相机让它观察player,并设定好各类信息:
创建texture然后放进去
然后将renderTexture给Stats里面创建的RawImage
记得将相机的设置放入prefab里面
然后可以显示:
接下来实现血量和伤害的显示:
然后在装备武器时进行信息的更新:
Unity3d - RPG项目学习笔记(二十)
前期工程将装备信息导入到了工程中,且实现了在背包内鼠标移动显示物品提示信息,本次工程开始构建装备穿戴功能。
项目需求:
右键点击背包内的装备,使其穿戴在身上。
需求分析:
右键点击背包内的装备,注意,此时的装备还是背包内的一个物品,即是工程所定义的id为2001-2010的InventoryItem而已;需求可以抽象为右键点击背包内的一个物品,如果该物品是装备类,则在角色的EquipmentUI相应的位置生成一个与之图标相同的EquipmentItem,则视为“装备”了该物品。
具体实现:
①右键点击:
更新InventoryItem类中的 void Update方法为:
Class InventoryItem
{
void Update( )
{
if(isHover) //检鼠标是否在图标上
{
InventoryDes._instance.Show(id);
if(Input.GetMouseDown(1))
{
EquipmentUI._instance.Dress(id); //调用EquipmentUI中的Dress方法(下一步构建)
}
}
}
}
构建EquipmentUI中的Dress方法,实现在装备栏上生成装备图标,更新脚本如下:
首先需要在PlayerState脚本中加入角色的分类,作为装备能否的标识位
public eunm CharacterType { Swordman,Magician }
Class PlayerState
{
public CharacterType herotype;
}
Class EquipmentUI
{
private PlayerState ps;
public GameObject equipitem;
void Awake( )
{
ps = GameObject.FindGameObjectWithTag(Tag.player).GetCompnent<PlayerState>( );
}
public bool Dress(int id)
{
ObjectInfo info = ObjectsInfo._instance.GetObjectInfoById(id);
if(info.Type != ObjectType.Equip)
{
return false;
}
if(ps.horetype == Magician)
{
if(info.CharaterType == charactertype.Swordman)
return false;
}
if(ps.horetype == Swordman)
{
if(info.CharaterType == charactertype.Magician)
return false;
}
//排除一切不能装备的情况,下面处理可以装备的情况,首先确定装备要装备在哪个框中,即确定生成装备的父类物体
private GameObject parent = null;
Swtich(info.dressType)
{
case DressType.Headgear:
parent = headgear;
break;
case DressType.Armor:
parent = armor;
break;
case DressType.LeftHand:
parent = lefthand;
break;
case DressType.RightHand:
parent = righthand;
break;
case DressType.Shoe:
parent = shoe;
break;
case DressType.Accessory:
parent = accessory;
break;
}
//将装备图示的范例创建在父类下,使用EquipmentItem下的SetInfo方法(下步构建)来改变装备的图标
//检测该处是否装备了物品
EquipmentItem item = parent.GetCompnentInChild<EquipmentItem>();
if( item = null )
{
GameObject GOitem = UITool.AddChild(parent,equipitem);
Goitem.transform.localPosition = Vector3.zero;
GOitem.GetCompnent<EquipmentItem>().SetInfo(info);
}
else
{
item.SetInfo(info);
}
}
}
最后构建EquipmentItem中的SetInfo方法
Class EquipmentItem
{
private UISprite sprite;
private int id;
void Awake()
{
sprite = GetCompnent<UISprite>();
}
public void SetId(int id)
{
this.id = id;
SetInfo(ObjectsInfo._instance.GetObjectInfoById(id));
}
public void SetInfo(ObjectInfo info)
{
this.id = info.id;
sprite.SpriteName = info.icon_Name;
}
}
这样就实现了右键背包中的装备图标,然后会相应装备的功能。
以上是关于Unity3D RPG实现 2 —— 背包系统的主要内容,如果未能解决你的问题,请参考以下文章