Featured image of post 麦田物语开发日记(七)

麦田物语开发日记(七)

完成瓦片地图的事件处理

游戏内容

鼠标图标的变化

使用一个图片来跟随鼠标移动,用该方式来表示鼠标的变化

由于是跨场景赋值,Manager是主场景,而UI是另一个场景,因此不能直接用层次窗口拖拽的方式,需要通过tag去查找我们的canvas的内容

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class CursorManager : MonoBehaviour
{
    public Sprite normal, tool, seed, item;
    private Sprite _currentSprite;      // 临时承接下一帧应该使用的图标
    private Image _cursorImage;     // 实际的图标的图片
    private RectTransform _cursorCanvas;        // 通过代码获得画布的内容

    private void Start()
    {
        /*跨场景获取鼠标图片的信息*/
        _cursorCanvas = GameObject.FindGameObjectWithTag("CursorCanvas").GetComponent<RectTransform>();
        _cursorImage = _cursorCanvas.GetChild(0).GetComponent<Image>();
        _currentSprite = normal;

    }

    // 当快捷栏的物品被选取时,鼠标图案会相应更改
    private void OnEnable()
    {
        EventHandler.ItemSelectedEvent += OnItemSelectCursorImage;
    }

    private void OnDisable()
    {
        EventHandler.ItemSelectedEvent -= OnItemSelectCursorImage;
    }

    

    private void Update()
    {
        if (_cursorCanvas == null) return;
        _cursorImage.transform.position = Input.mousePosition;
        
        /* 只有当鼠标与非UI互动时才修改鼠标样式 */
        if (!InteractWithUI())
        {
            SetCursorImage(_currentSprite);
        }
        else
        {
            SetCursorImage(normal);
        }
    }

    

    private void SetCursorImage(Sprite cursorSprite)
    {
        _cursorImage.sprite = cursorSprite;
        _cursorImage.color = new Color(1, 1, 1, 1); //保证原色
    }
    
    
    private void OnItemSelectCursorImage(ItemDetails itemDetail, bool isSelected)
    {
        /*将临时图标修改为当前选取的物品类型对应的图标*/
        if (isSelected)
        {
            _currentSprite = itemDetail.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
            };
        }
        else
        {
            _currentSprite = normal;
        }
        
    }
    
    /// <summary>
    /// 当前鼠标是否在与UI互动
    /// </summary>
    /// <returns></returns>
    private bool InteractWithUI()
    {
        /*通过当前UI的事件系统判断, 如果指针覆盖于UI的go上,鼠标为默认央视*/
        if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

设置瓦片地图的地图事件

方案1:设置每一个瓦片继承自一个tileGO类似ruleTile的形式,然后这个GO有什么属性,瓦片地图就会有什么属性(缺点:每个瓦爿都需要内容,瓦片过多时影响性能)

方案2:使用Unity提供的2D extra中的grid information组件来解决,该组件代码能够为对应位置的瓦片设置对应的属性(int, float等,没有bool)。(问题同上)

方案3:编写一个事件系统,能够让我们以绘制collision类似的方式去绘制信息,然后通过代码自动拿到绘制的信息存储到SO文件中,然后通过专门的代码去处理这些信息。

**思路:**首先需要设置每个Tile应该有的具体信息(坐标,事件类型,以及事件是否能被执行的变量),然后制作一个SO文件,为每个地图指定SO文件应该包含的的内容。

最后设计一个代码,该代码能够在tilemap的绘制完成后,将绘制的信息保存到SO文件中,即当我们手动关闭Grid Propertie的子物体的时候,进行保存。

特殊函数

1
2
3
Application.IsActive(this);	//是否在运行
tileMap.cellBounds;	// 压缩地图,获得当前GO下绘制的tileMap内容
tileMap.cellBounds.min, max;		// 获得压缩后地图的左下角和右上角坐标
  • 设计每个瓦片应该含有的信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[System.Serializable]
public class TileProperties
{
    public Vector2Int tileCoordinate; /*网格的坐标*/
    public GridType gridType;   /*该网格的具体类型*/
    public bool boolTypeValue; /*该类型是否可用*/
}

public enum GridType
{
    Diggable, DropItem, PlaceFurniture, NpcObstacle
}
  • 构建SO文件
1
2
3
4
5
6
7
// MapData_SO.cs
[CreateAssetMenu(fileName = "MapData_SO", menuName = "Map/MapData")]
public class MapData_SO : ScriptableObject
{
    [SceneName]public string sceneName;
    public List<TileProperties> tileProperties;
}
  • 设计自动将绘制信息读取到SO文件的代码

由于该部分内容是在编辑窗口运行的代码,在实际运行中不能直接执行,因此需要在该代码前写[ExecuteInEditMode]

并且设置该部分代码需要在GO关闭的时候运行,因此需要在代码的OnEnableOnDisable部分处理

在SO文件中,每次修改后,如果不直接保存,则下一次打开会直接丢失数据,因此需要在代码中设置EditorUtility.SetDirty(mapDataSo);来保存加载好的So文件

image-20230131013328127

该图片为压缩图片后的到的数据信息

注意该代码需要每一个Property的GO都挂载,并加载内容

image-20230131013731042

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Tilemaps;

[ExecuteInEditMode]
public class GridMap : MonoBehaviour
{
    /*设置当挂载该脚本的瓦片GO关闭时,将该GO下的所有有信息的瓦片全部存储在对应的GO中*/
    public MapData_SO mapDataSo;  /*存储的So文件*/
    public GridType gridType; /*当前GO对应的类型*/
    private Tilemap _currentTilemap;
    
    /*由于是在GO开启和关闭的时候绘制,因此需要使用OnEnable和 OnDisable*/
    private void OnEnable()
    {
        /*只有当程序没有运行的时候,才能执行此处的代码*/
        if (!Application.IsPlaying(this))
        {
            _currentTilemap = GetComponent<Tilemap>();
            /*为了便于不断的绘制,因此设计,开启GO就重新绘制,关闭GO就存储*/
            if (mapDataSo != null)
            {
                mapDataSo.tileProperties.Clear();
            }
        }
    }

    /*关闭后存储绘制的内容*/
    private void OnDisable()
    {
        /*只有当程序没有运行的时候,才能执行此处的代码*/
        if (!Application.IsPlaying(this))
        {
            _currentTilemap = GetComponent<Tilemap>();
            /*从整个界面中,遍历所有以及绘制了的瓦片,然后将其内容存储进入SO文件中*/
            UpdateTileProperties();
            /*只有当在编辑窗口的时候,才能保存数据*/
#if UNITY_EDITOR
            if (mapDataSo != null)
            {
                EditorUtility.SetDirty(mapDataSo);
            }
#endif
        }
    }

    /// <summary>
    /// 将所有绘制内容存储进入SO中
    /// </summary>
    private void UpdateTileProperties()
    {
        _currentTilemap.CompressBounds();   /*压缩范围,获取当前绘制瓦片的左下角和右上角返回*/
        if (!Application.IsPlaying(this))
        {
            if (mapDataSo != null)
            {
                /*议会制范围可能有空*/
                /*已绘制范围的左下角位置*/
                Vector3Int startPos = _currentTilemap.cellBounds.min;
                /*已绘制范围的右上角位置*/
                Vector3Int endPos = _currentTilemap.cellBounds.max;
                for (int x = startPos.x; x < endPos.x; x++)
                {
                    for (int y = startPos.y; y < endPos.y; y++)
                    {
                        /*获得当前坐标下瓦片的的内容*/
                        TileBase tile = _currentTilemap.GetTile(new Vector3Int(x, y, 0));

                        /*如果该位置是绘制了内容的*/
                        if (tile != null)
                        {
                            TileProperties newTile = new TileProperties
                            {
                                tileCoordinate = new Vector2Int(x, y),
                                gridType = this.gridType,
                                boolTypeValue = true
                            };
                            mapDataSo.tileProperties.Add(newTile);
                        }
                    }
                }
            }
        }
    }
}

生成地图信息

创建一个管理类,用于管理地图信息,方便后续的内容获得地图信息。

思路: 首先需要创建一个管理该瓦片所有信息的类,包含所有在tileMapData中存储的某一种Property的属性,以及未来可能会使用的信息(是否被挖掘,是否被浇水,种了什么种子,已经长了多少天,上一次收割后隔了多少天)

然后对整个mapdata进行遍历,将里面存放的内容初始化好后,全部读取进入一个字典中。

为了鼠标能够对这些地图信息进行识别地图信息,因此需要先设置在CursorManager中设置好地图的整体信息,然后让鼠标能够正常获取这些坐标信息,一边后续将鼠标信息和字典的信息进行比对

  • 管理mapData信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class GridMapManager : MonoBehaviour
{
    public List<MapData_SO> mapDataSos;
    private Dictionary<string, TileDetails> _tileDetailsMap 
        = new Dictionary<string, TileDetails>();    /*获得所有瓦片的具体信息*/

    /*初始化所有瓦片的信息*/
    private void Start()
    {
        foreach (MapData_SO mapData in mapDataSos)
        {
            InitTileDetailsMap(mapData);
        }
    }


    private void InitTileDetailsMap(MapData_SO mapDataSo)
    {
        if (mapDataSo.tileProperties != null)
        {
            foreach (TileProperties tileProperty in mapDataSo.tileProperties)
            {
                
                int tileX = tileProperty.tileCoordinate.x;
                int tileY = tileProperty.tileCoordinate.y;

                string tileDetailKey = tileX + 'x' + tileY + 'y' + mapDataSo.sceneName;
                
                /* 对数据进行处理,并将数据存放或更新进入map */
                TileDetails tileDetails = new TileDetails
                {
                    gridPos = new Vector2Int(tileX, tileY),
                };

                if (GetTileDetails(tileDetailKey) != null)
                {
                    tileDetails = _tileDetailsMap[tileDetailKey];
                    
                }
                
                /*更新数据*/
                switch (tileProperty.gridType)
                {
                    case GridType.Diggable:
                        tileDetails.canDig = tileProperty.boolTypeValue;
                        break;
                    case GridType.DropItem:
                        tileDetails.canDrop = tileProperty.boolTypeValue;
                        break;
                    case GridType.PlaceFurniture:
                        tileDetails.canPlaceFurniture = tileProperty.boolTypeValue;
                        break;
                    case GridType.NpcObstacle:
                        tileDetails.isNpcObstacle = tileProperty.boolTypeValue;
                        break;
                }
                
                if (GetTileDetails(tileDetailKey) != null)
                {
                    _tileDetailsMap[tileDetailKey] = tileDetails;
                }
                else
                {
                    _tileDetailsMap.Add(tileDetailKey, tileDetails);
                }
                
            }
        }
    }

    private TileDetails GetTileDetails(string key)
    {
        if (_tileDetailsMap.ContainsKey(key))
        {
            return _tileDetailsMap[key];
        }

        return null;
    }
}
  • 让鼠标能够识别地图信息的前提(获取鼠标的坐标)

因此先要获得摄像机的坐标已经当前地图的坐标,由于地图会随着场景切换而改变,因此需要在场景切换后才能获取该坐标

同时获取世界坐标的功能应该是实时更新的,因此摄像头理论上应该实时改变

重点函数

1
2
3
4
Camera.main.ScreenToWorld(new Vector3(Input.mousePosition.x, Input.mousePosition.x, -_mainCamera.transform.position.z)); //鼠标坐标) z轴是因为摄像机距离屏幕有一定距离
// 通过世界坐标转化为网格坐标
Vector3Int  = Grid.WorldToCell(世界坐标)
     
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 该函数需要在CursorManager.cs中的Update中调用
private void CheckCursorValid()
{
    /* 获得世界坐标和网格坐标*/
    _mouseWorldPos = _mainCamera.ScreenToWorldPoint(
        new Vector3(Input.mousePosition.x, Input.mousePosition.y, -_mainCamera.transform.position.z));
    _mouseGridPos = _currentGrid.WorldToCell(_mouseWorldPos);
    
    Debug.Log("mouseWorld::" + _mouseWorldPos + "  mouseGird::" + _mouseGridPos);
}

注意由于获得网格坐标我们是在切换场景后才启动的,但是刚刚加入场景的时候,并没有发生场景切换,即此时并没有得到网格坐标,但是在CursorManager中,获取网格坐标是实时更新的,因此会发送报错

该处的错误需要将在加载初始场景以后,才能执行加载场景后的事件,但是由于加载场景是使用异步加载完成的,因此会导致在加载的过程中,就调用加载后事件了,因此需要通过协程来保证先后顺序,即通过yield return先加载场景,然后再调用加载场景后的时间,因此需要将start函数以协程执行

协程的含义就是遇到yield return就让出时间片,先不执行后续内容,等yield return执行完成后再执行后续内容

1
2
3
4
5
6
7
8
// TransitionManager.cs
private IEnumerator Start()
{
yield return  LoadSceneSetActive(startSceneName);
sceneFaderCanvasGroup = FindObjectOfType<CanvasGroup>();
/*由于将start改为了协程,可以调用场景加载后事件了了*/
EventHandler.CallAfterSceneLoadEvent();
}

上述内容理解:由于yield return的存在,当进行到该start的时候,会先完成场景的加载,然后在场景加载完成后EventHandler.CallAfterSceneLoadEvent();才能被被正常调用

根据网格信息处理处理鼠标图片

根据地图信息来显示鼠标是可用还是不可用

**思路:**先获得网格字典中的信息,并将网格字典中的信息和当前选取内容进行比较,从而决定当前鼠标的样式以及当前鼠标是否可使用。

因此首先需要设计鼠标可用和不可用的样式,然后从GridMapManager中获取内容,将当前物品的信息和地图信息进行比较,然后将物品的信息和人物位置进行比较,最终确定鼠标的状态。

  • 设置鼠标的图案
1
2
3
4
5
6
7
8
9
private void SetCursorValid()
{
    _cursorImage.color = new Color(1, 1, 1, 1);
}

private void SetCursorInvalid()
{
    _cursorImage.color = new Color(1, 0, 0, 0.4f);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 获取GridMapManager的信息,此时需要将GridMapManager改为单例模式
/// <summary>
/// 通过鼠标的GRID坐标来获取当前地图的信息
/// </summary>
/// <param name="mousePos">鼠标的GRID坐标</param>
/// <returns></returns>
public TileDetails GetTileDetailsByMousePos(Vector3Int mousePos)
{
    string tileMapKey = mousePos.x + "x" + mousePos.y + "y" + SceneManager.GetActiveScene().name;
    return GetTileDetails(tileMapKey);
}
  • 处理鼠标图案的情况
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// <summary>
/// 实时检测鼠标的位置,并获取地图信息
/// </summary>
private void CheckCursorValid()
{
    /* 获得世界坐标和网格坐标*/
    _mouseWorldPos = _mainCamera.ScreenToWorldPoint(
        new Vector3(Input.mousePosition.x, Input.mousePosition.y, -_mainCamera.transform.position.z));
    _mouseGridPos = _currentGrid.WorldToCell(_mouseWorldPos);
    
    /*获得人物信息,用于比较人物和鼠标位置, 如果不满足使用范围则 不允许使用*/
    
    Vector3Int playerGridPos = _currentGrid.WorldToCell(playerTransform.position);
    if (Mathf.Abs(playerGridPos.x - _mouseGridPos.x) > _currentItem.itemUseRadius ||
        Mathf.Abs(playerGridPos.y - _mouseGridPos.y) > _currentItem.itemUseRadius)
    {
        SetCursorInvalid();
        return;
    }

    
    /*检测当前选取的物品是否满足使用要求*/
    TileDetails tileDetails = GridMapManager.Instance.GetTileDetailsByMousePos(_mouseGridPos);
    if (tileDetails != null && _cursorEnable)
    {
        switch (_currentItem.itemType)
        {
            case ItemType.Commodity:
                if (tileDetails.canDrop && _currentItem.canDropped) SetCursorValid();
                else SetCursorInvalid();
                break;
        }
    }
    else
    {
        SetCursorInvalid();
    }

该过程需要获得人物信息以及当前选取的物品信息,因此需要在对应的事件中获取内容,并且只有当选中某个物品的时候,图标才能被改变形态,其他时候都不能被改变。

实现鼠标选中物品后的场景点击事件

当鼠标点击后,然后将选取的物品信息和点的坐标,传递给需要的代码

因此需要一个鼠标的点击事件,然后触发人物的动画,接着再去执行对应的的代码(扔东西,或者树木摇晃)

由于很多内容涉及到要修改地图信息,因此可以将扔东西等代码放在GridMapManager中执行

  • 检测点击事件,保证当前位置是可以点击的,然后触发点击事件
1
2
3
4
5
6
7
8
public void CheckPlayerInput()
{
    if (Input.GetMouseButtonDown(0) && _cursorPositionValid)
    {
        /*调用点击事件,并传递当前点击的坐标以及当前触发点击的物品类型*/
        EventHandler.CallMouseClickedEvent(_mouseWorldPos, _currentItem);
    }
}
  • 此后人物的脚本应该能接受到该事件,然后完成相应的动画,并在此触发其他事件用以驱动其他代码
1
2
3
4
5
6
// player.cs中挂载的事件
private void OnMouseClickedEvent(Vector3 clickWorldPos, ItemDetails itemDetails)
{
    /*todo 执行对应的动画*/
    EventHandler.CallExecuteActionAfterAnimationEvent(clickWorldPos, itemDetails);
}
  • 完成丢弃功能,暂时在GridMapManager.cs中执行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// GridMapManager.cs 
// 获得_currentGrid需要通过注册场景切换后事件的方法,然后直接FindObjectOfType<Gird>寻找
private void OnExecuteActionAfterAnimationEvent(Vector3 mouseWorldPos, ItemDetails itemDetails)
{
    /*触发将物品丢弃事件,并需要更新InventoryManager中存储的物品信息,且更新UI*/
    Vector3Int mouseGridPos = _currentGrid.WorldToCell(mouseWorldPos);
    TileDetails currentTile = GetTileDetailsByMousePos(mouseGridPos);

    if (currentTile != null)
    {
        /*Todo携带物品完成不同事件*/
        switch (itemDetails.itemType)
        {
            case ItemType.Commodity:
                EventHandler.CallDropItemInSceneEvent(itemDetails.itemID, mouseGridPos);
                break;
        }
    }
}
  • InventoryManager.cs中实现物品丢弃功能的数据更改,在itemManager.cs实现物品丢弃的地面生成
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// InventoryManager.cs
private void RemoveItem(int itemID, int removeAmount)
{
    /*通过Id获取当前物品在背包中的位置,并判断对应位置的数量*/
    int itemIndexInBag = GetItemIndexInBag(itemID);
    if (playerBag.inventoryItemList[itemIndexInBag].itemAmount > removeAmount)
    {
        var amount = playerBag.inventoryItemList[itemIndexInBag].itemAmount - removeAmount;
        /*更新数据*/
        playerBag.inventoryItemList[itemIndexInBag] =
            new InventoryItem() { itemID = itemID, itemAmount = amount };
    }
    else if (playerBag.inventoryItemList[itemIndexInBag].itemAmount == removeAmount)
    {
        playerBag.inventoryItemList[itemIndexInBag] = new InventoryItem();
    }
    
    EventHandler.CallUpdateInventoryUI(InventoryLocation.Player, playerBag.inventoryItemList);
}

/// <summary>
/// 当丢弃某一个物品时候触发的时间
/// </summary>
/// <param name="itemID">丢弃物品的ID</param>
/// <param name="pos">丢弃的坐标</param>
private void OnDropItemInSceneEvent(int itemID, Vector3 pos)
{
    RemoveItem(itemID,1);

}

由于丢弃完成所有物品后,格子高亮应该也要清除,并放手,物品不能再被选取,因此需要修改中的清空界面的代码,该段代码是在InventoryUI.cs中调用的SlotUI.csUpdateEmptySlot方法,因此需要修改此处。

注意高亮的更新方法是是slotui的父级GOInventoryUI,以便于高亮一个以后,其他高亮消失

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// SlotUI.cs
public void UpdateEmptySlot()
{
    if (isSelected)
    {
        isSelected = false;
        inventoryUI.UpdateSlotHighlight(-1);
        EventHandler.CallItemSelectedEvent(slotItemDetails, isSelected);
    }
    
    slotItemDetails = null;
    slotImg.enabled = false;
    amountText.text = string.Empty;
    btn.interactable = false;
}

由于我们将slotItemDetails = null;设置为了Null,那么相应的所有以slotItemDetails 的数量来判断的地方都要修改,包括slotui以及显示物品信息的窗口

实现物品被扔出的效果

思路:利用生成物品的移动和阴影来完成视觉差的体验,即物品从人物身边过去,而物品从人物脚底直线过去

  • 首先需要一个获得阴影的代码,让阴影能够获得本身物品的样子
  • 然后对物体本身使用每一帧更新物体的位置和阴影
  • 最后在itemManager.cs重写丢弃事件的触发代码

由于物体的弹跳需要利用视觉差,因此实际阴影的移动是跟随主物体本身移动的,而物体本身的sprite则从人物头部投出,逐渐到达目标地点

阴影代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ItemShadow.cs
[RequireComponent(typeof(SpriteRenderer))]
public class ItemShadow : MonoBehaviour
{
    public SpriteRenderer baseSprite;
    private SpriteRenderer _shadowSprite;

    private void Awake()
    {
        _shadowSprite = GetComponent<SpriteRenderer>();
    }

    private void Start()
    {
        _shadowSprite.sprite = baseSprite.sprite;
        _shadowSprite.color = new Color(0, 0, 0, 0.3f);
    }
}

物体的移动代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// ItemBounce.cs
public class ItemBounce : MonoBehaviour
{
    private Transform Player => FindObjectOfType<Player>().transform;
    public float gravity = -3.5f;
    private Transform _spriteTransform;
    private BoxCollider2D _collider2D;  /*碰撞体,保证人物在丢弃过程中,人物不能捡起*/

    /*移动过程需要的参数*/
    private bool _inGround; /*是否到达地面*/
    private Vector3 _targetPos; /*物体的目标位置*/
    private Vector2 _direction; /*物体前进的方向*/
    private float _distance;    /*物体和目标地点的距离*/

    private void Awake()
    {
        _spriteTransform = transform.GetChild(0);
        _collider2D = GetComponent<BoxCollider2D>();
        _collider2D.enabled = false;
    }


    private void Update()
    {
        Bounce();
    }

    /// <summary>
    /// 对物体需要移动的初始变量进行赋值
    /// </summary>
    /// <param name="targetPos">物体的目标距离</param>
    /// <param name="dir">物体的目标方向</param>
    public void InitBounceItem(Vector3 targetPos, Vector2 dir)
    {
        _collider2D.enabled = false;
        _targetPos = targetPos;
        _direction = dir;
        _inGround = false;
        _distance = Vector3.Distance(transform.position, targetPos);

        /*保证初始位置在人物的头顶*/
        _spriteTransform.position += Vector3.up * 1.75f;
    }

    /// <summary>
    /// 物品弹跳的过程(移动过程)
    /// </summary>
    private void Bounce()
    {
        _inGround = _spriteTransform.position.y <= transform.position.y;
        if (Vector3.Distance(transform.position, _targetPos) > 0.1f && !_inGround)
        {
            /*平面移动*/
            transform.position += (Vector3)_direction * _distance * -gravity * Time.deltaTime;
        }

        if (!_inGround)
        {
            _spriteTransform.position += Vector3.up * 4f * gravity * Time.deltaTime;
        }
        else
        {
            _spriteTransform.position = transform.position;
            _collider2D.enabled = true;
        }
    }
}

实现挖坑效果

为了实现挖坑的图片合理,需要指定绘制瓦片地图的规则

  • 创建RuleTile的方法,类似于创建空物体的方法在Creat->2d->Tile->ruleTile

  • 创建好Tile规则以后,需要能绘制对应的挖地和浇水的瓦片,因此也需要通过标签来找到对应的瓦片地图

  • 然后再GridMapManager.cs中实现当携带工具点击可以挖地或浇水时候的功能,此时需要构建对应的瓦片,因此需要引入对应的瓦片地图的规则,并通过代码获取对应的瓦片地图。

  • 当可以执行对应的功能的时候,使用TileMap自带的函数TileMap.setTile来设置对应的规则,

  • 最后在对应的位置设置TileDetiles的具体内容

GridMapManager的设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[Header("瓦片地图规则")] 
public RuleTile digRuleTile;
public RuleTile waterRuleTile;

private Tilemap _digTileMap;    /*获得场景的可挖掘地图信息*/
private Tilemap _waterTileMap;  /*获得场景的可浇水地图信息*/

/*挖地事件*/
                    case ItemType.HoeTool:
                        SetDigTileMap(currentTile);
                        currentTile.canDig = false;
                        currentTile.canDrop = false;
                        currentTile.daySinceDig = 0;
                        // todo 音效
                        break;
                    case ItemType.WaterTool:
                        SetWaterTile(currentTile);
                        currentTile.daySinceWater = 0;
                        break;
                }
            }
        }

private void SetDigTileMap(TileDetails tileDetails)
{
    Vector3Int targetTilePos = new Vector3Int(tileDetails.gridPos.x, tileDetails.gridPos.y, 0);
    if (_digTileMap != null)
    {
        _digTileMap.SetTile(targetTilePos, digRuleTile);
    }
}

private void SetWaterTile(TileDetails tileDetails)
{
    Vector3Int targetTilePos = new Vector3Int(tileDetails.gridPos.x, tileDetails.gridPos.y, 0);
    if (_waterTileMap != null)
    {
        _waterTileMap.SetTile(targetTilePos, waterRuleTile);
    }
}

设置使用工具的动画

整体思路是在原baseController的基础上,增加新的BlendTree,从而增加不同的状态动画,同时为新的动画添加动画动作,同时设置工具的专门的动画,以实现在不同工具下的动画动作

  • 设置工具的AnimatorOverride动画,且都基于baseController

  • baseController设置工作的四个方向动画,并使用专门的变量来控制这些方向

  • 为所有基于baseController的工具动画设置对应的动画片段,并修改角色动画的修改关系

  • 编写人物触发点击事件时候的动画代码,设置动画的执行过程

image-20230201223739203 image-20230201223749058

然后设置在人物栏专门添加工具GO,并为其增加动画控制器。

AnimatorOverride.cs中该部分逻辑主要是获取当前所持物品的信息,然后找出该物品信息对应的的PartType,找出该需要改变的所有人物拥有的部分(身体,手臂,工具),为这些部分的所有动画控制器更换动画。

修改人物点击事件的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// player.cs
private void OnMouseClickedEvent(Vector3 clickWorldPos, ItemDetails itemDetails)
{
    if (_useTool) return;   // 在使用工具则不能点击
    
    /*todo 执行对应的动画*/
    if (itemDetails.itemType != ItemType.Seed && itemDetails.itemType != ItemType.Furniture &&
        itemDetails.itemType != ItemType.Commodity)
    {
        _mouseX = clickWorldPos.x - transform.position.x;
        _mouseY = clickWorldPos.y - transform.position.y;

        /*保证不会出现斜方向的动画*/
        if (Mathf.Abs(_mouseX) > Mathf.Abs(_mouseY))
        {
            _mouseY = 0;
        }
        else
        {
            _mouseX = 0;
        }

        StartCoroutine(UseToolRoutine(clickWorldPos, itemDetails));
    }
    else
    {
        EventHandler.CallExecuteActionAfterAnimationEvent(clickWorldPos, itemDetails);
    }
}

/// <summary>
/// 执行使用工具的动画,由于是一边动画,一边完成事件,因此要用协程
/// </summary>
/// <param name="mouseWorldPos">鼠标点击的位置</param>
/// <param name="itemDetails">使用的物品</param>
/// <returns></returns>
private IEnumerator UseToolRoutine(Vector3 mouseWorldPos, ItemDetails itemDetails)
{
    _useTool = true;
    _inputDisable = true;
    yield return null;      // 保证此时已经不能移动和使用工具
    
    /*执行动画, 由于在AnimatorOverride.cs中,选择物品就修改动画,因此此时直接执行即可*/
    // 将身上所有的动画都按需求执行
    foreach (var anim in _animations)
    {
        anim.SetTrigger("useTool");
        anim.SetFloat("MouseX", _mouseX);
        anim.SetFloat("MouseY", _mouseY);
    }
    // 动画执行到一定程度,触发动作的具体代码
    yield return new WaitForSeconds(0.45f);
    EventHandler.CallExecuteActionAfterAnimationEvent(mouseWorldPos, itemDetails);
    
    // 等待动画完成
    yield return new WaitForSeconds(0.35f);
    _inputDisable = false;
    _useTool = false;
}

实现地图信息随事件变化

由于每次切换场景瓦片地图数据会消失,但字典中的地图信息保留,因此就需要读取字典信息,并根据内部的内容,将瓦片地图重新绘制

  • 保存瓦片地图数据(通过每次执行完事件后更新字典信息完成)

  • 对地图数据进行refresh(每次切换场景,情况所有地图信息,并从字典中重新加载地图信息)

  • 增加时间对地图信息的影响,每过一天,更新地图信息,并重新绘制地图

保存瓦片地图的字典信息

1
2
3
4
5
6
7
8
private void UpdateGridMap(TileDetails tileDetails)
{
    string key = tileDetails.gridPos.x + "x" + tileDetails.gridPos.y + "y" + SceneManager.GetActiveScene();
    if (_tileDetailsMap.ContainsKey(key))
    {
        _tileDetailsMap[key] = tileDetails;
    }
}

重绘网格信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// <summary>
/// 删除地图的瓦片信息,并重新生成画面
/// </summary>
private void RefreshGridMap()
{
    if (_digTileMap != null)
        _digTileMap.ClearAllTiles();
    if (_waterTileMap != null)
        _waterTileMap.ClearAllTiles();
    
    DisplayMap(SceneManager.GetActiveScene().name);
}


/// <summary>
/// 重新生成当前地图的所有绘制内容
/// </summary>
/// <param name="sceneName"></param>
private void DisplayMap(string sceneName)
{
    foreach (var tileMapDic in _tileDetailsMap)
    {
        // workflow 显示当前地图的后期绘制的瓦片
        /*找到当前场景的所有瓦片数据*/
        string key = tileMapDic.Key;
        if (key.Contains(sceneName))
        {
            if (tileMapDic.Value.daySinceDig > -1)
                SetDigTileMap(tileMapDic.Value);
            if (tileMapDic.Value.daySinceWater > -1)
                SetWaterTile(tileMapDic.Value);
            // todo 种子的瓦片操作
        }
    }
}

设置每日更新事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// 每天更新的事件
/// </summary>
/// <param name="day">当前的天数</param>
/// <param name="season">当期的季节</param>
private void OnGameDayEvent(int day, Season season)
{
    /*将所有场景的瓦片信息中,有日期更新的数据都进行更新*/
    foreach (var tileDic in _tileDetailsMap)
    {
        if (tileDic.Value.daySinceDig > -1)
            tileDic.Value.daySinceDig++;
        if (tileDic.Value.daySinceWater > -1)
            tileDic.Value.daySinceWater = -1;
        
        /*如果坑太长时间没有种植,则消失*/
        if (tileDic.Value.daySinceDig > 4)
        {
            RestoreNotHoe(tileDic.Value);
        }
    }
    RefreshGridMap();   
}
Built with Hugo
Theme Stack designed by Jimmy