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 TT 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; // 合适的时间调起另一个cursorprivate 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 cursorVector3 cursorWorldPosition = GetWorldPositionForCursor();// Set cursor spriteSetCursorValidity(cursorWorldPosition, Player.Instance.GetPlayerCentrePosition());// Get rect transform position for cursorcursorRectTransform.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 cornersif (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 validif(Mathf.Abs(cursorPosition.x - playerPosition.x) > ItemUseRadius|| Mathf.Abs(cursorPosition.y - playerPosition.y) > ItemUseRadius){SetCursorToInvalid();return;}// Get selected item detailsItemDetails 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 overswitch (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 toolswitch(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 cursorgridCursor.DisableCursor();cursor.DisableCursor();// Set item type to nonegridCursor.SelectedItemType = ItemType.none;cursor.SelectedItemType = ItemType.none;}public void OnBeginDrag(PointerEventData eventData){if(itemDetails != null) {// Disable keyboard inputPlayer.Instance.DisablePlayerInputAndResetMovement();// Instatiate gameobject as dragged itemdraggedItem = Instantiate(inventoryBar.inventoryBarDraggedItem, inventoryBar.transform);// Get image for dragged itemImage draggedItemImage = draggedItem.GetComponentInChildren<Image>();draggedItemImage.sprite = inventorySlotImage.sprite;SetSelectedItem();}}public void OnDrag(PointerEventData eventData){// move game object as dragged itemif(!draggedItem != null){draggedItem.transform.position = Input.mousePosition;}}public void OnEndDrag(PointerEventData eventData){// Destroy game object as dragged itemif (draggedItem != null) {Destroy(draggedItem);// if drag ends over inventory bar, get item drag is over and swap thenif (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>() != null) {// get the slot number where the drag endedint toSlotNumber = eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>().slotNumber;// Swap inventory items in inventory listInventoryManager.Instance.SwapInventoryItems(InventoryLocation.player, slotNumber, toSlotNumber);// Destroy inventory text boxDestroyInventoryTextBox();// Clear selected itemClearSelectedItem();}else{// else attemp to drop the item if it can be droppedif (itemDetails.canBeDropped){DropSelectedItemAtMousePosition();}}// Enable player inputPlayer.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 hereif (gridCursor.CursorPositionIsValid) {Vector3 worldPosition = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, -mainCamera.transform.position.z));// Create item from prefab at mouse positionGameObject 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 inventoryInventoryManager.Instance.RemoveItem(InventoryLocation.player, item.ItemCode);// If no more of item then clear selectedif (InventoryManager.Instance.FindItemInInventory(InventoryLocation.player, item.ItemCode) == -1){ClearSelectedItem();}}}}public void OnPointerEnter(PointerEventData eventData){// Populate text box with item detailsif(itemQuantity != 0){// Instantiate inventory text boxinventoryBar.inventoryTextBoxGameobject = Instantiate(inventoryTextBoxPrefab, transform.position, Quaternion.identity);inventoryBar.inventoryTextBoxGameobject.transform.SetParent(parentCanvas.transform, false);UIInventoryTextBox inventoryTextBox = inventoryBar.inventoryTextBoxGameobject.GetComponent<UIInventoryTextBox>();// Set item type descriptionstring itemTypeDescription = InventoryManager.Instance.GetItemTypeDescription(itemDetails.itemType);// Populate text boxinventoryTextBox.SetTextboxText(itemDetails.itemDescription, itemTypeDescription, "", itemDetails.itemLongDescription, "", "");// Set text box position according to inventory bar positionif (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 clickif (eventData.button == PointerEventData.InputButton.Left){// if inventory slot currently selected then deselectif (isSelected == true){ClearSelectedItem();}else // 未被选中且有东西则显示选中的效果{if(itemQuantity > 0){SetSelectedItem();}}}}/// <summary>/// Set this inventory slot item to be selected/// </summary>private void SetSelectedItem(){// Clear currently highlighted itemsinventoryBar.ClearHighlightOnInventorySlots();// Highlight item on inventory barisSelected = true;// Set highlighted inventory slotsinventoryBar.SetHighlightedInventorySlots();// Set use radius for cursors gridCursor.ItemUseGridRadius = itemDetails.itemUseGridRadius;cursor.ItemUseRadius = itemDetails.itemUseRadius;// If item requires a grid cursor then enable cursorif(itemDetails.itemUseGridRadius > 0){gridCursor.EnableCursor();}else{gridCursor.DisableCursor();}// If item requires a cursor then enable cursorif(itemDetails.itemUseRadius > 0f){cursor.EnableCursor();}else{cursor.DisableCursor();}// Set item typegridCursor.SelectedItemType = itemDetails.itemType;cursor.SelectedItemType = itemDetails.itemType;// Set item selected in inventoryInventoryManager.Instance.SetSelectedInventoryItem(InventoryLocation.player, itemDetails.itemCode);if (itemDetails.canBeCarried == true){// Show player carrying itemPlayer.Instance.ShowCarriedItem(itemDetails.itemCode);}else {Player.Instance.ClearCarriedItem();}}private void ClearSelectedItem(){ClearCursors();// Clear currently highlighted iteminventoryBar.ClearHighlightOnInventorySlots();isSelected = false;// set no item selected in inventoryInventoryManager.Instance.ClearSelectedInventoryItem(InventoryLocation.player);// Clear player carrying itemPlayer.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的图标。