系列文章目录
麦田物语第二十一天
文章目录
一、设置鼠标可用状
上节课我们获取了鼠标所在的网格坐标,这节课我们可以根据网格坐标,利用GridManager脚本中的字典判断鼠标当前所有的网格位置是否可用,那么接下来我们来实现这个功能。
首先我们在CursorManager脚本中编写两个函数方法,即鼠标可用SetCursorVaild和鼠标不可用SetCursorInVaild并且声明一个bool类型的变量cursorPositionValid,用于判断鼠标当前的位置是否可用;鼠标可用时正常显示,并将cursorPositionValid设置为true,不可用时显示为半透明的红色将cursorPositionValid设置为false。
CursorManager脚本中SetCursorVaild方法和SetCursorInVaild方法代码如下:
/// <summary>
/// 设置鼠标可用
/// </summary>
private void SetCursorVaild()
{
cursorImage.color = new Color(1, 1, 1, 1);
cursorPositionValid = true;
}
/// <summary>
/// 设置鼠标不可用
/// </summary>
private void SetCursorInVaild()
{
cursorImage.color = new Color(1, 0, 0, 0.4f);
cursorPositionValid = false;
}
之后鼠标可用还是不可用调用这两个方法就好了。
现在我们就需要使用GridManager里面的地形情况来判断是否可用了。我们这里想到的方法是根据网格坐标获取该网格坐标的信息,在根据这个信息进行总体判断,那么我们首先需要将鼠标的网格信息传到GridManager脚本中,在里面的一个函数返回TileDetails的信息。
那么我们先编写这个函数GetTileDetailsOnMousePosition,根据字典的键(还记得上节课咱们说的键是什么吧!!!),得到键之后返回这个键所对应的值,一定要直接调用GetTileDetails,而不是返回字典里面的值(因为这个值很可能为空,运行是会出现报空的情况)。
GridManager脚本的GetTileDetailsOnMousePosition方法代码如下:
/// <summary>
/// 根据网格坐标返回瓦片信息
/// </summary>
/// <param name="mouseGridPos">鼠标网格坐标</param>
/// <returns></returns>
public TileDetails GetTileDetailsOnMousePosition(Vector3Int mouseGridPos)
{
string key = mouseGridPos.x + "x" + mouseGridPos.y + "y" + SceneManager.GetActiveScene().name;
return GetTileDetails(key);
}
我们接下来就可以调用这个方法了,但是我们怎么调用这个方法呢,我们将GridManager改为单例模式,因为这个脚本挂载的物体始终存在于Persistent场景中,不会发生改变,切换场景也不会丢失;并且在CursorManager脚本中引入MFarm.Map的命名空间,这样就可以调用了。
我们在CursorManager脚本中CheckCursorValid方法下面获取鼠标所在网格的网格信息,获取完了网格信息之后,我们其实获取它的网格信息是我们在选择完物体之后判断这个物体是否可以丢弃之类的,那么我们要进行这个判断的前提就是这个物体是可以丢弃的,所以我们必须获取到物品信息,在这个脚本中我们没有选择物品信息的引用,但是在我们调用的ItemSelectedEvent事件时会传递过来选中的物品信息,我们将这个物品信息进行保存;首先我们声明当前选择的物品信息currentItem,并且在OnItemSelectedEvent方法中对其进行赋值(此处会有问题,之后会有解释),然后我们返回CheckCursorValid方法,对currentItem的类型进行判断,如果为商品,就判断当前鼠标所在的网格是否可以丢弃,如果位于可丢弃的网格并且该物品可以丢弃的话,将鼠标设置为有效,即调用SetCursorVaild()方法,反之调用SetCursorInVaild()方法,返回Unity运行,好,你现在会发现很多报错,哈哈哈。
我们现在先解决这些报错,我们报错信息显示的是currentItem为空,因为我们上面提到了那个问题,不能在OnItemSelectedEvent直接对其进行赋值,因为如果我们isSelected为true,代表选中了,但是如果isSelected为false的话,代表未选中,就不能对currentItem进行赋值,所以我们在isSelected为true时对其赋值即可,并将cursorEnable设置为true,没有被选中时将currentItem赋值为空,并将cursorEnable设置为false,使其不设置鼠标图片(Update方法中)。
现在我们重新运行游戏,仍然有一个报空信息,发现当我们未选中时,仍然会执行下面的代码:
if (!InteractWithUI() && cursorEnable)
{
SetCursorImage(currentSprite);
CheckCursorValid();
}
我们按照逻辑来分析,因为我们只要选中物体时cursorEnable才是true,此时将currentItem进行赋值(一定不为空),这样才会执行CheckCursorValid方法,此时怎么都不为空的嘞,结果报空却还是因为CheckCursorValid方法中的currentItem,原因就是我们本来在OnAfterSceneLoadedEvent方法中对cursorEnable这个bool值也进行了更改,所以导致其可以执行CheckCursorValid方法,我们只需要删除cursorEnable = true;这句即可。只有我们选择物品之后才能判断CheckCursorValid方法可不可以执行。
CursorManager脚本中的OnAfterSceneLoadedEvent方法错误代码:
private void OnAfterSceneLoadedEvent()
{
currentGrid = FindObjectOfType<Grid>();
cursorEnable = true;
}
现在返回Unity就没有报错了,但是我们将鼠标移动到不可丢弃的区域,发现无法出现我们想要的效果(红色半透明),那是因为我们在房子的位置并未画区域标记其是否为true或者false,所以其无法执行switch语句,我们只需要在currentTile为空时在执行SetCursorInVaild就可以了。
但是我们忽略了一个就是我们其实丢物品时不能随意丢到地图上的任意一个位置,而是我们定义了一个使用范围(Use Radius),当在这个使用范围之内时我们可以丢弃,鼠标有效,在这个范围之外时将鼠标设置为无效即可;所以我们首先需要获取Player的坐标(利用FindObjectofType),接着在CheckCursorVaild方法中获取人物的网格坐标,判断鼠标和人物的距离如果超过使用范围的话,就将鼠标设置为无效并直接return。返回Unity,就实现了我们想要的效果。
CursorManager脚本的代码如下:
public class CursorManager : MonoBehaviour
{
public Sprite normal, tool, seed, item;
//存储当前图片
private Sprite currentSprite;
private Image cursorImage;
private RectTransform cursorCanvas;
//鼠标检测
//屏幕坐标切换为世界坐标就是需要调用mainCamera
private Camera mainCamera;
//将屏幕坐标转化为网格坐标需要拿到Grid,切换场景时要切换成当前场景的Grid
private Grid currentGrid;
private Vector3 mouseWorldPos;
private Vector3Int mouseGridPos;
private bool cursorEnable;
//鼠标在当前位置是否可用
private bool cursorPositionValid;
//存储当前选择的物品信息
private ItemDetails currentItem;
//为了获取玩家周围的Tile
private Transform playerTransform => FindObjectOfType<Player>().transform;
private void Start()
{
cursorCanvas = GameObject.FindGameObjectWithTag("CursorCanvas").GetComponent<RectTransform>();
cursorImage = cursorCanvas.GetChild(0).GetComponent<Image>();
currentSprite = normal;
SetCursorImage(normal);
//MainCamera一定是被标记为MainCamera的相机
mainCamera = Camera.main;
}
private void Update()
{
if (cursorImage == null) return;
cursorImage.transform.position = Input.mousePosition;
if (!InteractWithUI() && cursorEnable)
{
SetCursorImage(currentSprite);
CheckCursorValid();
}
else
{
SetCursorImage(normal);
}
}
private void OnEnable()
{
EventHandler.ItemSelectedEvent += OnItemSelectedEvent;
EventHandler.BeforeSceneUnloadEvent += OnBeforeSceneUnloadEvent;
EventHandler.AfterSceneLoadedEvent += OnAfterSceneLoadedEvent;
}
private void OnDisable()
{
EventHandler.ItemSelectedEvent -= OnItemSelectedEvent;
EventHandler.BeforeSceneUnloadEvent -= OnBeforeSceneUnloadEvent;
EventHandler.AfterSceneLoadedEvent -= OnAfterSceneLoadedEvent;
}
private void OnItemSelectedEvent(ItemDetails itemDetails, bool isSelected)
{
if (!isSelected)
{
currentItem = null;
cursorEnable = false;
currentSprite = normal;
}
else
{
currentItem = itemDetails;
//添加所有类型对应图片
currentSprite = itemDetails.itemType switch
{
ItemType.Seed => seed,
ItemType.Commodity => item,
ItemType.ChopTool => tool,
ItemType.HoeTool => tool,
ItemType.WaterTool => tool,
ItemType.BreakTool => tool,
ItemType.ReapTool => tool,
ItemType.Furniture => tool,
_ => normal,
};
cursorEnable = true;
}
}
/// <summary>
/// 判断是否跟UI互动
/// </summary>
/// <returns></returns>
private bool InteractWithUI()
{
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
{
return true;
}
else
return false;
}
private void OnBeforeSceneUnloadEvent()
{
cursorEnable = false;
}
private void OnAfterSceneLoadedEvent()
{
currentGrid = FindObjectOfType<Grid>();
}
#region 设置鼠标样式
/// <summary>
/// 设置鼠标图片
/// </summary>
/// <param name="sprite"></param>
private void SetCursorImage(Sprite sprite)
{
cursorImage.sprite = sprite;
cursorImage.color = new Color(1, 1, 1, 1);
}
/// <summary>
/// 设置鼠标可用
/// </summary>
private void SetCursorVaild()
{
cursorImage.color = new Color(1, 1, 1, 1);
cursorPositionValid = true;
}
/// <summary>
/// 设置鼠标不可用
/// </summary>
private void SetCursorInVaild()
{
cursorImage.color = new Color(1, 0, 0, 0.4f);
cursorPositionValid = false;
}
#endregion
private void CheckCursorValid()
{
mouseWorldPos = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y,-mainCamera.transform.position.z));
mouseGridPos = currentGrid.WorldToCell(mouseWorldPos);
var playerGridPos = currentGrid.WorldToCell(playerTransform.position);
//判断是否在适用范围内
if (Mathf.Abs(mouseGridPos.x - playerGridPos.x) > currentItem.itemUseRadius || Mathf.Abs(mouseGridPos.y - playerGridPos.y) > currentItem.itemUseRadius)
{
SetCursorInVaild();
return;
}
//Debug.Log(mouseGridPos);
TileDetails currentTile = GridMapManager.Instance.GetTileDetailsOnMousePosition(mouseGridPos);
if (currentTile != null)
{
switch (currentItem.itemType)
{
case ItemType.Commodity:
if (currentTile.canDropItem && currentItem.canDropped) SetCursorVaild(); else SetCursorInVaild();
break;
}
}
else
{
SetCursorInVaild();
}
}
}
二、实现鼠标选中物品后的场景点击事件流程
我们这节课想要实现的功能是当我们选择背包中的物品之后,在可点选的位置点击鼠标,完成丢弃,砍树等功能,包括人物的动画和动画结束后物品的效果。
1.CursorManager的CheckPlayerInput
首先我们需要在CursorManager脚本的CheckPlayerInput方法中检测鼠标的左键点击并且鼠标位置是有效的,我们就要执行很多方法,我们创建新的事件来通知需要执行的方法(EventHandler脚本中MouseClickedEvent事件);接着我们呼叫CallMouseClickedEvent,将鼠标的世界坐标和物品信息传递过去即可。当我们点击鼠标后,在Player脚本中需要添加这个事件方法OnMouseClickedEvent,这个事件的作用是执行动画并且调用之后的事件(之后又需要返回EventHandler编写新的事件CallExecuteActionAfterAnimation)。CheckPlayerInput该方法在Update中调用。
CursorManager的CheckPlayerInput代码如下:
private void CheckPlayerInput()
{
if (Input.GetMouseButtonDown(0) && cursorPositionValid)
{
//执行方法
EventHandler.CallMouseClickedEvent(mouseWorldPos, currentItem);
}
}
2.EventHandler
(1)CallMouseClickedEvent
我们首先需要编写的事件需要传递两个参数,分别是位置和物品信息,当鼠标点击后调用这个事件。
EventHandler脚本的CallMouseClickedEvent事件代码如下:
//选中物品之后点按鼠标触发的事件
public static event Action<Vector3, ItemDetails> MouseClickedEvent;
public static void CallMouseClickedEvent(Vector3 pos, ItemDetails itemDetails)
{
MouseClickedEvent?.Invoke(pos, itemDetails);
}
(2)CallExecuteActionAfterAnimation
该事件的参数与CallMouseClickedEvent的参数相同,表明在动画调用之后执行的事件。
EventHandler脚本的CallExecuteActionAfterAnimation事件代码如下:
//执行动作之后的事件(在上一个事件之后)
public static event Action<Vector3, ItemDetails> ExecuteActionAfterAnimation;
public static void CallExecuteAfterAnimation(Vector3 pos, ItemDetails itemDetails)
{
ExecuteActionAfterAnimation?.Invoke(pos, itemDetails);
}
(3)CallDropItemEvent
在EventHandler中添加CallDropItemEvent,参数与CallInstantiateItemInScene相同,只不过该方法用于丢弃物品(下节课实现扔东西的效果)
EventHandler脚本中CallDropItemEvent代码如下
public static event Action<int, Vector3> DropItemEvent;
public static void CallDropItemEvent(int ID, Vector3 pos)
{
DropItemEvent?.Invoke(ID, pos);
}
接着返回ItemManager脚本中
3.Player
Player脚本中的OnMouseClickedEvent方法一定要保证执行顺序:先执行动画(本节课先不执行动画),在调用其他事件CallExecuteActionAfterAnimation,我们这个事件的功能是在对应的地图位置生成物品,这个就需要在GridMapManager脚本中去调用,因为很多动作都涉及到更改地图的信息(接着到4)
Player脚本的OnMouseClickedEvent方法代码如下:
private void OnMouseClickedEvent(Vector3 pos, ItemDetails itemDetails)
{
//TODO:执行动画
EventHandler.CallExecuteAfterAnimation(pos, itemDetails);
}
4.GridMapManager的OnExecuteActionAfterAnimation
我们开始在GridMapManager脚本中进行事件注册并编写OnExecuteActionAfterAnimator方法(这个事件也可以在CursorManager中执行),接着编写OnExecuteActionAfterAnimator方法,我们需要拿到鼠标的Grid坐标,那么我们就需要定义当前地图的Grid,这个Gird需要在切换地图后进行获取,因此我们需要调用AfterSceneLoadedEvent事件并编写AfterSceneLoadedEvent,在该方法中获取当前地图的Grid即可;
得到当前场景的Grid后,我们获取鼠标的Grid坐标,有了我们鼠标所在的格子坐标,我们就可以获得鼠标所在的网格了currentTile(调用GridMapManager的GetTileDetailsOnMousePosition方法),我们接着进行一系列的判断和选择,
首先我们判断获取到的currentTile是否为空,如果不为空的话,进行switch的选择,对物品的功能进行选择(注意这个函数之后前代表鼠标一定位于可点击的位置,故不用再进行位置可行性判断),如果该物品为商品,我们直接在鼠标点击的位置生成该物品,我们在ItemManager脚本中调用了这个事件InstantiateItemInScene,但是调用之后我们出现了很多问题:我们丢弃物品时背包中的物品数量没有减少,并且没有丢东西的感觉。那么接下来我们来解决这个问题。
我们现在要编写一个方法,当我们丢弃物品时我们从Inventory中将该物品移除,因为我们想要移除物品,所以不能用OnInstantiateItemInScene方法生成物品,那么我们重新编写一个事件和方法(转到2-》(3))。
GridMapManager脚本的新增代码:
namespace MFarm.Map
{
public class GridMapManager : Singleton<GridMapManager>
{
[Header("地图信息")]
public List<MapData_SO> mapDataList;
//场景名字+坐标和对应的瓦片信息
private Dictionary<string, TileDetails> tileDetailsDict = new Dictionary<string, TileDetails>();
private Grid currentGrid;
private void OnEnable()
{
EventHandler.ExecuteActionAfterAnimation += OnExecuteActionAfterAnimator;
EventHandler.AfterSceneLoadedEvent += OnAfterSceneLoadEvent;
}
private void OnDisable()
{
EventHandler.ExecuteActionAfterAnimation -= OnExecuteActionAfterAnimator;
EventHandler.AfterSceneLoadedEvent -= OnAfterSceneLoadEvent;
}
/// <summary>
/// 根据网格坐标返回瓦片信息
/// </summary>
/// <param name="mouseGridPos">鼠标网格坐标</param>
/// <returns></returns>
public TileDetails GetTileDetailsOnMousePosition(Vector3Int mouseGridPos)
{
string key = mouseGridPos.x + "x" + mouseGridPos.y + "y" + SceneManager.GetActiveScene().name;
return GetTileDetails(key);
}
private void OnAfterSceneLoadEvent()
{
currentGrid = FindObjectOfType<Grid>();
}
/// <summary>
/// 在世界地图上执行实际工具或者物品功能
/// </summary>
/// <param name="mouseWorldPos">鼠标坐标</param>
/// <param name="itemDetails">物品信息</param>
private void OnExecuteActionAfterAnimator(Vector3 mouseWorldPos, ItemDetails itemDetails)
{
var mouseGridPos = currentGrid.WorldToCell(mouseWorldPos);
var currentTile = GetTileDetailsOnMousePosition(mouseGridPos);
if (currentTile != null)
{
//WORKFLOW:物品使用实际功能
switch (itemDetails.itemType)
{
case ItemType.Commodity:
EventHandler.CallDropItemEvent(itemDetails.itemID, mouseWorldPos);
break;
}
}
}
}
}
5.ItemManager的OnDropItemEvent
我们在ItemManager注册DropItemEvent事件并添加OnDropItemEvent方法,编写OnDropItemEvent方法,我们本节课不实现扔东西的效果,现在这个方法的代码与OnInstantiateItemInScene相同。
ItemManager的新增代码如下:
private void OnEnable()
{
EventHandler.InstantiateItemInScene += OnInstantiateItemInScene;
EventHandler.DropItemEvent += OnDropItemEvent;
EventHandler.BeforeSceneUnloadEvent += OnBeforeSceneUnloadEvent;
EventHandler.AfterSceneLoadedEvent += OnAfterSceneLoadedEvent;
}
private void OnDisable()
{
EventHandler.InstantiateItemInScene -= OnInstantiateItemInScene;
EventHandler.DropItemEvent -= OnDropItemEvent;
EventHandler.BeforeSceneUnloadEvent -= OnBeforeSceneUnloadEvent;
EventHandler.AfterSceneLoadedEvent -= OnAfterSceneLoadedEvent;
}
private void OnDropItemEvent(int ID, Vector3 pos)
{
//TODO:扔东西的效果
var item = Instantiate(itemPrefab, pos, Quaternion.identity, itemParent);
item.itemID = ID;
}
我们接着要实现背包物品的减少,因此到InventoryManager脚本也注册这个事件(转到6)
6.InventoryManager的RemoveItem
注册完DropItemEvent事件后添加OnDropItemEvent方法,编写OnDropItemEvent方法,这个方法的作用就是从背包中移除一个物品,那么我们添加移除物品的方法RemoveItem,我们通过GetItemIndexInBag方法通过物品的ID找到物品的序号,然后如果其数量大于我们想要移除的数量,就直接减去需要移除的数量,将其生成一个ID和相减后数量的InventoryItem并进行赋值。。如果数量等于我们想要移除的数量,将其生成一个空的InventoryItem并进行赋值,最后刷新UI即可。
我们在OnDropItemEvent调用RemoveItem方法,并返回GridMapManager脚本的OnExecuteActionAfterAnimator调用CallDropItemEvent方法;目前看来我们已经实现了这个功能,返回Unity进行测试,可以实现物品的丢弃,但是当我们扔完了之后还是选择了空的物品栏,本来应该需要的效果是高亮显示消失并且任务的动作为初始动作,我们点开SlotUI脚本更改一下之前的UpdateEmptySlot方法(转到7)。
InventoryManager脚本的RemoveItem方法代码如下:
/// <summary>
/// 移除指定数量的背包物品
/// </summary>
/// <param name="ID">物品ID</param>
/// <param name="removeAmount">移除物品的数量</param>
private void RemoveItem(int ID, int removeAmount)
{
var index = GetItemIndexInBag(ID);
if (playerBag_SO.itemList[index].itemAmount > removeAmount)
{
var amount = playerBag_SO.itemList[index].itemAmount - removeAmount;
var item = new InventoryItem { itemID = ID, itemAmount = amount };
playerBag_SO.itemList[index] = item;
}
else if (playerBag_SO.itemList[index].itemAmount == removeAmount)
{
var item = new InventoryItem();
playerBag_SO.itemList[index] = item;
}
//更新UI
EventHandler.CallUpdateInventoryUI(InventoryLocation.Player, playerBag_SO.itemList);
}
7.SlotUI的UpdateEmptySlot
在UpdateEmptySlot方法中,当我们清空物品时,如果其在被选择状态,我们更新InventoryUI的高亮显示,并且将当前格子SlotUI的ItemDetails设置为null,返回Unity重新测试,发现还是有报空 ,错误原因是啥呢,在SlotUI脚本的Start方法中我们会调用UpdateEmptySlot方法,但是我们这个if语句判断的是itemDetails的数量为0,此时应该改为itemDetails改为空作为判断条件,并将所有的itemAmount为空的判断全部改为itemDetails为null即可。同时更新UpdateEmptySlot方法,当该格子为空时,我们呼叫物品选择的事件,因为此时isSelected为false,那么我们就可以调整人物的动画和鼠标的状态了(这个我当时也不知道为啥,你看一下代码就会发现为啥会这样了)。
SlotUI脚本更改的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using MFram.Inventory;
namespace MFarm.Inventory
{
public class SlotUI : MonoBehaviour,IPointerClickHandler,IBeginDragHandler,IDragHandler,IEndDragHandler
{
/// <summary>
/// 将Slot更新为空
/// </summary>
public void UpdateEmptySlot()
{
if (isSelected)
{
isSelected = false;
inventoryUI.UpdateSlotHightlight(-1);
EventHandler.CallItemSelectedEvent(itemDetails, isSelected);
}
itemDetails = null;
slotImage.enabled = false;
amountText.text = string.Empty;
button.interactable = false;
}
public void OnPointerClick(PointerEventData eventData)
{
if (itemDetails == null) return;
isSelected = !isSelected;
inventoryUI.UpdateSlotHightlight(slotIndex);
if (slotType == SlotType.Bag)
{
//通知物品被选中的状态和信息
EventHandler.CallItemSelectedEvent(itemDetails, isSelected);
}
}
}
}
这样调整完之后还会有一个出错,就是ShowItemTooltip脚本中的OnPointerEnter 方法中的判断条件由slotUi.itemAmount != 0 改为slotUI.itemDetails !=null即可。
ShowItemTooltip脚本更改的代码如下:
public void OnPointerEnter(PointerEventData eventData)
{
if (slotUI.itemDetails != null)
{
inventoryUI.itemTooltip.gameObject.SetActive(true);
inventoryUI.itemTooltip.SetupTooltip(slotUI.itemDetails, slotUI.slotType);
inventoryUI.itemTooltip.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 0);
inventoryUI.itemTooltip.transform.position = transform.position + Vector3.up * 60;
}
else
{
inventoryUI.itemTooltip.gameObject.SetActive(false);
}
}
为什么要进行上述的更改嘞,因为我们在SlotUI的Start方法中调用了UpdateEmptySlot方法,我们在该方法中将itemDetails设置为null,该变量为空,其itemAmount也为空而不是0,所以我们用itemAmount == 0的判断就会报空了。
现在返回Unity运行我们就可以实现想要的效果了。(终于完了!!!)