1、目标
当鼠标移动到reapable scenary(可收割庄稼)上方时,光标会变成十字架。
之前章节中,Grid有Dug/Watered属性,光标移动上方时会显示方框。
而这次的功能并非基于Grid的属性,而是基于scenary(庄稼)的属性。
2、概念
(1)非基于网格的光标的原因
我们的一些游戏逻辑是基于网格方格和网格属性的:
-- 例如地面网格方格,以及它们是否被挖掘和浇水
-- 种植的作物-------每个网格方格只能种植一种作物
但其他游戏逻辑和物体并不局限于网格:
-- 例如可以丢弃和捡起的物品。如果每个网格方格只能放置一个物品,游戏就会显得非常受限。
-- 还有像草这种“可收割”的场景元素 ----- 草可以紧密放置。
所以对于草,我们要用镰刀来收割。这意味着我们需要一个非基于网格的光标,以指示我们是否在有效范围内,以及是否可以对草执行有效的“收割”动作。
(2)计算光标影响的区域
由于现在光标不基于网格工作,需要计算光标影响的区域。
-- 当我们实现基于网格的光标时,我们可以轻松获取网格方格的位置,然后确定哪些游戏对象会受到影响,以及基于该网格方格或相邻网格方格会发生哪些碰撞。
-- 对于非基于网格的光标,我们需要通过考虑以下因素来计算玩家动作将影响的区域:
- 玩家自然的(x,y)中心位置
- 玩家使用工具的方向
- 任何效果的半径大小
(3)光标影响范围
对于Item,我们会定义使用的半径为r。
玩家默认的中心是pivot Point,当我们计算光标的使用半径时,我们将调整中心的位置,将Player的中心移到Player的自然中心位置。 即从绿点 移到 黄点。
(4)各方向工具的光标影响范围
1)右方
2)上方
3)左方
4)下方
所以总的光标的范围如下:
计算方法分两步:
第1步:如果光标在以下红色区域中,就设置它无效。
计算红色区域的条件如下:
第2步:如果光标在以下红色区域中,就设置它无效。
(5)使用Physics2D获取2D碰撞器
我们可以使用 Physics2D 函数来获取光标范围内任何物体的 2D 碰撞器
3、修改Settings.cs脚本
添加下面一行代码:
// Player
public static float playerCentreYOffset = 0.875f;
4、修改HelperMethods.cs脚本
添加第一个功能函数:
/// <summary>
/// Gets Components of type T at positionToCheck. Returns truue if at least one found and the found components are
/// returned in componentAtPositionList
/// </summary>
/// <param name="componentsAtPositionList"></param>
/// <param name="positionToCheck"></param>
/// <returns></returns>
public static bool GetComponentsAtCursorLocation<T>(out List<T> componentsAtPositionList, Vector3 positionToCheck)
{
bool found = false;
List<T> componentList = new List<T>();
Collider2D[] collider2DArray = Physics2D.OverlapPointAll(positionToCheck);
// Loop through all colliders to get an object of type T
T tComponent = default(T);
for(int i = 0; i < collider2DArray.Length; i++)
{
tComponent = collider2DArray[i].gameObject.GetComponentInParent<T>();
if(tComponent != null)
{
found = true;
componentList.Add(tComponent);
}
else
{
tComponent = collider2DArray[i].gameObject.GetComponentInChildren<T>();
if(tComponent != null )
{
found = true;
componentList.Add(tComponent);
}
}
}
componentsAtPositionList = componentList;
return found;
}
为什么使用GetComponentInParent<T>()和GetComponentInChildren<T>()而不使用GetComponent<T>()?
GetComponent<T>()仅会在当前游戏对象上查找指定类型的组件。也就是说,它只检查该游戏对象自身是否挂载了类型为T的组件,不会去查找其父对象或者子对象。如果要查找的组件不在当前游戏对象上,而是在其父对象或者子对象上,那么GetComponent<T>()就无法找到该组件。
GetComponentInParent<T>():该方法会在当前游戏对象及其所有父对象中查找指定类型的组件。这意味着如果要查找的组件不在当前游戏对象上,但在其父对象的层级结构中,使用GetComponentInParent<T>()就能够找到它。 GetComponentInChildren<T>():此方法会在当前游戏对象及其所有子对象中查找指定类型的组件。也就是说,如果要查找的组件不在当前游戏对象上,但在其子对象的层级结构中,使用GetComponentInChildren<T>()就可以找到它。
添加第二个功能函数:
/// <summary>
/// Returns array of components of type T at box with centre point and size and angle.
/// The numberOfCollidersToTest for is passed as a parameter.
/// Found components are returned in the array
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="numberOfCollidersToTest"></param>
/// <param name="point"></param>
/// <param name="size"></param>
/// <param name="angle"></param>
/// <returns></returns>
public static T[] GetComponentsAtBoxLocationNonAlloc<T>(int numberOfCollidersToTest, Vector2 point, Vector2 size, float angle)
{
Collider2D[] collider2DArray = new Collider2D[numberOfCollidersToTest];
Physics2D.OverlapBoxNonAlloc(point, size, angle, collider2DArray);
T tComponent = default(T);
T[] componentArray = new T[collider2DArray.Length];
for(int i = collider2DArray.Length - 1; i >= 0; i--)
{
if (collider2DArray[i] != null)
{
tComponent = collider2DArray[i].gameObject.GetComponent<T>();
if( tComponent != null)
{
componentArray[i] = tComponent;
}
}
}
return componentArray;
}
Physics2D.OverlapBoxNonAlloc也是矩形框内的碰撞体检测,与Physics2D.OverlapBoxAll的区别是不会动态分配内存,而是存放在实现定义好的数组中,即collider2DArray变量。
5、修改Player.cs脚本
添加如下方法:
public Vector3 GetPlayerCentrePosition()
{
return new Vector3(transform.position.x, transform.position.y + Settings.playerCentreYOffset, transform.position.z);
}
6、创建Cursor.cs脚本
位于Assets -> Scripts -> UI下。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Cursor : MonoBehaviour
{
private Canvas canvas;
private Camera mainCamera; // 作用:通过camera内置函数将screenpoint转为worldposition
[SerializeField] private Image cursorImage = null;
[SerializeField] private RectTransform cursorRectTransform = null;
[SerializeField] private Sprite greenCursorSprite = null; // 绿色光标
[SerializeField] private Sprite transparentCursorSprite = null; // 透明光标
[SerializeField] private GridCursor gridCursor = null; // 合适的时间调起另一个cursor
private bool _cursorIsEnable = false;
public bool CursorIsEnable { get => _cursorIsEnable; set => _cursorIsEnable = value; }
private bool _cursorPositionIsValid = false;
public bool CursorPositionIsValid { get => _cursorPositionIsValid; set => _cursorPositionIsValid = value; }
private ItemType _selectedItemType;
public ItemType SelectedItemType { get => _selectedItemType; set => _selectedItemType = value; }
private float _itemUseRadius = 0f; // 非网格使用半径
public float ItemUseRadius { get => _itemUseRadius; set => _itemUseRadius = value; }
private void Start()
{
mainCamera = Camera.main;
canvas = GetComponentInParent<Canvas>();
}
private void Update()
{
if (CursorIsEnable)
{
DisplayCursor();
}
}
private void DisplayCursor()
{
// Get position for cursor
Vector3 cursorWorldPosition = GetWorldPositionForCursor();
// Set cursor sprite
SetCursorValidity(cursorWorldPosition, Player.Instance.GetPlayerCentrePosition());
// Get rect transform position for cursor
cursorRectTransform.position = GetRectTransformPositionForCursor();
}
public Vector3 GetWorldPositionForCursor()
{
Vector3 screenPosition = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0f);
Vector3 worldPosition = mainCamera.ScreenToWorldPoint(screenPosition);
return worldPosition;
}
public Vector2 GetRectTransformPositionForCursor()
{
Vector2 screenPosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
// 获取鼠标在canvas的rectTransform中的位置
return RectTransformUtility.PixelAdjustPoint(screenPosition, cursorRectTransform, canvas);
}
private void SetCursorValidity(Vector3 cursorPosition, Vector3 playerPosition)
{
SetCursorToValid();
// Check use radius corners
if (
cursorPosition.x > (playerPosition.x + ItemUseRadius / 2f) && cursorPosition.y > (playerPosition.y + ItemUseRadius / 2f)
||
cursorPosition.x < (playerPosition.x - ItemUseRadius / 2f) && cursorPosition.y > (playerPosition.y + ItemUseRadius / 2f)
||
cursorPosition.x < (playerPosition.x - ItemUseRadius / 2f) && cursorPosition.y < (playerPosition.y - ItemUseRadius / 2f)
||
cursorPosition.x > (playerPosition.x + ItemUseRadius / 2f) && cursorPosition.y < (playerPosition.y - ItemUseRadius / 2f)
) {
SetCursorToInvalid();
return;
}
// Check item use radius is valid
if(Mathf.Abs(cursorPosition.x - playerPosition.x) > ItemUseRadius
|| Mathf.Abs(cursorPosition.y - playerPosition.y) > ItemUseRadius)
{
SetCursorToInvalid();
return;
}
// Get selected item details
ItemDetails itemDetails = InventoryManager.Instance.GetSelectedInventoryItemDetails(InventoryLocation.player);
if(itemDetails == null)
{
SetCursorToInvalid();
return;
}
// Determine cursor validity based on inventory item selected and what object the cursor is over
switch (itemDetails.itemType)
{
case ItemType.Watering_tool:
case ItemType.Breaking_tool:
case ItemType.Chopping_tool:
case ItemType.Hoeing_tool:
case ItemType.Reaping_tool:
case ItemType.Collecting_tool:
if(!SetCursorValidityTool(cursorPosition, playerPosition, itemDetails))
{
SetCursorToInvalid();
return;
}
break;
case ItemType.none:
break;
case ItemType.count:
break;
default:
break;
}
}
/// <summary>
/// Set the cursor to be valid
/// </summary>
private void SetCursorToValid()
{
cursorImage.sprite = greenCursorSprite;
CursorPositionIsValid = true;
gridCursor.DisableCursor(); // 另外一个cursor不生效,两个不要同时生效
}
/// <summary>
/// Set the cursor to be invalid
/// </summary>
private void SetCursorToInvalid()
{
cursorImage.sprite = transparentCursorSprite;
CursorPositionIsValid = false;
gridCursor.EnableCursor(); // 另外一个cursor生效
}
/// <summary>
/// Sets the cursor as either valid or invalid for the tool for the target.
/// Returns true if valid or false if invalid
/// </summary>
/// <param name="cursorPosition"></param>
/// <param name="playerPosition"></param>
/// <param name="itemDetails"></param>
/// <returns></returns>
private bool SetCursorValidityTool(Vector3 cursorPosition, Vector3 playerPosition, ItemDetails itemDetails)
{
// Switch on tool
switch(itemDetails.itemType)
{
case ItemType.Reaping_tool:
return SetCursorValidityReapingTool(cursorPosition, playerPosition, itemDetails);
default:
return false;
}
}
private bool SetCursorValidityReapingTool(Vector3 cursorPosition, Vector3 playerPosition, ItemDetails equippedItemDetails)
{
List<Item> itemList = new List<Item>();
if(HelperMethods.GetComponentsAtCursorLocation<Item>(out itemList, cursorPosition))
{
if(itemList.Count != 0)
{
foreach(Item item in itemList)
{
if(InventoryManager.Instance.GetItemDetails(item.ItemCode).itemType == ItemType.Reapable_scenary)
{
return true;
}
}
}
}
return false;
}
public void DisableCursor()
{
cursorImage.color = new Color(1f, 1f, 1f, 0f);
CursorIsEnable = false;
}
public void EnableCursor()
{
cursorImage.color = new Color(1f, 1f, 1f, 1f);
CursorIsEnable = true;
}
}
7、优化UIInventorySlot.cs脚本
添加一行代码如下:
添加一行代码如下:
添加2行代码如下:
添加多行代码如下:
完整的代码如下:
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
public class UIInventorySlot : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
private Camera mainCamera;
private Transform parentItem; // 场景中的物体父类
private GameObject draggedItem; // 被拖动的物体
private Canvas parentCanvas;
private GridCursor gridCursor;
private Cursor cursor;
public Image inventorySlotHighlight;
public Image inventorySlotImage;
public TextMeshProUGUI textMeshProUGUI;
[SerializeField] private UIInventoryBar inventoryBar = null;
[SerializeField] private GameObject itemPrefab = null;
[SerializeField] private int slotNumber = 0; // 插槽的序列号
[SerializeField] private GameObject inventoryTextBoxPrefab = null;
[HideInInspector] public ItemDetails itemDetails;
[HideInInspector] public int itemQuantity;
[HideInInspector] public bool isSelected = false;
private void Awake()
{
parentCanvas = GetComponentInParent<Canvas>();
}
private void OnDisable()
{
EventHandler.AfterSceneLoadEvent -= SceneLoaded;
EventHandler.DropSelectedItemEvent -= DropSelectedItemAtMousePosition;
}
private void OnEnable()
{
EventHandler.AfterSceneLoadEvent += SceneLoaded;
EventHandler.DropSelectedItemEvent += DropSelectedItemAtMousePosition;
}
public void SceneLoaded()
{
parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;
}
private void Start()
{
mainCamera = Camera.main;
gridCursor = FindObjectOfType<GridCursor>();
cursor = FindObjectOfType<Cursor>();
}
private void ClearCursors()
{
// Disable cursor
gridCursor.DisableCursor();
cursor.DisableCursor();
// Set item type to none
gridCursor.SelectedItemType = ItemType.none;
cursor.SelectedItemType = ItemType.none;
}
public void OnBeginDrag(PointerEventData eventData)
{
if(itemDetails != null)
{
// Disable keyboard input
Player.Instance.DisablePlayerInputAndResetMovement();
// Instatiate gameobject as dragged item
draggedItem = Instantiate(inventoryBar.inventoryBarDraggedItem, inventoryBar.transform);
// Get image for dragged item
Image draggedItemImage = draggedItem.GetComponentInChildren<Image>();
draggedItemImage.sprite = inventorySlotImage.sprite;
SetSelectedItem();
}
}
public void OnDrag(PointerEventData eventData)
{
// move game object as dragged item
if(!draggedItem != null)
{
draggedItem.transform.position = Input.mousePosition;
}
}
public void OnEndDrag(PointerEventData eventData)
{
// Destroy game object as dragged item
if (draggedItem != null)
{
Destroy(draggedItem);
// if drag ends over inventory bar, get item drag is over and swap then
if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>() != null)
{
// get the slot number where the drag ended
int toSlotNumber = eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>().slotNumber;
// Swap inventory items in inventory list
InventoryManager.Instance.SwapInventoryItems(InventoryLocation.player, slotNumber, toSlotNumber);
// Destroy inventory text box
DestroyInventoryTextBox();
// Clear selected item
ClearSelectedItem();
}
else
{
// else attemp to drop the item if it can be dropped
if (itemDetails.canBeDropped)
{
DropSelectedItemAtMousePosition();
}
}
// Enable player input
Player.Instance.EnablePlayerInput();
}
}
/// <summary>
/// Drops the item(if selected) at the current mouse position. called by the DropItem event
/// </summary>
private void DropSelectedItemAtMousePosition()
{
if(itemDetails != null && isSelected)
{
// If can drop item here
if (gridCursor.CursorPositionIsValid) {
Vector3 worldPosition = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, -mainCamera.transform.position.z));
// Create item from prefab at mouse position
GameObject itemGameObject = Instantiate(itemPrefab, new Vector3(worldPosition.x, worldPosition.y - Settings.gridCellSize/2f, worldPosition.z), Quaternion.identity, parentItem);
Item item = itemGameObject.GetComponent<Item>();
item.ItemCode = itemDetails.itemCode;
// Remove item from player's inventory
InventoryManager.Instance.RemoveItem(InventoryLocation.player, item.ItemCode);
// If no more of item then clear selected
if (InventoryManager.Instance.FindItemInInventory(InventoryLocation.player, item.ItemCode) == -1)
{
ClearSelectedItem();
}
}
}
}
public void OnPointerEnter(PointerEventData eventData)
{
// Populate text box with item details
if(itemQuantity != 0)
{
// Instantiate inventory text box
inventoryBar.inventoryTextBoxGameobject = Instantiate(inventoryTextBoxPrefab, transform.position, Quaternion.identity);
inventoryBar.inventoryTextBoxGameobject.transform.SetParent(parentCanvas.transform, false);
UIInventoryTextBox inventoryTextBox = inventoryBar.inventoryTextBoxGameobject.GetComponent<UIInventoryTextBox>();
// Set item type description
string itemTypeDescription = InventoryManager.Instance.GetItemTypeDescription(itemDetails.itemType);
// Populate text box
inventoryTextBox.SetTextboxText(itemDetails.itemDescription, itemTypeDescription, "", itemDetails.itemLongDescription, "", "");
// Set text box position according to inventory bar position
if (inventoryBar.IsInventoryBarPositionBottom)
{
inventoryBar.inventoryTextBoxGameobject.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 0f);
inventoryBar.inventoryTextBoxGameobject.transform.position = new Vector3(transform.position.x, transform.position.y + 50f, transform.position.z);
}
else
{
inventoryBar.inventoryTextBoxGameobject.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 1f);
inventoryBar.inventoryTextBoxGameobject.transform.position = new Vector3(transform.position.x, transform.position.y - 50f, transform.position.z);
}
}
}
public void OnPointerExit(PointerEventData eventData)
{
DestroyInventoryTextBox();
}
private void DestroyInventoryTextBox()
{
if (inventoryBar.inventoryTextBoxGameobject != null)
{
Destroy(inventoryBar.inventoryTextBoxGameobject);
}
}
public void OnPointerClick(PointerEventData eventData)
{
// if left click
if (eventData.button == PointerEventData.InputButton.Left)
{
// if inventory slot currently selected then deselect
if (isSelected == true)
{
ClearSelectedItem();
}
else // 未被选中且有东西则显示选中的效果
{
if(itemQuantity > 0)
{
SetSelectedItem();
}
}
}
}
/// <summary>
/// Set this inventory slot item to be selected
/// </summary>
private void SetSelectedItem()
{
// Clear currently highlighted items
inventoryBar.ClearHighlightOnInventorySlots();
// Highlight item on inventory bar
isSelected = true;
// Set highlighted inventory slots
inventoryBar.SetHighlightedInventorySlots();
// Set use radius for cursors
gridCursor.ItemUseGridRadius = itemDetails.itemUseGridRadius;
cursor.ItemUseRadius = itemDetails.itemUseRadius;
// If item requires a grid cursor then enable cursor
if(itemDetails.itemUseGridRadius > 0)
{
gridCursor.EnableCursor();
}
else
{
gridCursor.DisableCursor();
}
// If item requires a cursor then enable cursor
if(itemDetails.itemUseRadius > 0f)
{
cursor.EnableCursor();
}
else
{
cursor.DisableCursor();
}
// Set item type
gridCursor.SelectedItemType = itemDetails.itemType;
cursor.SelectedItemType = itemDetails.itemType;
// Set item selected in inventory
InventoryManager.Instance.SetSelectedInventoryItem(InventoryLocation.player, itemDetails.itemCode);
if (itemDetails.canBeCarried == true)
{
// Show player carrying item
Player.Instance.ShowCarriedItem(itemDetails.itemCode);
}
else
{
Player.Instance.ClearCarriedItem();
}
}
private void ClearSelectedItem()
{
ClearCursors();
// Clear currently highlighted item
inventoryBar.ClearHighlightOnInventorySlots();
isSelected = false;
// set no item selected in inventory
InventoryManager.Instance.ClearSelectedInventoryItem(InventoryLocation.player);
// Clear player carrying item
Player.Instance.ClearCarriedItem();
}
}
8、优化GridCursor.cs脚本
在SetCursorValidity方法中,修改case条件,添加所有的tool信息。
9、设置UI组件
1)给UIPanel对象添加Cursor组件。
2)设置Grid Cursor属性
3)在UIPanel下创建新的空对象命名为Cursor。
4)给Cursor对象添加Image组件
设置Source Image为GreenCursor
设置Color为(255, 255, 255,0)
点击Set Native Size
5)设置UIPanel中Cursor组件的其他属性如下:
10、运行游戏
点击Scythe(镰刀)工具,放在草上会显示GreenCursor的图标。