前言

hallo and welcome~小半年不见想死我了,由于我懒的原因这么久没更新 所以今天为表歉意我来更新新的功能,第一个功能就是做一个可爱的小光标,让玩家知道哪个地方可以种植或者交互,还有一个功能就是割草

可爱的小光标

效果:在可交互范围内为绿色,不可交互范围内为红色(PS:割草的和普通的光标不同 我这里分别实现一下吧)在这里插入图片描述在这里插入图片描述

实现思路

以割草为例子 首先我们要有一个画布来存放光标Image,要有网格,要有光标Image,绿色图片精灵,红色图片精灵,默认精灵(没有选择工具就能显示图标),主摄像机用来坐标转换
然后我们需要判断鼠标的位置是否为可交互的有效位置(镰刀不能砍空地),工具和玩家的距离,玩家所持有工具的类型,光标是否显示

    private Canvas canvas;
    private Grid grid;
    private Camera mainCamera;
    [SerializeField] private Image cursorImage = null;
    [SerializeField] private RectTransform cursorRectTransform = null;
    [SerializeField] private Sprite greenSprite = null;
    [SerializeField] private Sprite transparentCursorSprite = null;
    [SerializeField] private GridCursor gridCursor = null;

    private bool _cursorPositionISValid = false;//当前位置是否有效 有效绿 无效红
    public bool CursorPositionISValid { get => _cursorPositionISValid; set => _cursorPositionISValid = value; }
    private float _itemUserRadius = 0;//物品和玩家的距离
    public float ItemUserRadius { get => _itemUserRadius; set => _itemUserRadius = value; }
    private ItemType _selectedItemType;
    public ItemType SelectedItemType { get => _selectedItemType; set => _selectedItemType = value; }
    private bool _cursorIsEnabled = false;
    public bool CursorIsEnabled { get => _cursorIsEnabled; set => _cursorIsEnabled = value; }
    private void Start()
    {
        grid = GameObject.FindObjectOfType<Grid>();
        mainCamera = Camera.main;
    }

设置光标的显示和隐藏

只需要控制image显示的sprite和设置位置是否有效即可

    private void SetCursorToInValid()
    {
        cursorImage.sprite = transparentCursorSprite;
        _cursorPositionISValid = false; 
    }

    private void SetCursorToValid()
    {
        cursorImage.sprite = greenSprite;
        _cursorPositionISValid = true; 
    }

获取玩家和光标在单元格上的位置

使用grid内置的函数即可获取,关于这里为什么不用vector3.distance,是因为我们之前写了各个物品的使用范围,而且我们种地是重在grid中的,如果使用vector3.distance的话,如果玩家站在某个格子的最边缘,按grid来算的话它的上下左右都应该可以交互,但是用vector3.distance的话可能某个相邻的grid它的距离是大于该物品的使用范围。 上图片:
在这里插入图片描述
就比如这两个相邻的格子 物品应该是可以使用的,但是用vector3.distance的话算出来的范围是大于可适用范围的

//获取玩家在格子的位置
public Vector3Int GetGridPositionForPlayer()
{
    return grid.WorldToCell(Player.Instance.transform.position);
}
//获取光标在格子的位置
public Vector3Int GetGridPositionForCursor()
{
    Vector3 worldPosition = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y,-mainCamera.transform.position.z));
    return grid.WorldToCell(worldPosition);
}
//把屏幕坐标转为世界坐标
 public Vector3 GetWorldPositionForCursor()
 {
     Vector3 sereenPosition = new Vector3(Input.mousePosition.x, Input.mousePosition.y, -mainCamera.transform.position.z);
     return mainCamera.ScreenToWorldPoint(sereenPosition);
 }
 //屏幕坐标转为ui坐标
     private Vector2 GetRectTransformPositionForCursor()
    {
        Vector2 screenPosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
        return RectTransformUtility.PixelAdjustPoint(screenPosition,cursorRectTransform,canvas);
    }

判断玩家和光标的距离 进行显示

根据上面设置的玩家坐标转格子坐标和鼠标坐标转格子坐标来判断光标是否显示
根据距离判断是否在可收割范围内,然后判断是否超过了最大的收割范围,如果满足条件就判断玩家手里的物品是否为如下的收割工具,如果是的话判断是不是镰刀,如果是镰刀就判断这个位置是否有可以收获的物品

private void SetCursorValidty(Vector3 cursorWorldPosition, Vector3 playerPosition)
{
    SetCursorToValid();
    //都不在可以收割的范围内
    if (cursorWorldPosition.x > (playerPosition.x + ItemUserRadius / 2f) && cursorWorldPosition.y > (playerPosition.y + ItemUserRadius / 2) ||//在右上
        cursorWorldPosition.x > (playerPosition.x + ItemUserRadius / 2f) && cursorWorldPosition.y < (playerPosition.y + ItemUserRadius / 2) ||//右下
         cursorWorldPosition.x < (playerPosition.x + ItemUserRadius / 2f) && cursorWorldPosition.y > (playerPosition.y + ItemUserRadius / 2) ||//左上
     cursorWorldPosition.x < (playerPosition.x + ItemUserRadius / 2f) && cursorWorldPosition.y > (playerPosition.y + ItemUserRadius / 2))//左下
    {
        SetCursorToInValid();
        return;
    }
    //上下左右超过了可收割的范围
    if (Mathf.Abs(cursorWorldPosition.x - playerPosition.x) > ItemUserRadius || Mathf.Abs(cursorWorldPosition.x - playerPosition.x) > ItemUserRadius)
    {
        SetCursorToInValid();
        return;
    }
    ItemDetial itemDetial = InventoryManager.Instance.GetSelectInventoryItemDetial(InventoryLocation.player);
    switch (itemDetial.itemType)
    {
        case ItemType.Watering_tool:
        case ItemType.Reaping_tool:
        case ItemType.Hoeing_tool:
        case ItemType.Collecting_tool:
        case ItemType.Chopping_tool:
        case ItemType.Breaking_tool:
            if (!SetCursorValidTool(cursorWorldPosition,playerPosition,itemDetial))
            {
                SetCursorToInValid();
                return;
            }
            break;
    }
}

private bool SetCursorValidTool(Vector3 cursorWorldPosition, Vector3 playerPosition, ItemDetial itemDetial)
{
    switch (itemDetial.itemType)
    {
        case ItemType.Reaping_tool:
                return SetCursorValidReapingTool(cursorWorldPosition, playerPosition ,itemDetial);

        default:
            return false;
    }
}
 private bool SetCursorValidReapingTool(Vector3 cursorWorldPosition, Vector3 playerPosition, ItemDetial itemDetial)
 {
     List<Item> itemList = new List<Item>();
     //HelperClass.GetComponentAtCursorLocation是根据 Physics2D.OverlapPointAll(cursorWorldPosition)判断是否有满足条件的物品 返回的数组如果大于0那么返回true
     if(HelperClass.GetComponentAtCursorLocation<Item>(out itemList, cursorWorldPosition))
     {
         if (itemList.Count != 0)
         {
             foreach(Item item in itemList)
             {
                 //如果碰到的是可以收获的物品
                 if (InventoryManager.Instance.GetItemDetial(item.ItemCode).itemType == ItemType.Reapable_scenary)
                 {
                     return true;
                 }
             }
         }
     }
     return false;
 }

HelperClass.GetComponentAtCursorLocation实现~

为了防止小伙伴们不知道怎么实现我上面代码注释的功能,我来写一下代码~
判断这个范围内是否碰撞检测到了含有杂草脚本的object 如果有就返回true

   public static bool GetCOmponentAtCursorLocation<T>(out List<T> list,Vector3 positionToCheck)
   {
       bool found = false;
       List<T> componentList = new List<T>();
       Collider2D[] collision2DArray = Physics2D.OverlapPointAll(positionToCheck);
       for(int i = 0; i < collision2DArray.Length; ++i)
       {
           T tComponent = collision2DArray[i].GetComponent<T>();
           if(tComponent!=null)
           {
               found = true;
               componentList.Add(tComponent);
           }
           else
           {
               tComponent = collision2DArray[i].GetComponentInChildren<T>();
               if (tComponent != null)
               {
                   found = true;
                   componentList.Add(tComponent);
               }
           }
       }
       list = componentList;
       return found;
   }

设置光标的显示模式(红或绿)

只需要控制image对应的color即可

    public void DisableCursor()
    {
        cursorImage.color = Color.clear;
        _cursorIsEnabled = false;
    }
    public void EnableCursor()
    {
        cursorImage.color = new Color(1, 1, 1, 1);
        _cursorIsEnabled = true;
    }

最后一步

在update中调用是否显示光标的函数 记住一定要把光标的位置转换,现在光标的位置是屏幕坐标 要显示到ui上就要用坐标转换把屏幕坐标转换成ui坐标

 private void Update()
 {
     if (CursorIsEnabled)
         DisplayCursor();
 }

 private void DisplayCursor()
 {
     Vector3 cursorWorldPosition = GetWorldPositionForCursor();
     SetCursorValidty(cursorWorldPosition, Player.Instance.GetPlayerCenterPosition());
     cursorRectTransform.position = GetRectTransformPositionForCursor();
 }

割草实现

到了这里我们发现 这只是光标的显示 怎么割草啊! 很简单,割草的逻辑就是:
1.玩家要在光标位置按下鼠标或者交互
2.判断玩家手里的是不是镰刀
3.交互的位置是否有可以交互的物品
4.播放交互音乐(后续音效系统做)
5.播放割草动画(之前的文章有写动画系统 大概 如果没有后续补上 )
6 播放割草粒子特效(后续粒子系统做)
7 割草
注:这些都要写在玩家脚本里!!!!

光标位置交互

 private void PlayerClickInput()
 {
     if (!playerInputIsDisabled)
     {
         if (Input.GetMouseButton(0))
         {
             if (cursor.CursorIsEnabled)
             {
                 Vector3Int playPosion = gridCursor.GetGridPositionForPlayer();
                 Vector3Int cursorPosion = gridCursor.GetGridPositionForCursor();
                 ProcessPlayerClickInput(cursorPosion, playPosion);
             }
         }
     }
 }

判断玩家手中的物品

gridPropertyDetails 用来判断格子内的信息
itemDetial 用来判断玩家手中的物品种类

    private void ProcessPlayerClickInput(Vector3Int cursorPosion, Vector3Int playPosion)
    {
        ResetMoveMent();
        //获取点击位置在玩家的方向
        Vector3Int playerDirection = GetPlayerClickDirection(cursorPosion, playPosion);
        //获取点击的grid信息
        GridPropertyDetails gridPropertyDetails = GridPropertyManager.Instance.GetGridPropertyDetails(cursorPosion.x, cursorPosion.y);
        ItemDetial itemDetial = InventoryManager.Instance.GetSelectInventoryItemDetial(InventoryLocation.player);
        if (itemDetial != null)
        {
            switch (itemDetial.itemType)
            { 
                case ItemType.Watering_tool:
                case ItemType.Hoeing_tool:
                case ItemType.Reaping_tool:
                case ItemType.Collecting_tool:
                case ItemType.Chopping_tool:
                case ItemType.Breaking_tool:
                    ProcessPlayerClickInputTool(gridPropertyDetails, itemDetial, playerDirection);
                    break;
                default:
                    break;
            }
        }
    }

这里为了防止大家忘记之前这两个寒素是做什么 我们再把源码拿出来
GridPropertyDetails 其实在割草是用不到的 这个只会在种了东西或者锄地之后调用 判断这个网格是否已经种过东西或者已经被锄过

    public GridPropertyDetails GetGridPropertyDetails(int gridx, int gridy)
    {
        return GetGridPropertyDetails(gridx, gridy, gridPropertyDictionary);
    }
    public GridPropertyDetails GetGridPropertyDetails(int gridx, int gridy, Dictionary<string, GridPropertyDetails> gridPropertyDictionary)
    {
        string key = "x" + gridx + "y" + gridy;
        GridPropertyDetails gridPropertyDetails;
        if (!gridPropertyDictionary.TryGetValue(key, out gridPropertyDetails))
            return null;
        return gridPropertyDetails;
    }

根据选择物品的itemcode返回itemDetial

     public ItemDetial GetSelectInventoryItemDetial(InventoryLocation inventoryLocation)
 	 {
      int itemCode = GetSelectedInventory(inventoryLocation);
      if (itemCode == -1) return null;
      else return GetItemDetial(itemCode);
  	}
    public ItemDetial GetItemDetial(int itemCode)
    {
        ItemDetial itemDetial;
        if (itemDetialDic.TryGetValue(itemCode, out itemDetial))
            return itemDetial;
        return null;
    }
 	private int GetSelectedInventory(InventoryLocation inventoryLocation)
 	{
      return selectedInventoryItem[(int)inventoryLocation];
  	}

判断是否可以交互

  private void ProcessPlayerClickInputTool(GridPropertyDetails gridPropertyDetails, ItemDetial itemDetial, Vector3Int playerDirection)
  {
      switch (itemDetial.itemType)
      { 
          case ItemType.Reaping_tool:
              if (cursor.CursorPositionISValid)
              {
                  playerDirection = GetPlayerDirection(cursor.GetWorldPositionForCursor(), 		playerDirection);
                  //AudioManager.Instance.PlaySound(SoundName.effectScythe);
                  ReapInPlayerDirectionAtCursor(itemDetial, playerDirection);
              }
              break; 
          default:
              break;
      }
  }
   private Vector3Int GetPlayerDirection(Vector3 cursorPosion, Vector3Int playerDirection)
 {
     if (cursorPosion.x > playerDirection.x && cursorPosion.y > (playerDirection.y - cursor.ItemUserRadius / 2) && cursorPosion.y < (playerDirection.y + cursor.ItemUserRadius / 2))
         return Vector3Int.right;
     else if (cursorPosion.x < playerDirection.x && cursorPosion.y > (playerDirection.y - cursor.ItemUserRadius / 2) && cursorPosion.y < (playerDirection.y + cursor.ItemUserRadius / 2))
         return Vector3Int.left;
     else if (cursorPosion.y > playerDirection.y)
         return Vector3Int.up;
     else
         return Vector3Int.down;
 }

播放割草动画和收获

 private void ReapInPlayerDirectionAtCursor(ItemDetial itemDetial, Vector3Int playerDirection)
 {
     StartCoroutine(ReapInPlayerDirectionAtRountine(playerDirection, itemDetial));
 }

 private IEnumerator ReapInPlayerDirectionAtRountine(Vector3Int playerDirection, ItemDetial itemDetial)
 {
     PlayerInputIsDisabled = true;
     playerToolUseDisable = true;
     toolCharacterAttribute.partVariantType = PartVariantType.scythe;
     characterAttributeList.Clear();
     characterAttributeList.Add(toolCharacterAttribute);
     animationOverried.ApplyCharacterCustomisationParameters(characterAttributeList);
     UseToolInPlayerDirection(itemDetial, playerDirection);
     yield return useToolAnimationPause;
     PlayerInputIsDisabled = false;
     playerToolUseDisable = false;
 }

割草是通过GetItemDetial来判断获取到的item的type是否为可收割的物品,如果是的话就把这个物品删掉 生成粒子特效(还没有做这个教程 先注释)
如果想要把范围内的全割掉就把和reapableItemCount 相关的注释掉 它是用来设置最多割掉多少个可收割的物品的

 private void UseToolInPlayerDirection(ItemDetial itemDetial, Vector3Int playerDirection)
 {
     if (Input.GetMouseButton(0))
     {
     //这里也是播放割草动画 判断往哪个方向割草
         switch (itemDetial.itemType)
         {
             case ItemType.Reaping_tool:
                 if (playerDirection == Vector3.right)
                     isSwingingToolRight = true;
                 if (playerDirection == Vector3.left)
                     isSwingingToolLeft = true;
                 if (playerDirection == Vector3.up)
                     isSwingingToolUp = true;
                 if (playerDirection == Vector3.down)
                     isSwingingToolDown = true;
                 break;
         }
     }
     Vector2 point = new Vector2(GetPlayerCenterPosition().x + playerDirection.x * itemDetial.itemUseRadius, GetPlayerCenterPosition().y + playerDirection.y * itemDetial.itemUseRadius);
     Vector2 size = new Vector2(itemDetial.itemUseRadius, itemDetial.itemUseRadius);
     Item[] itemAry = HelperClass.GetComponentsAtBoxLocationNonAlloc<Item>(Settings.maxColliderReapSwing, point, size, 0);
     int reapableItemCount = 0;
     for (int i = itemAry.Length - 1; i >= 0; i--)
     {
         if (itemAry[i] != null)
         {
             if (InventoryManager.Instance.GetItemDetial(itemAry[i].ItemCode).itemType == ItemType.Reapable_scenary)
             {
                 //Vector3 effectionPosition = new Vector3(itemAry[i].transform.position.x, itemAry[i].transform.position.y + Settings.gridSize / 2, itemAry[i].transform.position.z);
                 Destroy(itemAry[i].gameObject);
                // EventHandler.CallHarvestActionEffectEvent(effectionPosition, HarvestActionEffect.reaping);
                 reapableItemCount++;
                 if (reapableItemCount >= Settings.maxTargetComponentToDestoryReaping)
                     break;
             }
         }
     }
 }

获取收割范围内的heaper函数:根据提供的参数判断范围内被碰撞体碰到的物品 然后存入list中

 public static T[] GetComponentsAtBoxLocationNonAlloc<T>(int numberOfColliderToTest,Vector2 point,Vector2 size,float angle)
 {
     Collider2D[] collider2DArray = new Collider2D[numberOfColliderToTest];
     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].GetComponent<T>();
             if (tComponent != null)
             {
                 componentArray[i] = tComponent;
             }
         }
     }
     return componentArray;
 }

结尾

好 时间不早 代码刚好,这样我们就已经完成了割草功能的实现辣,朋友们可以试一下用我这个方法来实现锄头锄地的功能练练手~ 我们下期见 下期我来介绍怎么锄头锄地 砍树之类的功能开发 我们不见不散~

Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐