背包系统,无论是游戏还是应用,都是常常见到的功能,其作用及重要性不用我多说,玩过游戏的朋友都应该明白。

在Unity中实现一个简易的背包系统其实并不是太过复杂的事。本文要实现的是一个带检索功能的背包系统。先看一下我们要完成的效果 。由于上传的gif图不能大于5M,所以录制的质量比较一般。大家先将就看一下吧

那现在,我们就开始动手了~~

一. 拼UI

由于本文着重讲述的是背包系统的运作,所以,背包里面的元素获取就简洁地做成这个样子

事实上,一般游戏内获取道具的途径是敌人掉落,完成任务,商城购买等,而这里简化了这一步,右边的加号代表向背包里添加一个该元素。好了,现在开始实际制作。

1.1 在 Resources 文件夹下创建两个子文件夹 Prefab 和 Sprite ,用于存放资源

,

1.2 创建一个Canvas;

创建一个空物体,命名为Options;

创建一个 Image,用来显示道具,命名为 X1-装备

在上一步创建的 Image 下创建一个子 Image。

然后给这两张 Image 赋值

​     

这里需要说明的是,命名为 X1-装备 是为了更方便的说明背包如何运作,事实上一般游戏的道具都是有着名字,种类等属性,而这些属性应该储存在 配置表 里面。本文不会涉及到配置表的操作,所以这里就简洁处理。还有这些UI资源都是随意的,读者自行选择自己的图片资源。

1.3 进行第②中的操作,实现如下效果

1.4 制作背包

在Canvas下创建一个空物体,命名为BG,赋上一张背景图,调整尺寸大小

在BG下创建一个Panel,调整其尺寸大小。并添加 Content Size FitterGrid Layout Group 组件,修改其中参数

1.5 检索UI

在BG下创建一个空物体,命名为 Toggles ,并在其下创建3个子Toggle,分别命名为 Med,Eqi,Goods。

再创建一个 InputField。如图

1.6 单个道具Item

创建一张Image, 命名为Cell。在Cell下创建一个子Text。调整两者大小。做成prefab,放到prefab文件夹中

至此,UI 拼接就完成了。只缺逻辑了~~~

二. UI逻辑

2.1 物品类

首先创建一个C#脚本,命名为 CellItem,代表物品类。一个道具(物品),通常都会有 名字,所属种类,数量,价格等属性,而这里的话,只取前三种属性,完整代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class CellItem { private string name;
private string tag;
private int number; public CellItem(string name,string tag,int num)
{
Name = name;
Tag = tag;
Number = num;
} public string Name
{
get { return name; }
set { name = value; }
} public string Tag
{
get { return tag; }
set { tag = value; }
} public int Number
{
get { return number; }
set { number = value; }
}
}

2.2 数据管理类

当我们的角色获得了一个道具之后,理应有一个管理系统将新获得的道具信息添加进去。所以创建一个C#脚本,命名为DataManager

在本文中实现的背包系统中,只管理了三种道具:药品,装备,物品(消耗品)。对应着前文所描述的 UI拼接 。所以在 DataManager 中创建3个 Dictionary ,用来存储道具信息。

	/// <summary>
/// 药品
/// </summary>
public Dictionary<string, CellItem> Medicine = new Dictionary<string, CellItem>(); /// <summary>
/// 装备
/// </summary>
public Dictionary<string, CellItem> Equipment = new Dictionary<string, CellItem>(); /// <summary>
/// 物品(消耗品)
/// </summary>
public Dictionary<string, CellItem> Goods = new Dictionary<string, CellItem>();

以每个道具的名字为 key。同时为了方便管理,我把DateManager做成了单例模式。不熟悉单例模式的朋友可以参考一下这篇博文https://www.cnblogs.com/liaoguipeng/p/5130144.html

DataManager完整代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class DataManager { /// <summary>
/// 药品
/// </summary>
public Dictionary<string, CellItem> Medicine = new Dictionary<string, CellItem>(); /// <summary>
/// 装备
/// </summary>
public Dictionary<string, CellItem> Equipment = new Dictionary<string, CellItem>(); /// <summary>
/// 物品(消耗品)
/// </summary>
public Dictionary<string, CellItem> Goods = new Dictionary<string, CellItem>(); // 定义一个静态变量来保存类的实例
private static DataManager _Instance; // 定义一个标识确保线程同步
private static readonly object locker = new object(); public static DataManager GetInstance()
{
// 当第一个线程运行到这里时,此时会对locker对象 "加锁",
// 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
// lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
// 双重锁定只需要一句判断就可以了
if (_Instance == null)
{
lock (locker)
{
// 如果类的实例不存在则创建,否则直接返回
if (_Instance == null)
{
_Instance = new DataManager();
}
}
}
return _Instance;
}
}

2.3 UI管理类

这个类是整个背包的重点,用于管理第一步中创建的所以UI。

创建一个C#脚本,命名为UIManager。在场景中创建一个空物体,命名为GameManager,并把UIManager挂载上去。

为了获取第一步中所创建的UI,先定义与之相匹配的变量

	private Toggle selectMed;
private Toggle selectEqi;
private Toggle selectGoods;
private InputField SearchBox; private bool isSelectMed;
private bool isSelectEqi;
private bool isSelectGoods; private Button[] buttons;
private GameObject lattice;
private GameObject content;
private Dictionary<string, CellItem> whole = new Dictionary<string, CellItem>(); ​

其中,toggle 和 InputField 对应之前所制作的 toggle 和输入框;

中间三个 bool 变量表示检索时是否选择的toggle代表的种类;

buttons 代表左边6个添加按钮;  lattice 用来得到之前制作的 Cell 的prefab;

content 代表前文的Panel;

whole则会记录所以的道具信息;

先给这些变量赋值 。我会先给出每个UI所对应的响应方法,方法体中会牵涉其它核心的方法,这个稍后会讲。

private void Awake()
{
SearchBox = GameObject.Find("Canvas/BG/InputField").GetComponent<InputField>();
SearchBox.onValueChanged.AddListener(SearchItem); lattice = Resources.Load<GameObject>("Prefab/Cell"); content = GameObject.Find("Canvas/BG/Panel");
selectMed = GameObject.Find("Canvas/BG/Toggles/Med").GetComponent<Toggle>();
selectEqi = GameObject.Find("Canvas/BG/Toggles/Eqi").GetComponent<Toggle>();
selectGoods = GameObject.Find("Canvas/BG/Toggles/Goods").GetComponent<Toggle>(); selectMed.onValueChanged.AddListener(OnMedicineToggleClick);
selectEqi.onValueChanged.AddListener(OnEquipmentToggleClick);
selectGoods.onValueChanged.AddListener(OnGoodsToggleClick); GameObject buts = GameObject.Find("Canvas/Options");
for (int count = 0; count < buts.transform.childCount; count++)
{
GameObject kid = buts.transform.GetChild(count).gameObject;
kid.transform.GetChild(0).GetComponent<Button>().onClick.AddListener(() => OnAddButtonClick(kid.name));
} isSelectMed = true;
isSelectEqi = true;
isSelectGoods = true;
}

而3个bool值变量对应着三个方法。这三个方法作用于检索

	private string IsSelectMed()
{
if (isSelectMed) return "药品";
return " ";
} private string IsSelectEqi()
{
if (isSelectEqi) return "装备";
return " ";
} private string IsSelectGoods()
{
if (isSelectGoods) return "物品";
return " ";
}

3个Toggle也对应了3个响应方法

	public void OnMedicineToggleClick(bool isOn)
{
if (isOn)
isSelectMed = true;
else
isSelectMed = false; ReflashBackup();
} public void OnEquipmentToggleClick(bool isOn)
{
if (isOn)
isSelectEqi = true;
else
isSelectEqi = false; ReflashBackup();
} public void OnGoodsToggleClick(bool isOn)
{
if (isOn)
isSelectGoods = true;
else
isSelectGoods = false; ReflashBackup();
}

6个button对应着同一个方法,不过参数不同,参数为每个button所对应的名字,如“X1-装备”,“X2-药品”等

	public void OnAddButtonClick(string Name)
{
string[] parts = Name.Split('-');
string name = parts[0];
string tag = parts[1];
switch(tag)
{
case "药品":
ModifyKey(DataManager.GetInstance().Medicine, name,"药品");
break;
case "装备":
ModifyKey(DataManager.GetInstance().Equipment, name,"装备");
break;
case "物品":
ModifyKey(DataManager.GetInstance().Goods, name,"物品");
break;
}
ModifyKey(whole, name,tag);
ReflashBackup();
}

接下来就是比较重要的方法:

一. 修改键对值

当添加一个道具进来时,对其进行管理。应该先检索当前的 Dictionary 中是否有这个道具(key),如果有,则这个道具的数量属性 + 1;如果没有,则添加一个键对值。

	public void ModifyKey(Dictionary<string, CellItem> cells,string name,string tag)
{
//先判断是否存在这个Key,没有则add
if (cells.ContainsKey(name))
{
int count = cells[name].Number;
cells[name].Number = count + 1;
}
else
{
CellItem cellItem = new CellItem(name,tag,1);
cells.Add(name, cellItem);
}
}

二. 创建道具单元

在默认显示所有物品的情况下,只要遍历 Dictionary whole就可以创建出所有的道具。而在加入了检索功能后,则需要多进行一个判断,提取出符合检索标准的键对值,这一步我们用 Linq 可以快速获取。然后创建所有道具。

	public void InstantiateCell(Dictionary<string, CellItem> dictionary)
{
Dictionary<string, CellItem> SelectObjs = new Dictionary<string, CellItem>(); var selects = from cell in dictionary
where (cell.Value.Tag == IsSelectMed() || cell.Value.Tag == IsSelectEqi() || cell.Value.Tag == IsSelectEqi())
select cell; foreach(var select in selects)
{
SelectObjs.Add(select.Key, select.Value);
} foreach (KeyValuePair<string, CellItem> value in SelectObjs)
{
GameObject cell = GameObject.Instantiate(lattice, content.transform);
SetPicture(value.Key, cell);
cell.transform.GetChild(0).GetComponent<Text>().text = value.Value.Number.ToString();
}
}

创建单元时需要把道具相应的图片显示出来,需要注意的是,我把我的资源放在了Resources/Sprite下,读者的资源路径与我不一致时,仅修改一行代码即可

 

	public void SetPicture(string name,GameObject cell)
{
//图片资源路径
string path = "Sprite/" + name;
Texture2D tex = Resources.Load<Texture2D>(path);
Image img = cell.transform.GetComponent<Image>(); Sprite sp = Sprite.Create(tex, new Rect(new Vector2(0, 0), new Vector2(tex.width, tex.height)), new Vector2(0, 0));
img.rectTransform.sizeDelta = new Vector2(sp.rect.width, sp.rect.height);
img.sprite = sp;
}

三. 刷新背包

当我们添加一个道具或者按种类检索时,都应该刷新背包。而刷新的思路也很简单:先把 Panel 下的所以 CellItem 删除,然后根据新的 Dictionary 创建新的一批道具单元。

删除 方法

	public void ClearContent()
{
Transform[] lattices = content.GetComponentsInChildren<Transform>();
foreach (Transform lattice in lattices)
{
if (lattice.name != "Panel")
{
lattice.DetachChildren();
GameObject.Destroy(lattice.gameObject);
}
}
}

所以刷新函数如下

	public void ReflashBackup()
{
ClearContent();
InstantiateCell(whole);
}


四. 按名字搜索

按名字搜索对应着前文创建的 InputField。思路也是比较简单的,如果字典中有这个道具key,就创建出来

	public void SearchItem(string itemName)
{
ClearContent(); if (whole.ContainsKey(itemName))
{
GameObject cell = GameObject.Instantiate(lattice, content.transform);
SetPicture(itemName, cell);
cell.transform.GetChild(0).GetComponent<Text>().text = whole[itemName].Number.ToString();
} }

完整代码:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using System.Linq; public class UIManager : MonoBehaviour { private Toggle selectMed;
private Toggle selectEqi;
private Toggle selectGoods;
private InputField SearchBox; private bool isSelectMed;
private bool isSelectEqi;
private bool isSelectGoods; private Button[] buttons;
private GameObject lattice;
private GameObject content;
private Dictionary<string, CellItem> whole = new Dictionary<string, CellItem>(); private void Awake()
{
SearchBox = GameObject.Find("Canvas/BG/InputField").GetComponent<InputField>();
SearchBox.onValueChanged.AddListener(SearchItem); lattice = Resources.Load<GameObject>("Prefab/Cell"); content = GameObject.Find("Canvas/BG/Panel");
selectMed = GameObject.Find("Canvas/BG/Toggles/Med").GetComponent<Toggle>();
selectEqi = GameObject.Find("Canvas/BG/Toggles/Eqi").GetComponent<Toggle>();
selectGoods = GameObject.Find("Canvas/BG/Toggles/Goods").GetComponent<Toggle>(); selectMed.onValueChanged.AddListener(OnMedicineToggleClick);
selectEqi.onValueChanged.AddListener(OnEquipmentToggleClick);
selectGoods.onValueChanged.AddListener(OnGoodsToggleClick); GameObject buts = GameObject.Find("Canvas/Options");
for (int count = 0; count < buts.transform.childCount; count++)
{
GameObject kid = buts.transform.GetChild(count).gameObject;
kid.transform.GetChild(0).GetComponent<Button>().onClick.AddListener(() => OnAddButtonClick(kid.name));
} isSelectMed = true;
isSelectEqi = true;
isSelectGoods = true;
} private string IsSelectMed()
{
if (isSelectMed) return "药品";
return " ";
} private string IsSelectEqi()
{
if (isSelectEqi) return "装备";
return " ";
} private string IsSelectGoods()
{
if (isSelectGoods) return "物品";
return " ";
} public void OnAddButtonClick(string Name)
{
string[] parts = Name.Split('-');
string name = parts[0];
string tag = parts[1];
switch(tag)
{
case "药品":
ModifyKey(DataManager.GetInstance().Medicine, name,"药品");
break;
case "装备":
ModifyKey(DataManager.GetInstance().Equipment, name,"装备");
break;
case "物品":
ModifyKey(DataManager.GetInstance().Goods, name,"物品");
break;
}
ModifyKey(whole, name,tag);
ReflashBackup();
} public void OnMedicineToggleClick(bool isOn)
{
if (isOn)
isSelectMed = true;
else
isSelectMed = false; ReflashBackup();
} public void OnEquipmentToggleClick(bool isOn)
{
if (isOn)
isSelectEqi = true;
else
isSelectEqi = false; ReflashBackup();
} public void OnGoodsToggleClick(bool isOn)
{
if (isOn)
isSelectGoods = true;
else
isSelectGoods = false; ReflashBackup();
} public void ModifyKey(Dictionary<string, CellItem> cells,string name,string tag)
{
//先判断是否存在这个Key,没有则add
if (cells.ContainsKey(name))
{
int count = cells[name].Number;
cells[name].Number = count + 1;
}
else
{
CellItem cellItem = new CellItem(name,tag,1);
cells.Add(name, cellItem);
}
} public void ReflashBackup()
{
ClearContent();
InstantiateCell(whole);
} public void InstantiateCell(Dictionary<string, CellItem> dictionary)
{
Dictionary<string, CellItem> SelectObjs = new Dictionary<string, CellItem>(); var selects = from cell in dictionary
where (cell.Value.Tag == IsSelectMed() || cell.Value.Tag == IsSelectEqi() || cell.Value.Tag == IsSelectEqi())
select cell; foreach(var select in selects)
{
SelectObjs.Add(select.Key, select.Value);
} foreach (KeyValuePair<string, CellItem> value in SelectObjs)
{
GameObject cell = GameObject.Instantiate(lattice, content.transform);
SetPicture(value.Key, cell);
cell.transform.GetChild(0).GetComponent<Text>().text = value.Value.Number.ToString();
}
} public void SetPicture(string name,GameObject cell)
{
//图片资源路径
string path = "Sprite/" + name;
Texture2D tex = Resources.Load<Texture2D>(path);
Image img = cell.transform.GetComponent<Image>(); Sprite sp = Sprite.Create(tex, new Rect(new Vector2(0, 0), new Vector2(tex.width, tex.height)), new Vector2(0, 0));
img.rectTransform.sizeDelta = new Vector2(sp.rect.width, sp.rect.height);
img.sprite = sp;
} public void ClearContent()
{
Transform[] lattices = content.GetComponentsInChildren<Transform>();
foreach (Transform lattice in lattices)
{
if (lattice.name != "Panel")
{
lattice.DetachChildren();
GameObject.Destroy(lattice.gameObject);
}
}
} public void SearchItem(string itemName)
{
ClearContent(); if (whole.ContainsKey(itemName))
{
GameObject cell = GameObject.Instantiate(lattice, content.transform);
SetPicture(itemName, cell);
cell.transform.GetChild(0).GetComponent<Text>().text = whole[itemName].Number.ToString();
} }
}

有个问题是,当按名字搜索时,添加道具会执行相应的代码。事实上,当游戏中处于检索时一般是不会触发添加物体这种事件的。

三. 总结

总体来说,本文实现的这个背包系统不算复杂,也有人会问,本文一直在使用 Dictionary whole。而在 DataManager 中定义的三个字典除了存储了信息之外,并没有用到别的地方去,似乎是有点多余。

事实上,本文只是着重介绍整个背包系统的原理才使用了whole,可以更为简洁地说明。读者可以思考一下,如果我有着 10个种类的道具,每种道具都需要一页表来显示。如果我使用whole,那么对whole筛选10遍,重复操作多,浪费资源,而如果我有10个于种类对应的字典则可以直接使用,无需筛选。所以,分类保存是很有必要的。

在下一篇中,我会对这个背包系统进行优化和升级

  • 采用读取配置表来录入道具信息
  • 从单页背包升级分页背包
  • 更为完善的检索系统

码字不易,希望这篇文章能对各位读者有所帮助O(∩_∩)O哈哈~

【UGUI】 (三)------- 背包系统(上)之简易单页背包系统及检索功能的实现的更多相关文章

  1. vue入门(三)----使用vue-cli搭建一个单页富应用

    上面两节我们说了vue的一些概念,其实说的知识一点基础,这部分知识我觉得更希望大家到官网进行学习,因为在这里说的太多我觉得也只是对官网的照搬照抄而已.今天我们来学习一下vue-cli的一些基础知识,并 ...

  2. VUE基于ElementUI搭建的简易单页后台

    一.项目链接 GitHub 地址: https://github.com/imxiaoer/ElementUIAdmin 项目演示地址:https://imxiaoer.github.io/Eleme ...

  3. vue-router+webpack线上部署时单页项目路由,刷新页面出现404问题

    使用vue项目,线上部署的时候,访问首页以及通过路由打开二级页面没有问题,但是一刷新就出现404现象 因为刷新页面时访问的资源在服务端找不到,因为vue-router设置的路由不是真实存在的路径. 解 ...

  4. phpcms v9如何给父级单页栏目添加内容

    对于phpcms单页的调用相信大家都应该没问题,那么如果我们在后台添加的单页有二层甚至更多的时候,这样在管理内容上是没有给父级栏目添加内容这一功能的!那么我们该怎么实现这个功能并调用呢? 首先我们要修 ...

  5. 从零搭建一个IdentityServer——单页应用身份验证

    上一篇文章我们介绍了Asp.net core中身份验证的相关内容,并通过下图描述了身份验证及授权的流程: 注:改流程图进行过修改,第三方用户名密码登陆后并不是直接获得code/id_token/acc ...

  6. Nodejs之MEAN栈开发(六)---- 用Angular创建单页应用(上)

    在上一节中我们学会了如何在页面中添加一个组件以及一些基本的Angular知识,而这一节将用Angular来创建一个单页应用(SPA).这意味着,取代我们之前用Express在服务端运行整个网站逻辑的方 ...

  7. 单页WEB应用(三),Chat聊天模块

    Chat 聊天模块 这个模块应该就是该书全篇的唯一一个模块吧,后面差点儿全部的篇章都环绕这个模块去实现的,只是就通过这一个模块的实现和上线,也能体现单页应用开发到公布上线的整个过程,毕竟后面的数据.通 ...

  8. 手把手教你把web应用丢到服务器上(单页应用+ 服务端渲染)

    前两篇文章中,我分别介绍了框架的搭建利用vue-cli + vant搭建一个移动端开发模板,并且把项目中axios请求和vuex的用法做了简要的介绍如何在项目里管理好axios请求与vuex.在这两篇 ...

  9. word页码上加横线&&word删除单页页眉

    word(2010)页码上加横线 插入——>页脚(选择年刊型)——>如图 然后拖住“竖条条”将页码拖到正中间——>点中页脚右击——>选中“表格属性”——>“边框和底纹”— ...

随机推荐

  1. iPhone/android的viewport 禁止页面自动缩放

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scal ...

  2. codeforces 547E Mike and Friends

    codeforces 547E Mike and Friends 题意 题解 代码 #include<bits/stdc++.h> using namespace std; #define ...

  3. vs环境变量学习

    1. 查看vs环境变量: 在项目设置中的任何路径.目录编辑项目下,右下角有个“宏”,点开即可见所有vs环境变量的当前设置...听说还有其它地方,没看到. 2.上边的“宏”,即是英文的vs环境变量 3. ...

  4. nginx alias

    A path to the file is constructed by merely adding a URI to the value of the root directive. If a UR ...

  5. FTP response 421 received. Server closed connection

    现象: 在springboot的定时器轮询去下载ftp文件时,报以下错误: org.apache.commons.net.ftp.FTPConnectionClosedException: FTP r ...

  6. redis key/value 出现\xAC\xED\x00\x05t\x00\x05

    1.问题现象: 最近使用spring-data-redis 和jedis 操作redis时发现存储在redis中的key不是程序中设置的string值,前面还多出了许多类似\xac\xed\x00\x ...

  7. php中static静态变量的使用方法详解

    php中的变量作用范围的另一个重要特性就是静态变量(static 变量).静态变量仅在局部函数域中存在且只被初始化一次,当程序执行离开此作用域时,其值不会消失,会使用上次执行的结果.     看看下面 ...

  8. JS实现sleep()方法

    这种实现方式是利用一个伪死循环阻塞主线程.因为JS是单线程的.所以通过这种方式可以实现真正意义上的sleep(). function sleep(delay) { var start = (new D ...

  9. 使用JS获取上一页的url地址

    一般来说每个页面上面都有一个返回按钮,用来返回上一页,代码如下: <a href="javascript:history.go(-1)" class="jsBack ...

  10. .Net操作Excel公式实现

    //传入Excel公式,获取公式计算结果private string GetValue(string formula) { string result = ""; try { Ob ...