Unity3D仿星露谷物语开发39之非基于网格的光标

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的图标。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值