系列文章目录
麦田物语第十九天
文章目录
- 系列文章目录
- 一、保存和加载场景中的物品
- 二、设置鼠标指针根据物品调整
一、保存和加载场景中的物品
本小节我们想要解决一个问题,就是当我们跳转场景后,在返回之前场景,发现场景中被我们拾取的物品都重新出现了,这样肯定是不行的,现在我们来解决这个问题。
我们解决这个问题的思路就是我们在切换场景之前将当前场景中的物品保存,然后再次回到该场景时将场景中保存的物品重新加载(拾取之后的物品就不会被重新加载了)。
思路有了,现在我们进行代码方面的实现。
我们游戏中物品生成是在ItemManager脚本中实现的,我们给这个脚本一个编号,就可以生成对应的物品,那我们来到ItemManager脚本开始编写代码。
接着我们需要考虑怎么存储这个物品信息,因为我们不是只有一个场景啊,所以我们需要将场景和其对应的物品信息存储起来,没错,我们可以使用字典来实现存储功能,键对应的就是场景名称,值对应的就是每个场景的物品信息List数组;但是我们还会想到一个问题,物品信息包括很多东西:编号,位置等,我们还要重新编写一个数据结构去作为物品信息变量。
那我们首先编写DataCollection脚本,在我们实现的class类中,我们必须要序列化物品坐标,只有这样我们才能保存物品的位置;那么这里面肯定需要的变量是float类型的x,y,z坐标,接着我们需要编写这个类的构造函数,这个构造函数的参数是一个Vector3类型的位置变量,我们通过对x,y,z坐标序列化。然后我们还需要一个函数将保存的坐标传递出去,即返回类型为Vector3的值。最后我们做的是2D游戏,使用的是Grid Cell都是整型的,我们可以使用VectorInt类型的值作为函数的返回类型将x,y的值传递出去。
DataCollection脚本的SerializableVector3类代码如下:
[System.Serializable]
public class SerializableVector3
{
public float x, y, z;
public SerializableVector3(Vector3 pos)
{
this.x = pos.x;
this.y = pos.y;
this.z = pos.z;
}
public Vector3 ToVector3()
{
return new Vector3(x, y, z);
}
public Vector2Int ToVector2Int()
{
return new Vector2Int((int)x, (int)y);
}
}
还记得我们所说的物品信息都有什么吗,除了物品的位置,就是物品的编号了,我们也在编写一个新的Class类来存储物品信息。
DataCollection脚本的SceneItem类代码如下:
[System.Serializable]
public class SceneItem
{
public int itemID;
public SerializableVector3 position;
}
现在我们的准备工作做好了,就可以返回ItemManager脚本编写物品存储和物品读取的方法了。
首先我们先要声明字典变量,用来存储物品信息,字典变量的声明如下:
//记录场景物品,
private Dictionary<string, List<SceneItem>> sceneItemDict = new Dictionary<string, List<SceneItem>>();
然后编写获得场景中所有物品的方法GetAllSceneItems,首先我们要声明一个变量currentSceneItems(List数组),用来存储当前场景中的所有物品信息,然后我们遍历该场景中所有挂载Item脚本的物品,将所有物品的信息存储到List数组中;最后我们需要将这个List数组在字典中进行更新,为什么是更新不是添加呢,因为我们可能进入的是新的场景,那这个场景的物品信息可能没有添加到字典中,所以我们需要先判断,如果当前字典中没有这个场景的名字,代表该场景的物品信息还没有存入字典,我们就直接在字典中增加新的键值对,如果该字典中有该场景的名字,那么我们将该键所对应的值进行重新赋值,达到所谓更新的效果。
ItemManager脚本的GetAllSceneItems方法代码如下:
/// <summary>
/// 收集场景中的所有物品信息
/// </summary>
private void GetAllSceneItems()
{
List<SceneItem> currentSceneItems = new List<SceneItem>();
//遍历找到场景中的每一个挂载Item脚本的物体
foreach (var item in FindObjectsOfType<Item>())
{
SceneItem sceneItem = new SceneItem
{
itemID = item.itemID,
position = new SerializableVector3(item.transform.position)
};
currentSceneItems.Add(sceneItem);
}
//判断是否该场景中已经存储了物品信息,如果存储了,那么更新这个存储的信息,如果为新的场景,那么将物品信息添加到字典中
if (sceneItemDict.ContainsKey(SceneManager.GetActiveScene().name))
{
sceneItemDict[SceneManager.GetActiveScene().name] = currentSceneItems;
}
else
{
sceneItemDict.Add(SceneManager.GetActiveScene().name, currentSceneItems);
}
}
接着我们需要在跳转场景前执行这个方法就可以了,我们除了这个功能之外,还要在跳转场景之后将所有的物品都加载出来的功能。
我们编写RecreateAllItems方法,用来重新创建该场景的物品。我们首先新建一个List< SceneItem >数组,但是我们其实还有一个问题就是如果我们在字典中找不到该场景的物品信息嘞,为了防止报空,我们使用字典的TryGetValue方法,这个方法会返回一个bool值,并且如果返回值为true(字典中有当前场景的物品),那么会将物品信息数组赋值到新建的数组中,所以我们需要判断该数组是否为空,如果不为空的话,则代表我们当前保存了该场景的物品信息,所以我们要先清场,将所有挂载Item脚本的物品销毁;接着我们就根据数组中的物品信息将物品重新生成就可以了。
ItemManager脚本的RecreateAllItems方法代码如下:
/// <summary>
/// 刷新重建当前场景
/// </summary>
private void RecreateAllItems()
{
List<SceneItem> currentSceneItems = new List<SceneItem>();
if (sceneItemDict.TryGetValue(SceneManager.GetActiveScene().name, out currentSceneItems))
{
if (currentSceneItems != null)
{
//清空所有的物品
foreach (var item in FindObjectsOfType<Item>())
{
Destroy(item.gameObject);
}
foreach (var item in currentSceneItems)
{
Item newItem = Instantiate(itemPrefab, item.position.ToVector3(), Quaternion.identity, itemParent);
newItem.Init(item.itemID);
}
}
}
}
我们也是需要在加载场景后的方法中调用RecreateAllItems方法即可。
ItemManager脚本新添加的代码如下:
namespace MFarm.Inventory
{
public class ItemManager : MonoBehaviour
{
public Item itemPrefab;
private Transform itemParent;
//记录场景物品,
private Dictionary<string, List<SceneItem>> sceneItemDict = new Dictionary<string, List<SceneItem>>();
private void OnEnable()
{
EventHandler.InstantiateItemInScene += OnInstantiateItemInScene;
EventHandler.BeforeSceneUnloadEvent += OnBeforeSceneUnloadEvent;
EventHandler.AfterSceneLoadedEvent += OnAfterSceneLoadedEvent;
}
private void OnDisable()
{
EventHandler.InstantiateItemInScene -= OnInstantiateItemInScene;
EventHandler.BeforeSceneUnloadEvent -= OnBeforeSceneUnloadEvent;
EventHandler.AfterSceneLoadedEvent -= OnAfterSceneLoadedEvent;
}
private void OnBeforeSceneUnloadEvent()
{
GetAllSceneItems();
}
private void OnAfterSceneLoadedEvent()
{
itemParent = GameObject.FindWithTag("ItemParent").transform;
RecreateAllItems();
}
/// <summary>
/// 收集场景中的所有物品信息
/// </summary>
private void GetAllSceneItems()
{
List<SceneItem> currentSceneItems = new List<SceneItem>();
//遍历找到场景中的每一个挂载Item脚本的物体
foreach (var item in FindObjectsOfType<Item>())
{
SceneItem sceneItem = new SceneItem
{
itemID = item.itemID,
position = new SerializableVector3(item.transform.position)
};
currentSceneItems.Add(sceneItem);
}
//判断是否该场景中已经存储了物品信息,如果存储了,那么更新这个存储的信息,如果为新的场景,那么将物品信息添加到字典中
if (sceneItemDict.ContainsKey(SceneManager.GetActiveScene().name))
{
sceneItemDict[SceneManager.GetActiveScene().name] = currentSceneItems;
}
else
{
sceneItemDict.Add(SceneManager.GetActiveScene().name, currentSceneItems);
}
}
/// <summary>
/// 刷新重建当前场景
/// </summary>
private void RecreateAllItems()
{
List<SceneItem> currentSceneItems = new List<SceneItem>();
if (sceneItemDict.TryGetValue(SceneManager.GetActiveScene().name, out currentSceneItems))
{
if (currentSceneItems != null)
{
//清空所有的物品
foreach (var item in FindObjectsOfType<Item>())
{
Destroy(item.gameObject);
}
foreach (var item in currentSceneItems)
{
Item newItem = Instantiate(itemPrefab, item.position.ToVector3(), Quaternion.identity, itemParent);
newItem.Init(item.itemID);
}
}
}
}
}
}
Destroy物品时记得一定要删除其物品的gameobject。
运行Unity,你会发现切换场景之后,会重新生成物品,但是这个物品还是会删一下,你应该已经想到了,是调用顺序的问题,我们可以将进入新场景的通知在加载界面消失之前调用即可。
TransitionManager脚本的更改如下:
private IEnumerator Transition(string sceneName, Vector3 targetPosition)
{
EventHandler.CallBeforeSceneUnloadEvent();
yield return Fade(1);
yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene());
yield return LoadSceneSetActive(sceneName);
//切换场景之后需要将角色的坐标移动到合适的位置啦
EventHandler.CallMoveToPosition(targetPosition);
EventHandler.CallAfterSceneLoadedEvent();
yield return Fade(0);
}
二、设置鼠标指针根据物品调整
本小节我们来设置鼠标图片的切换,当我们进行浇水,建造,种植等事情时,鼠标会有不同的图标。
首先我们需要导入鼠标图片的素材包,导入之后调整所有图片的最大尺寸,以及Pixels Per Unit等参数的大小,参数如下所示。
其实我们可以更改图片的类型(Texture Type)为Cursor,可直接设置为鼠标指针,但是如果这么做的话,我们则无法改变Cursor的大小,但是我们如果希望Cursor有别的效果:缩小,切换图片,动画等,那么就还是不这么干比较好。
我们将Fade Panel的父物体改名为Cursor Canvas,并给其添加子物体Image以及给这个Canvas 添加 Cursor Canvas的标签;然后将cursor(11)作为Image的Source Image,调整Image的大小,高度及锚点位置等参数,使其刚好和鼠标指向的位置相同。
Cursor Canvas的参数构造如下:
Cursor Image的参数如下:
接着我们在PersistentScene场景中添加空物体CursorManager,并创建Scripts->Cursor->CursorManager脚本并添加到这个空物体上。
然后我们来编辑CursorManager脚本,首先声明需要使用的图片,例如正常图片,使用工具鼠标图片,使用种子鼠标图片,还需要一个变量currentSprite用来存储当前使用的鼠标图片,接着需要定义拿到Cursor Image组件的变量cursorImage,最后我们还需要声明其父物体的位置cursorCanvas,并在Start方法中获得这个组件的位置(通过标签)。记得返回Unity将变量进行赋值。
UI组件的Transfom组件都是Rect Transform。
接着我们可以通过父物体获取到子物体Cursor Image(记得看把Cursor Image放到其第一个子物体的位置)。
然后我们编写SetCursorImage方法改变鼠标的图片显示并设置其颜色完全显示即可。(当鼠标不能使用时切换为红色的半透明,这个功能之后可以实现),然后在Start方法中将鼠标设置为normal图标。接着我们也在Update方法中使Image跟随鼠标移动(Image的位置我们之前已经设置了,所以现在我们鼠标点击的位置就是鼠标图片点击的位置)
CursorManager脚本的SetCursorImage方法代码如下:
private void SetCursorImage(Sprite sprite)
{
cursorImage.sprite = sprite;
cursorImage.color = new Color(1, 1, 1, 1);
}
我们选择背包物品时鼠标就会切换成相应的图标,这是就需要使用EventHandler的事件了,我们调用选中背包UI的事件,编写OnItemSelectedEvent方法;我们使用新的语法糖来写,根据itemDetails的属性来选择当前的鼠标图片属性currentSprite,同时我们还要对isSelected变量进行判断,如果选中UI的话,那么可以进行语法糖的使用,如果为false,那么将currentSprite设置为normal图片即可。
OnItemSelectedEvent方法代码如下:
private void OnItemSelectedEvent(ItemDetails itemDetails, bool isSelected)
{
if (!isSelected)
{
currentSprite = normal;
}
else
{
//添加所有类型对应图片
currentSprite = itemDetails.itemType switch
{
ItemType.Seed => seed,
ItemType.Commodity => item,
ItemType.ChopTool => tool,
_ => normal
};
}
}
因为我们的这个更换是UI被选择之后,所以我们需要在Update方法中实时调用SetCursorImage方法从而实现鼠标图片可以实时切换。
最后我们选中物品后,在与其他UI互动时,还是应该使用normal而不是使用其他的图标,这个我们编写InteractWithUI方法(返回值为bool类型),同时添加新的命名空间using UnityEngine.EventSystems;首先我么你需要判断EventSystem是否为空并且是否跟UI的物品有互动,如果不为空并且与UI发生互动,那么返回true,反之返回false。
我们还要在Update方法中调用和这个方法,如果为true,那么直接设置鼠标图片为normal,反之才试试切换鼠标图片。
CursorManager脚本的代码如下:
public class CursoeManager : MonoBehaviour
{
public Sprite normal, tool, seed, item;
//存储当前图片
private Sprite currentSprite;
private Image cursorImage;
private RectTransform cursorCanvas;
private void Start()
{
cursorCanvas = GameObject.FindGameObjectWithTag("CursorCanvas").GetComponent<RectTransform>();
cursorImage = cursorCanvas.GetChild(0).GetComponent<Image>();
currentSprite = normal;
SetCursorImage(normal);
}
private void Update()
{
if (cursorImage == null) return;
cursorImage.transform.position = Input.mousePosition;
if (!InteractWithUI())
{
SetCursorImage(currentSprite);
}
else
{
SetCursorImage(normal);
}
}
private void SetCursorImage(Sprite sprite)
{
cursorImage.sprite = sprite;
cursorImage.color = new Color(1, 1, 1, 1);
}
private void OnEnable()
{
EventHandler.ItemSelectedEvent += OnItemSelectedEvent;
}
private void OnDisable()
{
EventHandler.ItemSelectedEvent -= OnItemSelectedEvent;
}
private void OnItemSelectedEvent(ItemDetails itemDetails, bool isSelected)
{
if (!isSelected)
{
currentSprite = normal;
}
else
{
//添加所有类型对应图片
currentSprite = itemDetails.itemType switch
{
ItemType.Seed => seed,
ItemType.Commodity => item,
ItemType.ChopTool => tool,
_ => normal
};
}
}
/// <summary>
/// 判断是否跟UI互动
/// </summary>
/// <returns></returns>
private bool InteractWithUI()
{
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
{
return true;
}
else
return false;
}
}