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

麦田物语开发日记(八)

完成作物的制作与生长功能

种庄稼整体

种子库的构建

创建一个单独的代码来保存种子生长过程的全部信息CropDetails(种子的ID,不同阶段生成的天数int[]、生长的总天数、不同生长阶段的物品Prefab、不同阶段的图片、可种植的季节、收割工具信息、每种工具的使用次数、转换的新物品ID、收割果实的信息、收获的果实的数量、生成物体在地图上的范围、再次生长的时间、次数;

 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
// CropDetails.cs
[System.Serializable]
public class CropDetails
{
   [Header("种子ID")] public int seedItemId;
   [Header("不同阶段的生长日期")] 
   public int[] growthDays;

   public int TotalGrowthDay
   {
      get
      {
         int amount = 0;
         foreach (var day in growthDays)
         {
            amount += day;
         }

         return amount;
      }
   }

   [Header("不同阶段生长的Prefab")] public GameObject[] growthObjects;
   [Header("不同阶段生长图片")] public Sprite[] growthSprites;
   [Header("可种植的季节")] public Season[] seasons;

   [Space] [Header("可用于收获的工具")] public int[] harvestToolItemID;
   [Header("每种工具使用的次数")] public int[] requireActionCount;
   [Header("转化的新物品ID")] public int transferItemId;

   [Space] [Header("收获的果实信息")] public int[] producedItemID;
   public int[] producedMinAmount;
   public int[] producedMaxAmount;
   [Header("收获时物品在地图生成的范围")]
   public Vector2 spawnRadius;

   [Header("再次生长的时间")] public int dayToRegrow;
   [Header("再次生长的次数")]public int regrowTimes;

   [Space] [Header("Options")] public bool generateAtPlayerPosition;
   public bool hasAnimation;
   public bool hasParticleEffect;
   //TODO:特效 音效 等

}

种植种子的逻辑

  • 使用CropManager.cs来控制我们控制种子内容的种子库
  • 然后设置使用种子的时候鼠标的效果
  • 当鼠标携带种子点击地面的时候,点击事件需要传递相关内容到CropManager.cs中完成种子的种植
  • 其中种子种植除了修改本身TileDetail的内容外,还要重新绘制地面信息等

为了实现控制种子库的内容需要先得到种子库的数据文件,然后当鼠标点击挖好的坑触发点击事件时,通过事件中心触发 种植逻辑

  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
104
105
106
107
108
109
110
public class CropManager : MonoBehaviour
{
    public CropDataList_SO cropDataListSo;

    private Transform _cropParent;
    private Grid _currentGrid;
    private Season _currentSeason;

    private void OnEnable()
    {
        EventHandler.PlantSeedEvent += OnPlantSeedEvent;
        EventHandler.AfterSceneLoadEvent += OnAfterSceneLoadEvent;
        EventHandler.GameDayEvent += OnGameDayEvent;
    }

    private void OnDisable()
    {
        EventHandler.PlantSeedEvent -= OnPlantSeedEvent;
        EventHandler.AfterSceneLoadEvent -= OnAfterSceneLoadEvent;
        EventHandler.GameDayEvent -= OnGameDayEvent;
    }

    

    #region 事件注册
    private void OnPlantSeedEvent(ItemDetails itemDetails, TileDetails currentTile)
    {
        CropDetails currentCrop = GetCropDetails(itemDetails.itemID);
        /*存在该种子能够种植的内容,并且当前季节能够耕种*/
        if (currentCrop != null && IsValidSeason(currentCrop) && currentTile.seedItemID == -1)
        {
            // 第一次种植
            currentTile.seedItemID = itemDetails.itemID;
            currentTile.growthDays = 0;
            /*显示农作物*/
            DisplayCropPlant(currentTile, currentCrop);
        }else if (currentTile.seedItemID != -1)     // 用于刷新地图
        {
            /**/
        }
        
    }
    private void OnAfterSceneLoadEvent()
    {
        _currentGrid = FindObjectOfType<Grid>();
        _cropParent = GameObject.FindWithTag("CropParent").transform;
    }
    
    
    private void OnGameDayEvent(int gameDay, Season season)
    {
        _currentSeason = season;
    }

    #endregion


    public CropDetails GetCropDetails(int seedID)
    {
        return cropDataListSo.cropDetailsList.Find(c => c.seedItemId == seedID);
    }

    /// <summary>
    /// 检测当前所处季节是否能够播种的当前种子
    /// </summary>
    /// <param name="cureDetails">手持的种子</param>
    /// <returns></returns>
    private bool IsValidSeason(CropDetails cureDetails)
    {
        foreach (var validSeason in cureDetails.seasons)
        {
            if (_currentSeason == validSeason) return true;
        }

        return false;
    }


    /// <summary>
    /// 种植种子后显示种子样式
    /// </summary>
    /// <param name="currentTail"></param>
    /// <param name="currentCrop"></param>
    private void DisplayCropPlant(TileDetails currentTail, CropDetails currentCrop)
    {
        int growthStages = currentCrop.growthObjects.Length;
        int currentStage = 0;
        int dayCount = currentCrop.TotalGrowthDay;
        
        /*从后往前获取当前种子应该所处的生长阶段*/
        for (int i = growthStages - 1; i >= 0; i--)
        {
            if (currentTail.growthDays >= dayCount)
            {
                currentStage = i;
                break;
            }
            else
            {
                dayCount -= currentCrop.growthDays[i];
            }
        }
        /*在对应位置生成对应的item*/
        Vector3 cropPos = new Vector3(currentTail.gridPos.x + 0.5f, currentTail.gridPos.y + 0.5f, 0);
        GameObject cropPrefab = currentCrop.growthObjects[currentStage];
        var cropInstance = Instantiate(cropPrefab, cropPos, Quaternion.identity,
            _cropParent);
        cropInstance.GetComponentInChildren<SpriteRenderer>().sprite = currentCrop.growthSprites[currentStage];
    }
}

种子的成长

为了让种子每个阶段的GO能够正常显示,且和原本的ItemBase不发生冲突,因此需要新建一个Prefab

由于拥有了Crop代码,刷新所有的种子只需要通过FindObjectsOfType<>找到所有的植物GO,然后逐个销毁,再重建即可。

因此需要修改重绘地图的代码

1
2
3
4
5
6
// 在GridManager.cs 的RefreshGridMap()方法中销毁种子图片实例
Crop[] totalCrop = FindObjectsOfType<Crop>();
foreach (Crop crop in totalCrop)
{
    Destroy(crop.gameObject);
}

然后再和其他瓦片信息一起重建

1
2
3
// 在GridManager.cs 的DisplayMap()方法中重新种植种子
if (tileMapDic.Value.seedItemID != -1)
    EventHandler.CallPlantSeedEvent(currentTile.seedItemID, currentTile);

根据日期更改来修改整个tileDetails中种子种植的信息

1
2
3
4
5
6
7
//  在GridManager.cs 的OnGameDayEvent中修改当前tile的信息
// 同时保证在昨天浇了水的情况下植物才能生长
bool tileWateredYesterday = tileDic.Value.daySinceWater > -1;
if (tileDic.Value.seedItemID != -1 && tileWateredYesterday)
{
    tileDic.Value.growthDays++;
}
  • 种子种下后修改当前人物背包中(由于有一个事件是丢弃人物背包中的内容,因此我们修改当前事件的部分条件,实现如果是种子则不生成物品即可。
1
2
3
4
5
6
7
// 在itemManager.cs中实现该方法
private void OnDropItemInSceneEvent(int itemId, Vector3 mousePos, ItemType itemType)
{
    // 实现丢东西后在对应位置生成物体功能
    if (itemType == ItemType.Seed) return;  // 如果种下种子,则不表示丢弃
    .....
}

使用菜篮子收菜

整体思路与普通的切动画,完成点击事件一样,仅仅在收获的时候,需要使用碰撞检测,鼠标点击后检测到了碰撞,才能表示点击到了需要收获的物品。

  • 点击事件

由于点击需要判断当前的种子是否长成熟,因此要将CropManager.cs修改为单例模式来获取种子信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Cursor.cs的 CheckCursorValid()方法
case ItemType.CollectTool:
    CropDetails currentCrop = CropManager.Instance.GetCropDetails(currentTile.seedItemID);
    if (currentCrop != null)
    {
        if (currentTile.growthDays >= currentCrop.TotalGrowthDay) 
            SetCursorValid();
        else
            SetCursorInvalid();
    }
    else SetCursorInvalid();
   
    break;

然后将动画修改为pull相关动画

使用Unity自带的碰撞体检测,OverlapPointAll()方法,该方法传递一个鼠标坐标,然后范围该坐标下的所有碰撞体,通过检测这些碰撞体是否有Crop元素即可。

 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
// 在GridMapManager.cs的执行事件方法中调用
case ItemType.CollectTool:
    Crop currentCrop = GetCropObject(mouseGridPos);
    if (currentCrop != null)
        Debug.Log(currentCrop);
    break;



private Crop GetCropObject(Vector3 mousePos)
{
    Collider2D[] collider2Ds = Physics2D.OverlapPointAll(mousePos);
    Crop currentCrop = null;
    foreach (var col in collider2Ds)
    {
        if (col.GetComponent<Crop>())
        {
            currentCrop = col.GetComponent<Crop>();
            break;
        }
        
    }

    return currentCrop;
}

实现收割庄稼产生果实

由于不同植物需要收获的次数不同,因此需要传递采集次数给Crop来判断是否能够完成收割。

首先我们也要在CursorManager中判断当前选择的采集物品是否和当前采集的植物匹配,而匹配的方法将使用工具的ID和采集植物的工具ID是否匹配的方式进行判断,因此直接在CropDetails中进行修改

然后我们需要完成采集工作,即在gridManager中调用Crop中包含的采集方法ProcessToolAction,该方法检查当前采集次数是否满足,不满足则要求继续执行,满足则将采集的物品数量和ID通过事件的方式传递给InventoryManager,存入人物的背包中。

 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
/// <summary>
/// 检查当前使用的收割工具是否满足当前物品的要求
/// </summary>
/// <param name="toolID">当前的工具Id</param>
/// <returns></returns>
public bool CheckToolAvailable(int toolID)
{
    foreach (int harvestToolID in harvestToolItemIDs)
    {
        if (harvestToolID == toolID) return true;
    }

    return false;
}

/// <summary>
/// 检测当前工具需要收获的次数
/// </summary>
/// <param name="toolID">当前的工具Id</param>
/// <returns></returns>
public int GetTotalRequireCount(int toolID)
{
    for (int i = 0; i < harvestToolItemIDs.Length; i++)
    {
        if (harvestToolItemIDs[i] == toolID)
        {
            return requireActionCounts[i];
        }
    }

    return -1;
}
 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
public CropDetails currentCropDetails;

private int _currentHarvestCnt;

/// <summary>
/// 完成工具处理植物的具体内容
/// </summary>
/// <param name="itemID"></param>
public void ProcessToolAction(int itemID, TileDetails tileDetails)
{
    int requireHarvestCnt = currentCropDetails.GetTotalRequireCount(itemID);
    if (requireHarvestCnt == -1) return;
    if (++_currentHarvestCnt < requireHarvestCnt)
    {
        _currentHarvestCnt++;
        // todo 音效 粒子效果
    }
    else
    {
        /*判断当前的农作物是否是树(是否在人物头顶生成*/
        if (currentCropDetails.generateAtPlayerPosition)
        {
            // 真实生成物品
            SpawnHarvestItem();
        }
    }
}

public void SpawnHarvestItem()
{
    for (int i = 0; i < currentCropDetails.producedItemIDs.Length; i++)
    {
        int produceCnt = currentCropDetails.producedMinAmount[i] == currentCropDetails.producedMaxAmount[i] ? currentCropDetails.producedMinAmount[i] :
        Random.Range(currentCropDetails.producedMinAmount[i], currentCropDetails.producedMaxAmount[i] + 1);

        /* 生成对应数量的物品 */
        if (currentCropDetails.generateAtPlayerPosition)
        {
            EventHandler.CallHarvestAtPlayerPosEvent(currentCropDetails.producedItemIDs[i], produceCnt);
        }

    }
}

// inventoryManager.cs中完成
private void OnHarvestAtPlayerPosEvent(int produceID, int produceAmount)
{

    var indexIndex = GetItemIndexInBag(produceID);
    if (AddItemInIndex(produceID, indexIndex, produceAmount))
    {
        // 更新UI
        EventHandler.CallUpdateInventoryUI(InventoryLocation.Player, playerBag.inventoryItemList);
    }
}

实现农作物收割后的销毁或重新生成

实现农作物收割后的情况处理,需要在人物收获到农作物以后对当前作物的特性进行判别,如果是可以重新生长的则将其生长日期减少,直到重生次数用尽,否则就直接销毁。

判断能否重新生长需要看作物的属性以及当前生长的次数,因此需要从当前TileDetails中获得部分信息。此时如果不重新生成,则将地图信息中的种子重置为-1,然后使用事件通知的方式重新刷新地图

当前瓦片的信息可以通过调用Crop ProcessToolAction方法获取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Crop.cs 的SpawnHarvestItem()
{
    
        
       
        /*判断当前作物的是否能够重新生长,其生长次数是否达标*/
        if (currentCropDetails.dayToRegrow > -1 &&
            ++_currentTileDetails.reHarvestCnt <= currentCropDetails.regrowTimes)
        {
            _currentTileDetails.growthDays = currentCropDetails.TotalGrowthDay - currentCropDetails.dayToRegrow;
            // 刷新种子
            EventHandler.CallRefreshCurrentMapEvent();
        }
        else
        {
            _currentTileDetails.daySinceHarvest = -1;
            _currentTileDetails.seedItemID = -1;
            _currentTileDetails.daySinceDig = 0;
        }
        
        if (gameObject !=null)
            Destroy(gameObject);
    
}

制作可砍伐的木头摇晃和倒下的动画

基础设计思路和制作普通的农作物一致,只是最后一个阶段的预制体需要替换为我们预先设计好的可砍伐的树木的预制体。然后需要保证该树有我们定好的Crop脚本。

然后制作对应的动画,由于收集逻辑和砍树逻辑在鼠标的可执行阶段是类似的,因此可以将两个case合并。

GridManager.cs中采集主要的方法是ProcessToolAction,可以在该处判断当前的物体需要砍伐的次数,以及生成物品的位置。

在该过程中可以通过点击事件获取当前物体的物体的动画组件,然后执行被砍伐的时候的动画

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Crop.cs 的ProcessToolAction
if (++_currentHarvestCnt < requireHarvestCnt)
{
    _currentHarvestCnt++;
    // todo 音效 粒子效果
    if (_animator != null && currentCropDetails.hasAnimation)
    {
        /*判断人物和物品的位置,判断该从那边伐木*/
        if (Player.position.x < transform.position.x)
        {
            // 树木向右被砍的动画
            _animator.SetTrigger("RotateRight");
        }
        else
        {
            _animator.SetTrigger("RotateLeft");
        }
    }
}

摇晃动画

image-20230203165909499

实现砍树功能

由于鼠标的有效性检测检测的是瓦片格,但实际树木应该能够点按树干,因此需要对整个内容进行修改,因此需要再使用ChopTool的时候,判断当前鼠标和gridMap是否有作物成熟,但由于树干整体偏大,因此当鼠标移动到目标位置时,应该直接判断当前作物是否成熟,因此当植物成熟的时候,就可以为其添加一个标致,使得其能够被鼠标识别,同时在将对应的瓦片地图信息传递给ProcessToolAction方法时,传递的应该时当前作物的瓦片,而不是当前地图块的瓦片,因此在成熟生成prefab的时候,就应该传递当前作物的成熟信息。

  • CursorManager 增加对树干的识别,currentCrop需要通过鼠标的坐标去获取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// CursorManager
Crop currentCrop = GridMapManager.Instance.GetCropObject(_mouseWorldPos);


    case ItemType.ChopTool:
        if (currentCrop != null && currentCrop.CanHarvest)
            SetCursorValid();
        else
            SetCursorInvalid();
        break;
  • GridMapManager 此时ProcessToolAction不应该传递鼠标所在位置的tileDetails,应该是物体的信息
1
2
3
4
5
case ItemType.ChopTool:
    currentCrop = GetCropObject(mouseWorldPos);
    if (currentCrop != null)
        currentCrop.ProcessToolAction(itemDetails.itemID,  currentCrop.currentTileDetails);
    break;
  • CropManger,增加成熟后直接获取当前地图块信息,保存作物能够直接知道自己是否成熟
1
cropInstance.GetComponent<Crop>().currentTileDetails = currentTail;
  • Crop.cs,添加变量CanHarvest,让鼠标能得到信息
1
public bool CanHarvest => currentTileDetails.growthDays >= currentCropDetails.TotalGrowthDay;

修改相应动画

注意动画需要在对应载体上才能进行操作,同时添加关键帧的操作需要先点击录制.

注意动画迁移是否需要上一个状态完整播放(是否有退出时间)

实现随机数生成收割物品

由于树木倒下有时间,因此需要有一点时间,因此需要使用协程的方式循环检测当前的动画是否播放到树木倒下结束。当树木倒下的时候,生成物品。

由于生成物品是在地面上,因此需要先得知物体倒下的位置,并获得一个掉落位置的随机值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private IEnumerator HarvestAfterAnimation()
{
    while (!_animator.GetCurrentAnimatorStateInfo(0).IsName("END"))
    {
        yield return null;
    }
    SpawnHarvestItem();
    /* 生成遗留物品 */
    currentTileDetails.seedItemID = currentCropDetails.transferItemId;
    currentTileDetails.growthDays = 0;
    currentTileDetails.daySinceHarvest = 0;
    EventHandler.CallRefreshCurrentMapEvent();

}
 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
// Crop.cs中的 ProcessToolAction
if (++_currentHarvestCnt < requireHarvestCnt)
        {
            _currentHarvestCnt++;
            // todo 音效 粒子效果
            if (_animator != null && currentCropDetails.hasAnimation)
            {
                /*判断人物和物品的位置,判断该从那边伐木*/
                if (Player.position.x < transform.position.x)
                {
                    // 树木向右被砍的动画
                    _animator.SetTrigger("RotateRight");
                }
                else
                {
                    _animator.SetTrigger("RotateLeft");
                }
            }
        }
        else
        {
            /*判断当前的农作物是否是树(是否在人物头顶生成*/
            if (currentCropDetails.generateAtPlayerPosition || !currentCropDetails.hasAnimation)
            {
                // 真实生成物品
                SpawnHarvestItem();
            }
            else
            {
                if (Player.position.x < transform.position.x)
                {
                    // 树木向右被砍的动画
                    _animator.SetTrigger("FallRight");
                }
                else
                {
                    _animator.SetTrigger("FallLeft");
                }
                StartCoroutine(HarvestAfterAnimation());
            }
        }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Crop.cs 中生成果实的方法
/* 生成对应数量的物品 */
if (currentCropDetails.generateAtPlayerPosition)
{
    EventHandler.CallHarvestAtPlayerPosEvent(currentCropDetails.producedItemIDs[i], produceCnt);
}
else
{
    // 世界地图上生成物品   
    int dirx = transform.position.x - Player.position.x > 0 ? 1 : -1;   /* 农作物和人物的相对位置 */

    for (int j = 0; j < produceCnt; j++)
    {
        float randomXPos = transform.position.x +
            Random.Range(dirx, dirx + currentCropDetails.spawnRadius.x * dirx);
        float randomYPos = transform.position.y + Random.Range(-currentCropDetails.spawnRadius.y, currentCropDetails.spawnRadius.y);
        Vector3 spawnPos = new Vector3(randomXPos, randomYPos, 0);
        EventHandler.CallInstantiateBounceItemInScene(currentCropDetails.producedItemIDs[i], transform.position + new Vector3(dirx * 1.4f, 0, 0),  spawnPos);    
    }

}

实现快捷键

SlotUI增加按键方法,然后调用InventoryUI中的控制高亮的方法

 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
[RequireComponent(typeof(SlotUI))]
public class ActionBarButton : MonoBehaviour
{
    public KeyCode key;
    private SlotUI _slotUI;

    private void Start()
    {
        _slotUI = GetComponent<SlotUI>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(key))
        {
            if (_slotUI != null && _slotUI.slotItemDetails != null)
            {
                _slotUI.isSelected = !_slotUI.isSelected;
                if (_slotUI.isSelected)
                {
                    _slotUI.InventoryUI.UpdateSlotHighlight(_slotUI.slotIndex);
                }
                else
                {
                    _slotUI.InventoryUI.UpdateSlotHighlight(-1);
                }
                EventHandler.CallItemSelectedEvent(_slotUI.slotItemDetails, _slotUI.isSelected);
            }
        }
    }
}

粒子系统

在场景中增加例子效果,并将其作为一个预制体,为后续用对象池完成粒子生成做准备。因此首先需要设计粒子的类型

1
2
3
4
public enum ParticleEffectType
{
    None, LeavesFalling01,LeavesFalling02, Rock, ReapableScenery
}

使用对象池创建粒子效果

通过对象池来完成粒子效果的创建,能够不用我们手动一直创建并销毁由对象池生成的GO,其有三个主要方法,Get,ReleseDestroy,其中如果对象池如果有内容,则Get会直接获得对象池里的内容。

在本部分使用Get来是粒子效果Active,Relese让效果Active失效,这样如果需要新的粒子,对象池会先去找对象池内的GO,让其Active

 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
public class PoolManager : MonoBehaviour
{
    public List<GameObject> prefabList;
    private Dictionary<string, ObjectPool<GameObject>> _poolEffectDic = new Dictionary<string, ObjectPool<GameObject>>();

    private void OnEnable()
    {
        EventHandler.EffectEvent += OnEffectEvent;
    }

    

    private void OnDisable()
    {
        EventHandler.EffectEvent -= OnEffectEvent;
    }

    private void Start()
    {
        CreatPool();
    }

    private void CreatPool()
    {
        if (prefabList == null) return;
        foreach (GameObject prefabItem in prefabList)
        {
            string key = prefabItem.name;
            GameObject parent = new GameObject(key);
            parent.transform.SetParent(transform);      // 让所有粒子效果都属于自己对应的名称下,每个名称属于PoolManager下

            if (!_poolEffectDic.ContainsKey(key))
            {
                _poolEffectDic.Add(key, new ObjectPool<GameObject>(
                    () => Instantiate(prefabItem, parent.transform),
                    e => {e.SetActive(true);},
                    e=>{e.SetActive(false);},
                    e => {Destroy(e);}));
            }
        }
    }
    
    
    private void OnEffectEvent(ParticleEffectType effectType, Vector3 effectPos)
    {
        Debug.Log(effectType.ToString());
        if (_poolEffectDic.ContainsKey(effectType.ToString()))
        {
            ObjectPool<GameObject> effectPool = _poolEffectDic[effectType.ToString()];

            GameObject effect = effectPool.Get();
            effect.transform.position = effectPos;
            
            /*延迟关闭*/
            StartCoroutine(ReleaseRoutine(effectPool, effect));
        }
    }

    private IEnumerator ReleaseRoutine(ObjectPool<GameObject> effectPool, GameObject effect)
    {
        yield return new WaitForSeconds(1.5f);
        effectPool.Release(effect);
    }

    
}

然后通过事件调用即可

预生产树木

由于所有的农作物都是需要保证地块有种子信息,否则会在场景加载的时候被销毁。因此对于需要预制的植物,需要为其的地块设定种子信息。

因此新增一个脚本CropGenerator.cs,该脚本用于对当前瓦片信息进行赋值,并更新瓦片地图dictionary的内容。最后保证只有在首次加载地图的时候才生成。

注意此时CropGenerator.cs的生成代码是需要先给tileMapDic增加新的值,才能保证刷新地图的时候预制的物体被销毁后能够重新绘制

但是由于不同脚本的执行顺序不能确定,因此需要在刷新之前先将赋值完成,因此需要在每次加载场景的时候先保证预制物品能够修改tileMapDic的内容,并保证只有第一次加载场景,才为树木增加预制体,

 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
public class CropGenerator : MonoBehaviour
{
    [Header("作物信息")]
    public int seedId;
    public int growthDays;
    
    private Grid _currentGrid;

    private void OnEnable()
    {
        EventHandler.GenerateCropEvent += GenerateItem;
    }

    private void OnDisable()
    {
        EventHandler.GenerateCropEvent -= GenerateItem;
    }


    private void Awake()
    {
        _currentGrid = FindObjectOfType<Grid>();
    }


    public void GenerateItem()
    {
        Vector3Int curGridPos = _currentGrid.WorldToCell(transform.position);
        if (seedId != 0)
        {
            TileDetails currentTileDetails = GridMapManager.Instance.GetTileDetailsByMousePos(curGridPos);
            
            if (currentTileDetails == null)
            {
                currentTileDetails = new TileDetails();
            }

            currentTileDetails.daySinceWater = -1;
            currentTileDetails.growthDays = growthDays;
            currentTileDetails.seedItemID = seedId;
            currentTileDetails.gridPos = new Vector2Int(curGridPos.x, curGridPos.y);
            
            GridMapManager.Instance.UpdateGridMap(currentTileDetails);
        }
    }
}
1
2
3
4
5
6
7
8
// OnAfterSceneLoadEvent()
/*只有初次加载地图的时候生成预制树木*/
if (_firstLoadDic.ContainsKey(SceneManager.GetActiveScene().name) && _firstLoadDic[SceneManager.GetActiveScene().name])
{
    EventHandler.CallGenerateCropEvent();
    _firstLoadDic[SceneManager.GetActiveScene().name] = false;
}
RefreshGridMap();

制作割草的全部流程和稻草互动摇晃

当杂草过多的时候,一个一个检查杂草的碰撞体然后收割效率太低,因此 使用额外的方法来完成收割大量杂草的方法.

首先需要将杂草设定为item而不是Crop因此需要一个额外的脚本ReapItem来控制割草事件。该脚本需要获得cropDetails的信息,因此在item初始化的时候就要为其赋值,然后再ReapItem中调用收割方法,产生种子。

而检测是否是杂草的方法需要在CursorManager中进行调用,在GridMapManager中使用函数获得相应的内容,然后再CursorManager中调用该方法,最后在GridMapManager触发事件。

  • ReapItem

该部分用于实现可割除草的初始化,以及果实的产生

 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
private CropDetails _currentCropDetail;

private Transform Player => FindObjectOfType<Player>().transform;


public void InitReapItem(int cropID)
{
    _currentCropDetail = CropManager.Instance.GetCropDetails(cropID);
}


/*生成果实*/
public void SpawnHarvestItem()
{
    for (int i = 0; i < _currentCropDetail.producedItemIDs.Length; i++)
    {
        int produceCnt = _currentCropDetail.producedMinAmount[i] == _currentCropDetail.producedMaxAmount[i]
            ? _currentCropDetail.producedMinAmount[i]
            : Random.Range(_currentCropDetail.producedMinAmount[i],
                           _currentCropDetail.producedMaxAmount[i] + 1);

        /* 生成对应数量的物品 */
        if (_currentCropDetail.generateAtPlayerPosition)
        {
            EventHandler.CallHarvestAtPlayerPosEvent(_currentCropDetail.producedItemIDs[i], produceCnt);
        }
        else
        {
            // 世界地图上生成物品   
            int dirx = transform.position.x - Player.position.x > 0 ? 1 : -1; /* 农作物和人物的相对位置 */

            for (int j = 0; j < produceCnt; j++)
            {
                float randomXPos = transform.position.x +
                    Random.Range(dirx, dirx + _currentCropDetail.spawnRadius.x * dirx);
                float randomYPos = transform.position.y + Random.Range(-_currentCropDetail.spawnRadius.y,
                                                                       _currentCropDetail.spawnRadius.y);
                Vector3 spawnPos = new Vector3(randomXPos, randomYPos, 0);
                EventHandler.CallInstantiateBounceItemInScene(_currentCropDetail.producedItemIDs[i],
                                                              transform.position + new Vector3(dirx * 1.4f, 0, 0), spawnPos);
            }

        }
    }
}
  • item.cs

该部分用于在初始化物品的时候对,就对可割除的草进行初始化

1
2
3
4
5
if (itemDetails.itemType == ItemType.ReapableScenery)
{
    gameObject.AddComponent<ReapItem>();
    gameObject.GetComponent<ReapItem>().InitReapItem(itemID);
}
  • GridMapManager.cs

在该部分,用于具体的执行收割和果实生产的内容,因此需要先获得所有可收割的内容,使用碰撞检测器的一个特殊方法,找出鼠标范围内的所有可收割的部分

1
Physics2D.OverlapCircleNonAlloc(mousePos, toolDetail.itemUseRadius, colliders);

然后将所有检测到有可收割部分的内容添加到一个列表中,当点击割除事件的时候,遍历这些内容,然后进行销毁并产生果实。

注意由于鼠标要可用的时候才能执行点击方法,因此该部分需要有一个方法能够返回当前是否检测到了可收割内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 检测是否可以收割
public bool HaveReapableItemInRadius(Vector3 mousePos, ItemDetails toolDetail)
{
    /*实时检测鼠标周围是否有杂草*/
    _reapItemInRadius = new List<ReapItem>();
    Collider2D[] colliders = new Collider2D[20];        // 接受检测到碰撞的内容

    Physics2D.OverlapCircleNonAlloc(mousePos, toolDetail.itemUseRadius, colliders);

    if (colliders.Length > 0)
    {
        foreach (var col in colliders)
        {
            if (col is not null &&col.GetComponent<ReapItem>())
            {
                _reapItemInRadius.Add(col.GetComponent<ReapItem>());
            }
        }
    }

    return _reapItemInRadius.Count > 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 执行收割事件
case ItemType.ReapTool:
    
    /* 循环将被检测到的杂草一一清除,并生成对应果实 */
    for (int i = 0; i < _reapItemInRadius.Count; i++)
    {
        EventHandler.CallEffectEvent(ParticleEffectType.ReapableScenery, _reapItemInRadius[i].transform.position + Vector3.up);
        _reapItemInRadius[i].SpawnHarvestItem();
        Destroy(_reapItemInRadius[i].gameObject);
        
        // 由于鼠标检测的内容是实时生成的,因此每次列表都会刷新
        if (i > Settings.ReapLimit)
        {
            break;
        }
    }
    break;
  • CursorManager

当鼠标检测到可以收割的时候,就允许触发相应事件。

1
2
3
4
case ItemType.ReapTool:
    if (GridMapManager.Instance.HaveReapableItemInRadius(_mouseWorldPos, _currentItem)) SetCursorValid();
    else SetCursorInvalid();
    break;
  • 实现摇晃功能

使用代码完成该功能,当有碰撞体的东西i经过的时候,使用协程完成物体sprite的摇晃

因此需要摇晃的物体需要挂载一个新的脚本

  • item
1
2
3
4
5
6
7
8
if (itemDetails.itemType == ItemType.ReapableScenery)
{
    gameObject.AddComponent<ReapItem>();
    gameObject.GetComponent<ReapItem>().InitReapItem(itemID);
    gameObject.AddComponent<ItemInteractive>();
    gameObject.GetComponent<BoxCollider2D>().isTrigger = true;
    _boxCollider2D.size *= 0.5f;
}
 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
public class ItemInteractive : MonoBehaviour
{
    private bool _isAnimation = false;

    private WaitForSeconds _pauseSeconds = new WaitForSeconds(0.04f);
    
    private void OnTriggerEnter2D(Collider2D col)
    {
        if (!_isAnimation)
        {
            if (col.transform.position.x > transform.position.x)
            {
                StartCoroutine(RotateLeft());
            }
            else
            {
                StartCoroutine(RotateRight());
            }
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (!_isAnimation)
        {
            if (other.transform.position.x > transform.position.x)
            {
                StartCoroutine(RotateRight());
            }
            else
            {
                StartCoroutine(RotateLeft());
            }
        }
    }



    private IEnumerator RotateLeft()
    {
        _isAnimation = true;
        for (int i = 0; i < 4; i++)
        {
            transform.GetChild(0).Rotate(0,0,2);        // 延Z轴摇晃两度
            yield return _pauseSeconds;
        }
        
        for (int i = 0; i < 5; i++)
        {
            transform.GetChild(0).Rotate(0,0,-2);        // 延Z轴摇晃两度
            yield return _pauseSeconds;
        }
        transform.GetChild(0).Rotate(0,0,2);        // 延Z轴摇晃两度
        yield return _pauseSeconds; 
        

        _isAnimation = false;
    }

    private IEnumerator RotateRight()
    {
        _isAnimation = true;
        for (int i = 0; i < 4; i++)
        {
            transform.GetChild(0).Rotate(0,0,-2);        // 延Z轴摇晃两度
            yield return _pauseSeconds;
        }
        
        for (int i = 0; i < 5; i++)
        {
            transform.GetChild(0).Rotate(0,0,2);        // 延Z轴摇晃两度
            yield return _pauseSeconds;
        }
        transform.GetChild(0).Rotate(0,0,-2);        // 延Z轴摇晃两度
        yield return _pauseSeconds;
        _isAnimation = false;
    }
}
Built with Hugo
Theme Stack designed by Jimmy