Unity 农场 1 —— 环境搭建、背包系统、时间系统

这篇具有很好参考价值的文章主要介绍了Unity 农场 1 —— 环境搭建、背包系统、时间系统。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

搭建初始地图环境

素材预处理

遮挡层级效果

景观的半遮挡与透明

人物移动

绘制瓦片地图

碰撞层

添加摄像机的边界

(Editor)使用 UI Toolkit 和 UI Builder 制作物品编辑器【待解决】

 背包系统

背包数据初始化

InventoryManager——总库存管理

实现地图上显示数据库中的物品

背包系统

行动栏

根据数据库中的数据显示背包中的数量

选中物品高亮和背包开关

拖拽交换及数据改变

在地图上生成物品

显示物品的详细信息

人物的移动及举起物品

实现选中背包物品触发举起动画

构建游戏的时间系统

时间流逝

时间UI及对应的时间变更

场景间切换

场景切换

人物跨场景移动以及场景加载前后事件

设置鼠标指针根据物品调整


视频演示:

unity农场游戏(包含背包、种植系统及npc使用A*寻路)_哔哩哔哩bilibili_演示

Unity 农场 1 —— 环境搭建、背包系统、时间系统

搭建初始地图环境

在package manager中删除无用的包可以使速度变快。

素材预处理

找到素材中的:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

为了使得其他的素材图片也按照这种方式,可以以当前设定来创建一种预设。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

然后就可以同时选中多个物体,让他们应用同样的这个预设。

切割图片的方法:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来对人物动画同理做切割:

切割时由于动画是8帧,选择切成8个,锚点选在脚底是为了能够实现一个正确的遮挡效果。 

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来创建人物,

 在人物的锚点这里选择底部

Unity 农场 1 —— 环境搭建、背包系统、时间系统

创建人物:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

遮挡层级效果

为了防止人物的一些部位对其他物品进行遮挡,产生“分家”的现象,添加一个sorting group

在父物体添加一个sorting group可以使得父物体和子物体一起渲染

Unity 农场 1 —— 环境搭建、背包系统、时间系统

然后新建一个层,将几个子物体设定为

Unity 农场 1 —— 环境搭建、背包系统、时间系统Unity 农场 1 —— 环境搭建、背包系统、时间系统 

接下来我们希望实现这样的效果(因为是俯视角的游戏)(当人物走到草丛前面时会遮挡草丛(这个走到前面和后面不是通过z轴,而是通过y轴)):

但是在2d的情况下,在同样的层次下渲染前后遮挡关系是通过z轴实现的。我们希望通过y轴来实现,则修改如下:

(注意物体的锚点要设置在底部)

Unity 农场 1 —— 环境搭建、背包系统、时间系统

景观的半遮挡与透明

简单来说就是走到树后面,树会变成半透明。

让树渐变的方法可以当触发trigger函数时使用协程,让树的α值缓慢的从1变成0。

此处使用DOTween。

调用思路:在树的身上挂载一个脚本叫做ItemFader,这个脚本具有淡入和淡出的函数:

using UnityEngine;
using DG.Tweening;


[RequireComponent(typeof(SpriteRenderer))]
public class ItemFader : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;


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

    public void FadeIn()
    {
        Color targetColor = new Color(1, 1, 1, 1);
        spriteRenderer.DOColor(targetColor, Settings.fadeDuration);
    }

    public void FadeOut()
    {
        Color targetColor = new Color(1, 1, 1, Settings.targetAlpha);
        spriteRenderer.DOColor(targetColor, Settings.fadeDuration);
    }

}

为了方便后续查找更改的数据,建立一个setting脚本用于储存常量:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Settings
{
    public const float fadeDuration = 0.35f;

    public const float targetAlpha = 0.45f;

}

当玩家触发了trigger函数时,获取碰撞体的所有子物体的ItemFader脚本,然后调用淡入和淡出的函数。

效果如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

人物移动

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed = 5;
    Rigidbody2D rb;
    private float inputX;
    private float inputY;
    private Vector2 movementInput;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        //用update函数接受数据,而不是改变物体的刚体
        PlayerInput();   
    }

    private void FixedUpdate()
    {
        //对于改变刚体的运动,使用fixupdate来实现
        Movement();
    }

    private void PlayerInput()
    {
        inputX = Input.GetAxisRaw("Horizontal");
        inputY = Input.GetAxisRaw("Vertical");
        if (inputX != 0 && inputY != 0)
        {
            inputX *= 0.6f;
            inputY *= 0.6f;
        }
        movementInput = new Vector2(inputX, inputY);
    }


    private void Movement()
    {
        rb.MovePosition(rb.position + movementInput * speed * Time.deltaTime);
    }
}

绘制瓦片地图

为了使得和环境更好交互,创建一系列的瓦片地图,其在不同的sorting layer上:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 Unity 农场 1 —— 环境搭建、背包系统、时间系统

  • 创建规则的瓦片信息

Unity 农场 1 —— 环境搭建、背包系统、时间系统

然后根据自己想要设定的规则,可以绘制出有规律的地图

Unity 农场 1 —— 环境搭建、背包系统、时间系统

注意到瓦片地图间会有这样的缝隙:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

解决方法:

创建一个Sprite Altas,这个图集会将所有的素材打包在一起,引用时忽略该图集。

将Maps以及其他需要打包的地图素材这个文件夹放入,即可实现。Unity 农场 1 —— 环境搭建、背包系统、时间系统

添加这个相机使得 

Unity 农场 1 —— 环境搭建、背包系统、时间系统

碰撞层

为了产生碰撞效果,且碰撞是个整体,添加这三个组件并设定如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来绘制空气墙:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

可以修改三角形的碰撞体积,通过下面这种方式手动调整:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 此时碰撞体积就改变了​​​

​​​​Unity 农场 1 —— 环境搭建、背包系统、时间系统Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来根据地图来绘制碰撞体:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

添加摄像机的边界

Unity 农场 1 —— 环境搭建、背包系统、时间系统

创建一个边界,添加polygon coiilder并设定边界范围:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来我们希望通过读取不同的场景时,摄像机会读取该场景的边界并设定边界:

为上面添加的polygon collider添加tag。

然后为虚拟摄像机添加如下代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;

public class SwitchBounds : MonoBehaviour
{

    private void Start()
    {
        SwitchConfinerShape();
    }
    private void SwitchConfinerShape()
    {
        PolygonCollider2D confinerShape = GameObject.FindGameObjectWithTag("BoundsConfiner").GetComponent<PolygonCollider2D>();

        CinemachineConfiner confiner = GetComponent<CinemachineConfiner>();

        confiner.m_BoundingShape2D = confinerShape;

        //Call this if the bounding shape's points change at runtime
        confiner.InvalidatePathCache();
    }
}
  • 完善地图

创建房子

Unity 农场 1 —— 环境搭建、背包系统、时间系统

注意,房子的两个部分要建立在不同的层级,房子下半部分不会遮挡玩家,但有碰撞,吃呢价格i设置在GroundTop,但是房子的上层会遮挡玩家,但无碰撞,因此放在Front1的层级。

  •  创建树和其对应的动画

效果:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 以及可以砍伐的树木:由树的树木和树干所组成

Unity 农场 1 —— 环境搭建、背包系统、时间系统Unity 农场 1 —— 环境搭建、背包系统、时间系统

(Editor)使用 UI Toolkit 和 UI Builder 制作物品编辑器【待解决】

  • 要自己写好 ItemDataLis_SO

目标是设计成下面这个样子

Unity 农场 1 —— 环境搭建、背包系统、时间系统

UIToolKit可以在可视化的情况下来编辑Customer Editor,在runtime下也可以使用。

创建一个Editor文件夹:(Editor文件夹在打包时不会被打包进去)

Unity 农场 1 —— 环境搭建、背包系统、时间系统

创建一个这个 

Unity 农场 1 —— 环境搭建、背包系统、时间系统

则会生成三个文件:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

制作好了编辑器后,添加物品如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

背包系统

背包数据初始化

采用MVC的模式,将控制、数据、和显示逐一分开,通过inventory manager来管理数据,并通过它来呼叫UI的显示内容。

创建一个DataCollection,我们会将一些由多个自己定义的变量组合成的类都放在这个文件当中,方便集中管理,查找和修改。

接下来创建一个物品所具体拥有的各种信息,

1.为便于管理,这些物品具有ID,名字。

2.为了之后能让玩家根据不同类型的物品有不同的效果,设定itemType。

3.还需为该物体设定背包中的图片展示以及该物体在世界中的图片效果。

4.该物体具有描述信息

5.各种后续可能会用到的一些物品属性,如能否被拾取、丢弃等。


using UnityEngine;

[System.Serializable]
//用来存储物品的详细信息 
public class ItemDetails
{
    public int itemID;
    public string name;

    public ItemType itemType;

    public Sprite itemIcon;
    public Sprite itemOnWorldSprite;

    public string itemDescription;
    public int itemUseRadius;

    public bool canPickedup;
    public bool canDropped;
    public bool canCarried;
    public int itemPrice;

    [Range(0, 1)]
    public float sellPercentage;
}
  • Enums 创建 ItemType 物品类型

public enum ItemType
{
    Seed,Commodity,Furniture,
    HoeTool,ChopTool,BreakTool,ReapTool,WaterTool,CollectTool,
    ReapableScenery
}
  • 生成 ItemDetailsList_SO 文件做为整个游戏的物品管理数据库

创建一个SO菜单栏,其对应的SO包含很多个物品,所以使用的是List<ItemDetails>的数据结构。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


[CreateAssetMenu(fileName ="ItemDataList_SO",menuName ="Inventory/ItemDataList")]

public class ItemDataList_SO : ScriptableObject
{
    public List<ItemDetails> itemDetailsList;
}

创建一个文件夹用来管理数据,并创建一个示例SO。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

InventoryManager——总库存管理

InventoryManager是一个用来管理所有背包系统的代码,在后续不断更新中会不断补充代码。

其主要核心包括物品的SO数据(即所有种类物品的一个列表)和背包的SO数据。

而其因为是单例模式,所以我们想获取背包或者某种物品时,也可以通过直接调用InventoryManager.ItemDataList_SO或者InventoryManager.playerBag。

这就是单例模式的作用之一。

下面书写一个泛型单例模式,后续有脚本想让其使用单例模式就可以让其实现这个泛型即可

泛型单例模式代码


using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static T instance;

    public static T Instance
    {
        get => instance;
    }

    protected virtual void Awake()
    {
        if (instance != null)
            Destroy(gameObject);
        else instance = (T)this;
    }


    protected virtual void OnDestroy()
    {
        if (instance != this)
            instance = null;
    }

}

 为inventory manager添加命名空间,方便管理数据,避免互相乱调用的耦合情况。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MFarm.Inventory
{
    public class InventoryManager : Singleton<InventoryManager>
    {
        public ItemDataList_SO ItemDataList_SO;
    }

}

添加命名空间后,想在其他的代码中使用inventoryManager,就得using这个命名空间。

为其添加通过ID查找的功能后的完整代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MFarm.Inventory
{
    public class InventoryManager : Singleton<InventoryManager>
    {
        public ItemDataList_SO ItemDataList_SO;


        /// <summary>
        /// 
        ///通过ID返回物品信息
        /// </summary>
        /// <param name="ID"></param>
        /// <returns></returns>
        public ItemDetails GetItemDetails(int ID)
        {
            //找到某个itemDetails,它的ID等于所给ID
            return ItemDataList_SO.itemDetailsList.Find(i => i.itemID == ID);
        }

    }



}

在主场景中添加这个并绑定SO,因为是在主场景,所以场景切换时这个不需要改变。 

Unity 农场 1 —— 环境搭建、背包系统、时间系统

实现地图上显示数据库中的物品

创建一个itemBase,其作用是,当场景中会生成一些物品,比如砍了树会生成木头,以及果实开花产生种子。我们希望这些情况时,可以通过代码去inventoryManager里面获得ID对应的物品详情。

为其添加碰撞体,作用是比如当玩家触碰时,就将它添加到背包中。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来通过代码来根据不同的ID显示不同的物品,思路很简单,就是通过ID去物品数据库中获取对应的图片并展示出图片即可。而不同的图片的大小不一样,所以根据图片的大小去修改碰撞体的体积。

(这个物品数据库并不是背包,而是ID为1的物品,它的信息是怎么样的,ID为2的物品,信息是怎么样的)

例如这样:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MFarm.Inventory
{


    public class Item : MonoBehaviour
    {
        public int itemID;

        private SpriteRenderer spriteRenderer;
        private BoxCollider2D coll;
        private ItemDetails itemDetails;

        private void Awake()
        {
            spriteRenderer = GetComponentInChildren<SpriteRenderer>();
            coll = GetComponent<BoxCollider2D>();
        }

        private void Start()
        {
            if (itemID != 0)
            {
                Init(itemID);
            }
        }

        public void Init(int ID)
        {
            itemID = ID;

            //Inventory获得当前数据
            itemDetails = InventoryManager.Instance.GetItemDetails(itemID);

            //因为返回的结果有可能是空的,如果在非空的情况下显示图片即可
            if (itemDetails != null)
            {
                spriteRenderer.sprite = itemDetails.itemOnWorldSprite != null ? itemDetails.itemOnWorldSprite : itemDetails.itemIcon;

                //修改碰撞体尺寸,让其能和不同的图片一一对应
                Vector2 newSize = new Vector2(spriteRenderer.sprite.bounds.size.x, spriteRenderer.sprite.bounds.size.y);
                coll.size = newSize;
                coll.offset = new Vector2(0, spriteRenderer.sprite.bounds.center.y);
            }
        }

    }

}
  • 拾取物品

拾取物品的逻辑如下,为玩家添加拾取物品的脚本,当触发trigger时,获取该物体的item组件,然后如果有item组件,则调用数据库InventoryManager中的AddItem函数,添加物品进背包所在的数据库中。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 当玩家挂载上这个脚本时将使得玩家可以拾取物体
/// </summary>
public class ItemPickUp : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("trigger");
        Item item = collision.GetComponent<Item>();

        if (item != null)
        {
            if (item.itemDetails.canPickedup)
            {
                InventoryManager.Instance.AddItem(item, true);
            }
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        Debug.Log("collision");
    }
}

具体的往数据库里添加信息的代码如下:

在InventoryManager中添加如下代码:

        /// <summary>
        /// 添加物品到Player背包里
        /// </summary>
        /// <param name="item"></param>
        /// <param name="toDestory">是否要销毁物品</param>
        public void AddItem(Item item, bool toDestory)
        {
            Debug.Log(GetItemDetails(item.itemID).itemID + "Name: " + GetItemDetails(item.itemID).itemName);
            if (toDestory)
            {
                Destroy(item.gameObject);
            }
        }

背包系统

首先先书写背包中的物品的格式:

[System.Serializable]
/// <summary>
/// 这个是在背包中的物品,具有两个变量,物品的ID,及背包中该物品的数量
/// </summary>
public struct InventoryItem
{
    //使用结构而不是类,是因为类需要进行判空,可能会导致一些意想不到的bug
    public int itemID;
    public int itemAmount;
}

然后背包就是含有这类物品的一个List,将其用SO存储,如下所示:

背包的SO代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


[CreateAssetMenu(fileName ="InventoryBag_SO",menuName ="Inventory/InventoryBag_SO")]
public class InventoryBag_SO : ScriptableObject
{
    public List<InventoryItem> itemList;
}

这个背包适用于所有东西,除了玩家自己的背包,还可以是npc的商店。

对于添加一个物体前需要思考,背包是否满了,是否有该物品,这些都需要在AddItem里面实现。

在InventoryManager中添加如下代码即可实现该功能:

        /// <summary>
        /// 添加物品到Player背包里
        /// </summary>
        /// <param name="item"></param>
        /// <param name="toDestory">是否要销毁物品</param>
        public void AddItem(Item item, bool toDestory)
        {

            var index = GetItemIndexInBag(item.itemID);
            AddItemAtIndex(item.itemID,index,1);


            //对于添加一个物体前需要思考,背包是否满了,是否有该物品
            Debug.Log(GetItemDetails(item.itemID).itemID + "Name: " + GetItemDetails(item.itemID).itemName);
            if (toDestory)
            {
                Destroy(item.gameObject);
            }
        }


        /// <summary>
        /// 检查背包是否有空位
        /// </summary>
        /// <returns></returns>
        private bool CheckBagCapacity()
        {
            for(int i = 0; i < playerBag.itemList.Count;i++)
            {
                if (playerBag.itemList[i].itemID == 0)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 根据ID查找包里是否有这个东西
        /// </summary>
        /// <param name="ID"></param>
        /// <returns></returns>
        private int GetItemIndexInBag(int ID)
        {
            for (int i = 0; i < playerBag.itemList.Count; i++)
            {
                if (playerBag.itemList[i].itemID == ID)
                {
                    return i;
                }
            }
            return -1;
        }


        private void AddItemAtIndex(int ID,int index,int amount)
        {

            if (index == -1&&CheckBagCapacity())
            {
                var item = new InventoryItem { itemID = ID, itemAmount = amount };

                for (int i = 0; i < playerBag.itemList.Count; i++)
                {
                    if (playerBag.itemList[i].itemID == 0)
                    {
                        playerBag.itemList[i] = item;
                        break;
                    }
                }
            }
            else
            {
                int currentAmount = playerBag.itemList[index].itemAmount + amount;

                var item = new InventoryItem { itemID = ID, itemAmount = currentAmount };

                playerBag.itemList[index] = item;
            }
        }

    }

添加了以上这些代码后,就可以实现拾取物品后将其 添加到数据库中了

上面实现了代码层面将物品添加到数据库,下面把UI面板展示出来,并且让UI中的每个格子都对应数据库的内容
 

行动栏

此处使用一个新的场景来实现UI,并创建画布

Unity 农场 1 —— 环境搭建、背包系统、时间系统

画布中做出如下更改:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 Unity 农场 1 —— 环境搭建、背包系统、时间系统

 Unity 农场 1 —— 环境搭建、背包系统、时间系统

 做好行动栏的相关UI并排好布局。Unity 农场 1 —— 环境搭建、背包系统、时间系统

做好背包中的UI

Unity 农场 1 —— 环境搭建、背包系统、时间系统

根据数据库中的数据显示背包中的数量

Unity 农场 1 —— 环境搭建、背包系统、时间系统

在键盘的navigation中,有这个选项,它是用来实现可以通过键盘来更改选项的。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

通过visualize可以看到各个间的关系

Unity 农场 1 —— 环境搭建、背包系统、时间系统

此处不需要该功能,因此选择none。

在enums中增添三种枚举:用于区分三种不同背包间的格子类型的type:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来需要通过脚本获取这个SlotBag上的一些属性,

并且通过代码来实现,初始化为空(让其变为非选中状态,以及让其不能被选中,图片也关闭的状态,),以及更新格子的代码。

对于这个格子,其获取对应的子物体的image,最高效率的是直接在inspector窗口中拖拽。 

(在Awake函数里获取,可能会导致一些意想不到的bug)

Unity 农场 1 —— 环境搭建、背包系统、时间系统

完整代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;


namespace MFarm.Inventory
{
    public class SlotUI : MonoBehaviour
    {
        [Header("组件获取")]

        //使用SerializeField可以使得提前在inspector窗口中提取获取好image
        [SerializeField] private Image slotImage;

        [SerializeField] private TextMeshProUGUI amountText;
        [SerializeField] private Image slotHighlight;
        [SerializeField] private Button button;//需要实现无物品的地方不能被点按,所以需要获取button

        [Header("格子类型")]
        public SlotType slotType;

        public bool isSelected;//用于判断是否被选择


        public ItemDetails itemDetails;
        public int itemAmount;



        private void Start()
        {
            //开始的时候需要判空
            isSelected = false;

            if (itemDetails.itemID == 0)
            {
                UpdateEmptySlot();
            }
        }

        /// <summary>
        /// 更新格子UI和信息
        /// </summary>
        /// <param name="item">itemDetails</param>
        /// <param name="amount">持有数量</param>
        public void UpdateSlot(ItemDetails item, int amount)
        {
            itemDetails = item;
            slotImage.sprite = item.itemIcon;
            itemAmount = amount;
            amountText.text = amount.ToString();
            button.interactable = true;
        }


        /// <summary>
        /// 如果是空的slot则不能让其选择,不能互动
        /// </summary>
        public void UpdateEmptySlot()
        {
            if (isSelected)
            {
                isSelected = false;
            }

            slotImage.enabled = false;
            amountText.text = string.Empty;
            button.interactable = false;
        }

    }

}

上面写的是格子本身的UI脚本,其包含初始化自身,以及提供了一个供外部调用的更新UI的接口。

接下来书写一个Inventory的UI脚本,其用来控制背包自身的一些东西(比如金钱数量的改变),以及控制格子。

在InventoryUI中应该包含所有的格子(包含Action Bar行动栏和背包里的所有格子)

namespace MFarm.Inventory
{
    public class InventoryUI : MonoBehaviour
    {

        [SerializeField] private SlotUI[] playerSlots;
        // Start is called before the first frame update
    }

}

将行动栏和背包中的Slot拖拽过来:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 我们在InventoryManager添加物品以及交换物品时,需要使得对应的UI发生改变。

在此处,我们往更大的范围想,不止局限于背包,比如还有场景中的箱子。因此我们在更新UI时,要确定它是在哪个位置。

所以在enums中添加新的枚举类型,用于说明这个库存在哪个位置 

Unity 农场 1 —— 环境搭建、背包系统、时间系统

那么如何更新UI信息呢?此处我们摈弃在3D RPG中使用的,通过单例模式的InventoryManger去调取InventoryUI,然后获取变量值去修改的方法。

此处使用一个类似事件中心的方法,每次呼叫这个事件时提供对应的参数,所有注册到这个事件里的函数都会被执行。比如写一个开始新游戏的事件,这个事件的Action一执行,所有注册函数都将执行。

事件的代码逻辑如下:

首先事件我们将其放在一个静态类中,使得其他代码可以直接调用,且不需要继承MonoBehaviour:

public static class EventHandler 
{
}

书写一个事件的类型,想要注册到这个事件的函数必须具有相同的参数:

    public static event Action<InventoryLocation, List<InventoryItem>> UpdateInventoryUI;

接下来,我们在管理InventoryUI的脚本中添加如下代码:逻辑是将函数注册到这个事件

Unity 农场 1 —— 环境搭建、背包系统、时间系统

这个函数是用于更新和Player相关的InventoryUI的信息。

更新UI需要做什么操作?很简单,当前代码是InventoryUI,我们只需要根据list中的物品,如果其物品数量大于0则更新数据,为0则将其更新为0,方法是调用每个格子内部SlotUI提供给外部的更新数据的接口。

注意因为需要注册到这个事件,所以对应的参数必须相同。

根据以上两个要求写出的代码如下:

        /// <summary>
        /// 更新UI需要做的操作,根据对应的Slot里的物品数量更新信息
        /// /// </summary>
        /// <param name="location"></param>
        /// <param name="list"></param>
        private void OnUpdateInventoryUI(InventoryLocation location, List<InventoryItem> list)
        {
            switch (location)
            {
                case InventoryLocation.Player:
                    for(int i = 0; i < playerSlots.Length; i++)
                    {
                        if (list[i].itemAmount > 0)
                        {
                            var item = InventoryManager.Instance.GetItemDetails(list[i].itemID);
                            playerSlots[i].UpdateSlot(item, list[i].itemAmount);
                        }
                        else
                        {
                            playerSlots[i].UpdateEmptySlot();
                        }
                    }
                    break;            
            }
        }


书写完了注册的函数后,接下来需要书写调用这些函数的方法:

在EventHandler中添加这个函数:其功能是唤醒所有注册到了UpdateInventoryUI这个事件上的函数。

    public static void CallUpdateInventoryUI(InventoryLocation location,List<InventoryItem> list)
    {
        UpdateInventoryUI?.Invoke(location, list);//判断是否为空,不为空的话则执行
    }

使用示例如下,当比如在InventoryManager中添加物品时,我们去EventHandler中调用这个函数即可:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

别忘了开始时若有数据也需要更新一下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

这样即可实现拾取物品时更新对应的UI信息。

此处在注册函数时,把注册函数的时机放在OnEnable中。

OnAwake和OnEnable的区别?

OnAwake是在脚本实例生命周期中仅被调用一次,用来进行初始化的操作。OnEnable是当对象被激活时,就会调用,如果反复激活就会反复调用。

效果如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

选中物品高亮和背包开关

  • 背包按钮的控制开关 

逻辑很简单,就是获取Bag的GameObject,然后通过按钮或者按键控制其SetActive即可。

每按一次按键B或者按钮就调用这个函数就可以了。

        public void OpenBagUI()
        {
            bagOpened = !bagOpened;

            bagUI.SetActive(bagOpened);
        }
  • 选中物体时的高亮效果

首先需要在SlotUI中引入事件系统,然后让SlotUI继承这个接口:IPointerClickHandler,这个接口是在按下去时会被触发。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

继承了这个接口后,然后就可以重写这些接口所承载的函数,这些函数会在适当的时机自动被调用

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接口中的参数包含了很多信息,比如点击的次数还有时间,如果想要获取这个信息只需要调用这个参数的成员变量即可。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

起初的逻辑很简单,被点选时高亮即可,代码如下:

        public void OnPointerClick(PointerEventData eventData)
        {
            if (itemAmount == 0) return;//如果没有东西就不做任何操作
            isSelected = !isSelected;//切换成相反的状态

            slotHighlight.gameObject.SetActive(isSelected);
        }
    }

但是这样会出现一个问题,那就是两个格子可以同时被选上同时被高亮!

Unity 农场 1 —— 环境搭建、背包系统、时间系统

我们只希望同时只有一个能被高亮。最简单方法就是当某个格子按下时,通知其他格子暗下去。但是显然一个格子没有通知其他格子暗下去的方法。但是SlotUI的父物体InventoryUI显然可以控制全部格子。

那方法就是首先在每个格子的SlotUI中获取父物体:

        private InventoryUI inventoryUI => GetComponentInParent<InventoryUI>();

当需要只让自己的格子亮,其他的格子暗下时,把当前格子的index传给父物体,让父物体InventoryUI去控制自己亮其他暗。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

我们希望点选时有个动画效果,方法如下,在预制体的Highlight中添加动画和动画控制器:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

然后创建一个序列帧动画给这个动画控制器即可。最终效果如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

补充:此处将前面的bug修改一些:

SlotUI中修改下面的代码

更新空物体的UI时,之前漏了把amount也要设置为0才可以

Unity 农场 1 —— 环境搭建、背包系统、时间系统

拖拽交换及数据改变

  • 物品拖拽

在3d rpg中实现物体拖拽时,是通过直接将该物体拖拽起来,此处换一种方法实现,是新生成一个对应的图片。拖拽完成时将它关闭。

创建一个canvas画布,用于拖拽的物品,因为拖拽的物品要展示在前面,为了使得该画布的渲染顺序在前,在这里需要设定次序:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

这里的image有一个点需要注意:选中Raycast Target时它会阻挡鼠标的射线判断。 

当鼠标前面有一个图片的时候,这个射线就会被遮挡,就没有办法选中或者测试图片下面其他的东西了。所以这个不能勾选

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 (其实就是,这个物体是否是射线的目标。如果我们不希望它是射线的目标,我们就不勾选)

别忘了字体的涉嫌遮挡也要剔除:

字体的射线遮挡在extra中:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 对于背包,我们不希望背包下面的这三个物体遮挡射线,我们希望射线能直接投射到格子上面,这样我们就可以实现检测到鼠标释放时的射线是射到了哪个格子上,从而实现物品交换。

因此 ,在这下面,我们

Unity 农场 1 —— 环境搭建、背包系统、时间系统

这样就可以检测到格子了。

  • 实现拖拽物品交换数据

交换背包和交换数据的思想如下,在playerbag的SO中有26个数据,交换对应的数据即可:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 代码如下:

在InventoryManager中添加如下代码:

        /// <summary>
        /// Player背包范围内交换物品
        /// </summary>
        /// <param name="fromIndex">起始序号</param>
        /// <param name="targetIndex">目标数据序号</param>
        public void SwapItem(int fromIndex, int targetIndex)
        {
            InventoryItem currentItem = playerBag.itemList[fromIndex];
            InventoryItem targetItem = playerBag.itemList[targetIndex];

            if (targetItem.itemID != 0)
            {
                playerBag.itemList[fromIndex] = targetItem;
                playerBag.itemList[targetIndex] = currentItem;
            }
            
            else
            {
                playerBag.itemList[targetIndex] = currentItem;
                playerBag.itemList[fromIndex] = new InventoryItem();//如果其中一个物品为空,则新建即可
            }

            EventHandler.CallUpdateInventoryUI(InventoryLocation.Player, playerBag.itemList);
        }



    }

在结束拖拽时候,代码如下:

思路是,首先判断射线检测的物品是否非空,是否含格子。如果含格子,是不是同种格子的交换,如果是的话,调用InventoryManager中的函数交换格子即可。

        public void OnBeginDrag(PointerEventData eventData)
        {
            if (itemAmount != 0)
            {
                inventoryUI.dragItem.enabled = true;
                inventoryUI.dragItem.sprite = slotImage.sprite;
                inventoryUI.dragItem.SetNativeSize();//如果拖拽的东西很大,通过调用这个选项防止图片失真
                isSelected = true;
                inventoryUI.UpdateSlotHighlight(slotIndex);
            }
        }

        //拖拽时让拖拽的图片的位置等于鼠标的位置
        public void OnDrag(PointerEventData eventData)
        {
            inventoryUI.dragItem.transform.position = Input.mousePosition;
        }

        //结束拖拽时让其不可见
        public void OnEndDrag(PointerEventData eventData)
        {
            inventoryUI.dragItem.enabled = false;
            Debug.Log(eventData.pointerCurrentRaycast.gameObject);
            // Debug.Log(eventData.pointerCurrentRaycast.gameObject);

            if (eventData.pointerCurrentRaycast.gameObject != null)
            {
                if (eventData.pointerCurrentRaycast.gameObject.GetComponent<SlotUI>() == null)
                    return;

                var targetSlot = eventData.pointerCurrentRaycast.gameObject.GetComponent<SlotUI>();
                int targetIndex = targetSlot.slotIndex;

                //在Player自身背包范围内交换,通过传入index然后让manager进行交换
                if (slotType == SlotType.Bag && targetSlot.slotType == SlotType.Bag)
                {
                    InventoryManager.Instance.SwapItem(slotIndex, targetIndex);
                }

                //清空所有高亮显示
                inventoryUI.UpdateSlotHighlight(-1);
            }
            //else    //测试扔在地上
            //{
            //    if (itemDetails.canDropped)
            //    {
            //        //获取鼠标对应世界地图坐标,然后作为调用事件的参数即可
            //        var pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, -Camera.main.transform.position.z));

            //        EventHandler.CallInstantiateItemInScene(itemDetails.itemID, pos);
            //    }
            //}
        }

Unity 农场 1 —— 环境搭建、背包系统、时间系统

在地图上取出背包的物品

在地图上取出背包的物品的思路也是通过事件实现。

完整思路是:在SlotUI中的拖拽物品结束时,如果目的地不是格子而是世界里,那么我们先获取鼠标射线的位置,然后将位置和ID作为参数去调用这个事件。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

因此需要在EventHandler中添加事件:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

所有背包里的物品是通过一个InventoryManager来对其进行统一管理的,对于世界中的物品Item,我们同样创建一个Manager对其进行管理。

承接上文,我们有了事件,我们需要有具体的函数注册该事件,这样该事件才有用。由于我们通过ItemManager对事件进行管理,所以在ItemManager中书写具体的生成物品的函数。

代码思路很简单,首先将函数注册到该事件,然后书写具体生成物品的函数,生成物品的函数也很简单,根据传入的位置、物品ID,调用Instantiate生成该物品即可。

创建一个专门用来生成物品的prefab,其含有item脚本和boxCollider(勾选trigger)

我们使用Instantiate生成物品时,只需要生成下面这个物品即可,然后我们只要为其设定好ID,其就会自动根据ID在地图中显示它长什么样。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

为了统一方便管理,使用一个ItemParent作为它们的父物体:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MFarm.Inventory
{
    public class ItemManager : MonoBehaviour
    {
        public Item itemPrefab;//待生成物品的预制体
        private Transform itemParent;
        
        private void OnEnable()
        {
            EventHandler.InstantiateItemInScene += OnInstantiateItemInScene;
        }

        private void OnDisable()
        {
            EventHandler.InstantiateItemInScene -= OnInstantiateItemInScene;
        }
        private void Start()
        {
            itemParent = GameObject.FindWithTag("ItemParent").transform;
        }

        private void OnInstantiateItemInScene(int ID, Vector3 pos)
        {
            var item = Instantiate(itemPrefab, pos, Quaternion.identity,itemParent);
            item.itemID = ID;
        }


    }

}

在上面的ItemManager调用完毕生成了这个预制体后,因为其包含item这个脚本,item脚本则会在此处自动调用Init函数自动根据ID来赋予属性。


效果如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

设定了verticalLayoutGroup后,所有的一级子物体,会整齐的排列在一起:Unity 农场 1 —— 环境搭建、背包系统、时间系统

 如下图所示,下图就是对应的三个框

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 假如设定了space 下面就会变成这样

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

显示物品的详细信息

首先创建一个UIImage,和下面的这几个组件

Unity 农场 1 —— 环境搭建、背包系统、时间系统

  

由于其从上到下包含多个信息:如名字,描述。为了让其能纵向排列使用vertical layoutGroup。

为了使其能根据内容拉伸,使用ContentSizeFitter。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

设计一个描述的内容ui,

Unity 农场 1 —— 环境搭建、背包系统、时间系统

为了实现描述的内容会根据内容多少而扩充长度,可以在父物体使用content Size Filter,并勾选

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 Unity 农场 1 —— 环境搭建、背包系统、时间系统

它会对这个组件以及这个组件的一级子物体,当大小变换时也会随之变换(注意是一级子物体,意思就是子物体变大,自己也会跟着变大,但是如果子物体的子物体变大,那么自己是不会变大的。)

由于contentSizeFilter会随着组件的大小而自动扩充大小,那么当没有字体的时,框的大小就为0,因此可以使用:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

书写ItemTooltip,用于具体的实现展示详情的函数,然后在另外一个函数中会有On

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ItemTooltip : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI nameText;
    [SerializeField] private TextMeshProUGUI typeText;
    [SerializeField] private TextMeshProUGUI descriptionText;
    [SerializeField] private Text valueText;
    [SerializeField] private GameObject bottomPart;

    public void SetupTooltip(ItemDetails itemDetails, SlotType slotType)
    {
        nameText.text = itemDetails.itemName;
        typeText.text = itemDetails.itemType.ToString();
        //typeText.text = GetItemType(itemDetails.itemType);

        descriptionText.text = itemDetails.itemDescription;


        //显示下半部分的售卖金额是多少
        if (itemDetails.itemType == ItemType.Seed || itemDetails.itemType == ItemType.Commodity || itemDetails.itemType == ItemType.Furniture)
        {
            bottomPart.SetActive(true);

            var price = itemDetails.itemPrice;
            if (slotType == SlotType.Bag)
            {
                price = (int)(price * itemDetails.sellPercentage);
            }

            valueText.text = price.ToString();
        }
        else
        {
            bottomPart.SetActive(false);
        }

        //这一行代码是为了防止在格子长度发生改变时,渲染较慢的问题
        LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
    }

//    private string GetItemType(ItemType itemType)
//    {
//        return itemType switch
//        {
//            ItemType.Seed => "种子",
//            ItemType.Commodity => "商品",
//            ItemType.Furniture => "家具",
//            ItemType.BreakTool => "工具",
//            ItemType.ChopTool => "工具",
//            ItemType.CollectTool => "工具",
//            ItemType.HoeTool => "工具",
//            ItemType.ReapTool => "工具",
//            ItemType.WaterTool => "工具",
//            _ => "无"
//        };
//}
}

我们在调用itemTooltip里面的东西的时候,我们希望通过其父类进行调用,所以在其父类InventoryUI里需要有一个ItemTooltip的子物体的实例,供showItemTooltip实际调用。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

为格子里的物品SlogBag的预制体中添加ShowItemTooltip的代码:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

为了实现鼠标放上去时有详情显示,退出时也会小时,让它继承两个接口:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

namespace MFarm.Inventory
{

    [RequireComponent(typeof(SlotUI))]
    public class ShowItemTooltip : MonoBehaviour,IPointerEnterHandler,IPointerExitHandler
    {
        private SlotUI slotUI;
        private InventoryUI inventoryUI => GetComponentInParent<InventoryUI>();

        private void Awake()
        {
            slotUI = GetComponent<SlotUI>();
        }

        public void OnPointerEnter(PointerEventData eventData)
        {
            if (slotUI.itemAmount != 0)
            {
                inventoryUI.itemTooltip.gameObject.SetActive(true);
                inventoryUI.itemTooltip.SetupTooltip(slotUI.itemDetails, slotUI.slotType);

                inventoryUI.itemTooltip.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 0);
                inventoryUI.itemTooltip.transform.position = transform.position + Vector3.up * 60;

            }
            else
            {
                inventoryUI.itemTooltip.gameObject.SetActive(false);
            }
        }

        public void OnPointerExit(PointerEventData eventData)
        {
            inventoryUI.itemTooltip.gameObject.SetActive(false);
        }


    }
}

Unity 农场 1 —— 环境搭建、背包系统、时间系统

人物的移动及举起物品

  • 玩家的移动动画

这个板块比较简单,由于手部动作会有所不同,所以将人物分为三个部分:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

三个部分会有不同的动画控制器,对于基础移动只需要继承一个基础控制器即可。

这个基础的控制器包含两个融合树:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

idle的融合树如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

walk的融合树复杂一点,除了在方向上有分支

Unity 农场 1 —— 环境搭建、背包系统、时间系统

对于每个方向还分为走和跑两个的一维融合

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 Unity 农场 1 —— 环境搭建、背包系统、时间系统

相关代码如下:

 Player中:

    private void PlayerInput()
    {
        inputX = Input.GetAxisRaw("Horizontal");
        inputY = Input.GetAxisRaw("Vertical");
        if (inputX != 0 && inputY != 0)
        {
            inputX *= 0.6f;
            inputY *= 0.6f;
        }


        if (Input.GetKey(KeyCode.LeftShift))
        {
            inputX *= 0.5f;
            inputY *= 0.5f;
        }

        movementInput = new Vector2(inputX, inputY);

        isMoving = movementInput != Vector2.zero;
    }



    private void SwitchAnimation()
    {
        foreach(var anim in animators)
        {
            anim.SetBool("isMoving", isMoving); 
            if (isMoving)
            {
                anim.SetFloat("InputX", inputX);
                anim.SetFloat("InputY", inputY);

            }
        }
    }

Unity 农场 1 —— 环境搭建、背包系统、时间系统

实现选中背包物品触发举起动画

首先在player身下创建一个hold item,用于展示举起的物品。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

确保其在人物前面显示,层级设置为2。 图片锚点设置在下方。

我们更改人物动画的方式通过使用animatorOverride Controller来实现的:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

根据不同的状态,然后切换不同的controller即可 

Unity 农场 1 —— 环境搭建、背包系统、时间系统

然后我们用一个类将PartType、PartName和animatorOverriderContoller合在一起,方便调用,作为一个AnimatorType这个类给我们更换控制器的脚本使用

Unity 农场 1 —— 环境搭建、背包系统、时间系统

然后在切换控制器的脚本里声明一个List,在inspector窗口中如下:

此时根据我们选择的PartType和PartName修改不同的控制器:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

添加一个物品被点选后的事件,当物品被点选时触发这个事件:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来是修改动画控制器的脚本,思路是首先获取player的三个子物体(hair、body、arm)三个部分的animator。接下来animator对应的动画控制器思路很简单:

Animator.runtimeAnimatorController = 其他的控制器即可。

另一方面,我们需要设定,不同的部位,在不同的状态下时应该使用什么动画控制器?

这个需要我们在inspector窗口中手动设定:

 Unity 农场 1 —— 环境搭建、背包系统、时间系统

注意到上面给出的不同的部位,是用名字来进行区分的。而我们需要根据名字去获取对应的animator,所以此处使用字典:

于是将其存储起来在字典里,key是名字(即hair),value则是对应的animator。

随后,当我们点击物品时,则会触发点击事件,点击事件会执行绑定对应的函数。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

该函数会执行以下的内容:

如果该物品的类型是种子或者商品,说明是可以举起来的,那么接下来就会把当前状态变更为举起的状态:

    private void OnItemSelectedEvent(ItemDetails itemDetails, bool isSelected)
    {
        //WORKFLOW:不同的工具返回不同的动画在这里补全
        PartType currentType = itemDetails.itemType switch
        {
            ItemType.Seed => PartType.Carry,
            ItemType.Commodity => PartType.Carry,
            _ => PartType.None
        };

当前状态改变完毕时,我们要确保当前的物品仍是选中状态,如果不是则不举起,也就是切换回原来的形态:

        if (isSelected == false)
        {
            currentType = PartType.None;
            holdItem.enabled = false;
        }
        else
        {
            if (currentType == PartType.Carry)
            {
                holdItem.sprite = itemDetails.itemOnWorldSprite;
                holdItem.enabled = true;
            }

最后执行切换状态控制器的函数:

        SwitchAnimator(currentType);
    private void SwitchAnimator(PartType partType)
    {
        foreach (var item in animatorTypes)
        {
            if (item.partType == partType)
            {
                animatorNameDict[item.partName.ToString()].runtimeAnimatorController = item.overrideController;
            }
        }
    }

完整代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AnimatorOverride : MonoBehaviour
{
    private Animator[] animators;

    public SpriteRenderer holdItem;

    [Header("各部分动画列表")]
    public List<AnimatorType> animatorTypes;//这个列表是在inspector窗口中去设置的

    private Dictionary<string, Animator> animatorNameDict = new Dictionary<string, Animator>();

    private void Awake()
    {
        animators = GetComponentsInChildren<Animator>();

        foreach (var anim in animators)
        {
            animatorNameDict.Add(anim.name, anim);//根据在inspector窗口中设定的内容设定字典
        }
    }

    private void OnEnable()
    {
        EventHandler.ItemSelectedEvent += OnItemSelectedEvent;
    }

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

    private void OnItemSelectedEvent(ItemDetails itemDetails, bool isSelected)
    {
        //WORKFLOW:不同的工具返回不同的动画在这里补全
        PartType currentType = itemDetails.itemType switch
        {
            ItemType.Seed => PartType.Carry,
            ItemType.Commodity => PartType.Carry,
            _ => PartType.None
        };

        if (isSelected == false)
        {
            currentType = PartType.None;
            holdItem.enabled = false;
        }
        else
        {
            if (currentType == PartType.Carry)
            {
                holdItem.sprite = itemDetails.itemOnWorldSprite;
                holdItem.enabled = true;
            }
        }

        SwitchAnimator(currentType);
    }


    private void SwitchAnimator(PartType partType)
    {
        foreach (var item in animatorTypes)
        {
            if (item.partType == partType)
            {
                animatorNameDict[item.partName.ToString()].runtimeAnimatorController = item.overrideController;
            }
        }
    }
}

实际效果:

Unity 农场 1 —— 环境搭建、背包系统、时间系统


 

构建游戏的时间系统

时间流逝

代码思路很简单,通过Time.deltaTime让时间进行流逝,然后进行秒++,如果秒到59则进位到分钟,再到时、天、月、季、年

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    private int gameSecond, gameMinute, gameHour, gameDay, gameMonth, gameYear;
    private Season gameSeason = Season.春天;
    private int monthInSeason = 3;

    public bool gameClockPause;
    private float tikTime;

    private void Awake()
    {
        NewGameTime();
    }

    private void Update()
    {
        if (!gameClockPause)
        {
            tikTime += Time.deltaTime;

            if (tikTime >= Settings.secondThreshold)
            {
                tikTime -= Settings.secondThreshold;
                UpdateGameTime();
            }
        }
    }

    private void NewGameTime()
    {
        gameSecond = 0;
        gameMinute = 0;
        gameHour = 7;
        gameDay = 1;
        gameMonth = 1;
        gameYear = 2022;
        gameSeason = Season.春天;
    }

    private void UpdateGameTime()
    {
        gameSecond++;
        if (gameSecond > Settings.secondHold)
        {
            gameMinute++;
            gameSecond = 0;

            if (gameMinute > Settings.minuteHold)
            {
                gameHour++;
                gameMinute = 0;

                if (gameHour > Settings.hourHold)
                {
                    gameDay++;
                    gameHour = 0;

                    if (gameDay > Settings.dayHold)
                    {
                        gameDay = 1;
                        gameMonth++;

                        if (gameMonth > 12)
                            gameMonth = 1;

                        monthInSeason--;
                        if (monthInSeason == 0)
                        {
                            monthInSeason = 3;

                            int seasonNumber = (int)gameSeason;
                            seasonNumber++;

                            if (seasonNumber > Settings.seasonHold)
                            {
                                seasonNumber = 0;
                                gameYear++;
                            }

                            gameSeason = (Season)seasonNumber;

                            if (gameYear > 9999)
                            {
                                gameYear = 2022;
                            }
                        }
                    }
                }
            }
        }

        // Debug.Log("Second: " + gameSecond + " Minute: " + gameMinute);
    }
}

时间UI及对应的时间变更

创建时间相关的UI组件,并设定好布局

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

当时间进行切换时,用事件的形式去通知

Unity 农场 1 —— 环境搭建、背包系统、时间系统

思路其实很简单,时间流逝->进位->触发事件,事件根据时间修改UI展示内容即可:

代码如下:

TimeManager:用于管理时间的各个单位,让时间流逝,并触发事件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    private int gameSecond, gameMinute, gameHour, gameDay, gameMonth, gameYear;
    private Season gameSeason = Season.春天;
    private int monthInSeason = 3;

    public bool gameClockPause;
    private float tikTime;

    private void Awake()
    {
        NewGameTime();
    }


    private void Start()
    {
        EventHandler.CallGameDateEvent(gameHour, gameDay, gameMonth, gameYear, gameSeason);
        EventHandler.CallGameMinuteEvent(gameMinute, gameHour);

    }
    private void Update()
    {
        if (!gameClockPause)
        {
            tikTime += Time.deltaTime;

            if (tikTime >= Settings.secondThreshold)
            {
                tikTime -= Settings.secondThreshold;
                UpdateGameTime();
            }
        }

        if (Input.GetKey(KeyCode.T))
        {
            for(int i = 0; i < 60; i++)
            {
                UpdateGameTime();
            }
        }
    }

    private void NewGameTime()
    {
        gameSecond = 0;
        gameMinute = 0;
        gameHour = 7;
        gameDay = 1;
        gameMonth = 1;
        gameYear = 2022;
        gameSeason = Season.春天;
    }

    private void UpdateGameTime()
    {
        gameSecond++;
        if (gameSecond > Settings.secondHold)
        {
            gameMinute++;
            gameSecond = 0;

            if (gameMinute > Settings.minuteHold)
            {
                gameHour++;
                gameMinute = 0;

                if (gameHour > Settings.hourHold)
                {
                    gameDay++;
                    gameHour = 0;

                    if (gameDay > Settings.dayHold)
                    {
                        gameDay = 1;
                        gameMonth++;

                        if (gameMonth > 12)
                            gameMonth = 1;

                        monthInSeason--;
                        if (monthInSeason == 0)
                        {
                            monthInSeason = 3;

                            int seasonNumber = (int)gameSeason;
                            seasonNumber++;

                            if (seasonNumber > Settings.seasonHold)
                            {
                                seasonNumber = 0;
                                gameYear++;
                            }

                            gameSeason = (Season)seasonNumber;

                            if (gameYear > 9999)
                            {
                                gameYear = 2022;
                            }
                        }
                    }
                }
                EventHandler.CallGameDateEvent(gameHour, gameDay, gameMonth, gameYear, gameSeason);

            }
            EventHandler.CallGameMinuteEvent(gameMinute, gameHour);
        }

        // Debug.Log("Second: " + gameSecond + " Minute: " + gameMinute);
    }
}

TimeUI:让UI组件和代码进行绑定,实现一个注册到时间改变事件的函数,当触发事件时,相应的去改变UI上显示的信息即可。

using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class TimeUI : MonoBehaviour
{
    public RectTransform dayNightImage;
    public RectTransform clockParent;
    public Image seasonImage;
    public TextMeshProUGUI dateText;
    public TextMeshProUGUI timeText;

    public Sprite[] seasonSprites;

    private List<GameObject> clockBlocks = new List<GameObject>();

    private void Awake()
    {
        for (int i = 0; i < clockParent.childCount; i++)
        {
            clockBlocks.Add(clockParent.GetChild(i).gameObject);
            clockParent.GetChild(i).gameObject.SetActive(false);
        }
    }

    private void OnEnable()
    {
        EventHandler.GameMinuteEvent += OnGameMinuteEvent;
        EventHandler.GameDateEvent += OnGameDateEvent;
    }

    private void OnDisable()
    {
        EventHandler.GameMinuteEvent -= OnGameMinuteEvent;
        EventHandler.GameDateEvent -= OnGameDateEvent;
    }
    private void OnGameMinuteEvent(int minute, int hour)
    {
        timeText.text = hour.ToString("00") + ":" + minute.ToString("00");
    }

    private void OnGameDateEvent(int hour, int day, int month, int year, Season season)
    {
        dateText.text = year + "年" + month.ToString("00") + "月" + day.ToString("00") + "日";
        seasonImage.sprite = seasonSprites[(int)season];

        SwitchHourImage(hour);//让六个格子亮起来,每4个小时亮一个格子
        DayNightImageRotate(hour);//旋转图片
    }

    /// <summary>
    /// 根据小时切换时间块显示
    /// </summary>
    /// <param name="hour"></param>
    private void SwitchHourImage(int hour)
    {
        int index = hour / 4;

        if (index == 0)
        {
            foreach (var item in clockBlocks)
            {
                item.SetActive(false);
            }
        }
        else
        {
            for (int i = 0; i < clockBlocks.Count; i++)
            {
                if (i < index)
                    clockBlocks[i].SetActive(true);
                else
                    clockBlocks[i].SetActive(false);
            }
        }
    }

    private void DayNightImageRotate(int hour)
    {
        var target = new Vector3(0, 0, hour * 15 - 90);
        dayNightImage.DORotate(target, 1f, RotateMode.Fast);
    }
}

效果如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

场景间切换

绘制一个新的第二场景,注意添加碰撞层

Unity 农场 1 —— 环境搭建、背包系统、时间系统

场景切换

思路很简单,卸载旧的场景,加载新的场景即可。场景切换在TransitionManager使用注册到事件的函数实现。需要设定一个出门点。当触碰到出门点时则触发Ontrigger,在Ontrigger里面调用事件即可。

代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace MFarm.Transition
{
    public class TransitionManager : MonoBehaviour
    {
        public string startSceneName = string.Empty;

        private void OnEnable()
        {
            EventHandler.TransitionEvent += OnTransitionEvent;
        }

        private void OnDisable()
        {
            EventHandler.TransitionEvent -= OnTransitionEvent;
        }



        private void Start()
        {
            StartCoroutine(LoadSceneSetActive(startSceneName));
        }


        private void OnTransitionEvent(string sceneToGo, Vector3 positionToGo)
        {
            StartCoroutine(Transition(sceneToGo, positionToGo));
        }

        /// <summary>
        /// 场景切换,卸载旧的场景,并执行加载新场景的协程
        /// </summary>
        /// <param name="sceneName">目标场景</param>
        /// <param name="targetPosition">目标位置</param>
        /// <returns></returns>
        private IEnumerator Transition(string sceneName, Vector3 targetPosition)
        {
            yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene());

            yield return LoadSceneSetActive(sceneName);
        }

        /// <summary>
        /// 加载场景并设置为激活
        /// </summary>
        /// <param name="sceneName">场景名</param>
        /// <returns></returns>
        private IEnumerator LoadSceneSetActive(string sceneName)
        {
            //使用异步加载的方式,加载模式使用的是在原有的场景中叠加一个新的场景
            yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);

            Scene newScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);

            SceneManager.SetActiveScene(newScene);
        }
    }
}

设定的出门点的代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Teleport : MonoBehaviour
{
    public string sceneToGo;
    public Vector3 positionToGo;
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            EventHandler.CallTransitionEvent(sceneToGo, positionToGo);
        }
    }
}

这里别忘了添加事件:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

人物跨场景移动以及场景加载前后事件

场景切换时,一些在之前从场景通过Start获取的物体会因为场景切换而丢失目标。 

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

为了防止场景加载时人物移动,添加一个变量

Unity 农场 1 —— 环境搭建、背包系统、时间系统

为了使得场景切换时人物不能移动,添加事件控制,卸载场景时使其不能动

Unity 农场 1 —— 环境搭建、背包系统、时间系统

移动位置也很简单:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

然后进行一些类似上面的操作,在原来需要在Start里面获取的一些物品或者执行的函数,放到OnEnable和OnDisable中通过注册事件的函数来实现,保证不会因场景切换而出现

NullReferenceException: Object reference not set to an instance of an object

这样的bug。

效果如下:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

Unity 农场 1 —— 环境搭建、背包系统、时间系统

 在TransitionManager中添加Fade的协程:

        /// <summary>
        /// 淡入淡出场景
        /// </summary>
        /// <param name="targetAlpha">1是黑,0是透明</param>
        /// <returns></returns>
        private IEnumerator Fade(float targetAlpha)
        {
            isFade = true;

            fadeCanvasGroup.blocksRaycasts = true;//这个时候点击无效

            float speed = Mathf.Abs(fadeCanvasGroup.alpha - targetAlpha) / Settings.fadeDuration;

            while (!Mathf.Approximately(fadeCanvasGroup.alpha, targetAlpha))
            {
                fadeCanvasGroup.alpha = Mathf.MoveTowards(fadeCanvasGroup.alpha, targetAlpha, speed * Time.deltaTime);
                yield return null;//只要不相等,就会一直执行该协程
            }

            fadeCanvasGroup.blocksRaycasts = false;

            isFade = false;
        }

加载场景时调用:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

效果如下:

 Unity 农场 1 —— 环境搭建、背包系统、时间系统

为了存储场景中的物品信息,使得在场景切换时场景中的物品不是过去的信息(比如防止拾取物品后再回来又有重新物品的情况)

我们此处使用一个类来存储场景中的物品信息。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

使用一个字典列表来存储场景中的所有物品,key是string类型的,是记录场景的名字,而value就是这个场景中的所有物品组成的列表。:

Unity 农场 1 —— 环境搭建、背包系统、时间系统

接下来我们希望在每次读取场景前,都先扫描当前场景有哪些物品,那么这个实现的方法就是在卸载场景后,我们保存信息。这样下次读取场景时就有对应的列表可以更新了。如果当前场景是新场景,则将新场景和新场景有的物品添加到列表中。

        /// <summary>

        /// 获得当前场景所有Item

        /// </summary>

        private void GetAllSceneItems()

        {

            List<SceneItem> currentSceneItems = new List<SceneItem>();

            foreach (var item in FindObjectsOfType<Item>())

            {

                SceneItem sceneItem = new SceneItem

                {

                    itemID = item.itemID,

                    position = new SerializableVector3(item.transform.position)

                };

                currentSceneItems.Add(sceneItem);

            }

            //首先上面先要获取当前场景中的物品

            if (sceneItemDict.ContainsKey(SceneManager.GetActiveScene().name))

            {

                //找到数据就更新item数据列表

                sceneItemDict[SceneManager.GetActiveScene().name] = currentSceneItems;

            }

            else    //如果是新场景

            {

                sceneItemDict.Add(SceneManager.GetActiveScene().name, currentSceneItems);

            }

        }

调用时机则是在场景卸载前:

        private void OnBeforeSceneUnloadEvent()

        {

            GetAllSceneItems();

        }

在场景加载完毕后,我们则更新当前场景

        /// <summary>

        /// 刷新重建当前场景物品

        /// </summary>

        private void RecreateAllItems()

        {

            List<SceneItem> currentSceneItems = new List<SceneItem>();

            if (sceneItemDict.TryGetValue(SceneManager.GetActiveScene().name, out currentSceneItems))

            {

                if (currentSceneItems != null)

                {

                    //清场

                    foreach (var item in FindObjectsOfType<Item>())

                    {

                        Destroy(item.gameObject);

                    }

                    foreach (var item in currentSceneItems)

                    {

                        Item newItem = Instantiate(itemPrefab, item.position.ToVector3(), Quaternion.identity, itemParent);

                        newItem.Init(item.itemID);

                    }

                }

            }

        }

这样即可实现保存场景中的物品效果。

设置鼠标指针根据物品调整

如果在此处使用unity自带的canvas,则当前版本无法修改它的大小。如果希望cursor有额外的效果,放大、渐变的效果,换颜色的话,不太容易。

Unity 农场 1 —— 环境搭建、背包系统、时间系统

所以此处使用image来跟随鼠标的位置实现cursor的效果。

思路也很简单,先对图片设置一个跟随,然后根据当前是什么物品然后切换不同的指针图片,并且在选中物品和取消选中时改变。然后还需要注意如果当前鼠标指向的地方是UI界面,则让其恢复为默认状态。

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 OnEnable()

    {

        EventHandler.ItemSelectedEvent += OnItemSelectedEvent;

    }

    private void OnDisable()

    {

        EventHandler.ItemSelectedEvent -= OnItemSelectedEvent;

    }

    private void Start()

    {

        cursorCanvas = GameObject.FindGameObjectWithTag("CursorCanvas").GetComponent<RectTransform>();

        cursorImage = cursorCanvas.GetChild(0).GetComponent<Image>();

        currentSprite = normal;

        SetCursorImage(normal);

    }

    private void Update()

    {

        if (cursorCanvas == null) return;

        cursorImage.transform.position = Input.mousePosition;

        if (!InteractWithUI())

        {

            SetCursorImage(currentSprite);

        }

        else

        {

            SetCursorImage(normal);//如果不是和UI有互动,则设置为普通的图片

        }

    }

    /// <summary>

    /// 设置鼠标图片

    /// </summary>

    /// <param name="sprite"></param>

    private void SetCursorImage(Sprite sprite)

    {

        cursorImage.sprite = sprite;

        cursorImage.color = new Color(1, 1, 1, 1);

    }

    /// <summary>

    /// 物品选择事件函数,根据选中不同类型的物品选择不同的类型图片

    /// </summary>

    /// <param name="itemDetails"></param>

    /// <param name="isSelected"></param>

    private void OnItemSelectedEvent(ItemDetails itemDetails, bool isSelected)

    {

        if (!isSelected)

        {

            currentSprite = normal;//没被选中则切换为普通的图片

        }

        else    //物品被选中才切换图片

        {

            //WORKFLOW:添加所有类型对应图片

            currentSprite = itemDetails.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,

            };

        }

    }

    /// <summary>

    /// 是否与UI互动

    /// </summary>

    /// <returns></returns>

    private bool InteractWithUI()

    {

        if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())

        {

            return true;

        }

        return false;

    }

}文章来源地址https://www.toymoban.com/news/detail-476880.html

到了这里,关于Unity 农场 1 —— 环境搭建、背包系统、时间系统的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity背包系统与存档(附下载链接)

    下载地址: https://download.csdn.net/download/qq_58804985/88184776 视频演示: 功能: 拖动物品在背包中自由移动,当物品拖动到其他物品上时,和其交换位置.基于EPPlus的背包数据与位置保存 原理: 给定一个道具池表格与一个背包表格 道具池表格负责存储所有道具的信息 背包表格负责存储将玩

    2024年02月13日
    浏览(40)
  • Unity之背包系统(轻松储存10万条数据)

    @作者 : SYFStrive @博客首页 : HomePage 📌: 个人社区(欢迎大佬们加入) 👉: 社区链接🔗 📌: 觉得文章不错可以点点关注 👉: 专栏连接🔗 💃: 程序员每天坚持锻炼💪 🔗: 点击直接阅读文章 👉 Unity算法相关文章 (🔥) 博主累了,休息一会💤(可以先看看代码原理

    2024年02月11日
    浏览(31)
  • Maven环境搭建及Maven部分目录分析

    Maven 本身就是⼀套由 Java 开发的软件,所以 Maven 的运⾏需要依赖 JDK 环境。在安装 Maven 之前请 确认JDK 是否配置正确(主要依赖 JAVA_HOME 环境变量)。如果没有正确安装和配置 JDK ,则运⾏ Maven 时 会出现以下错误信息:          The JAVA_HOME environment variable is not defined correct

    2024年01月20日
    浏览(36)
  • Unity3D实现背包系统、物品的拖拽、拾取物品功能

    要在Unity中实现背包系统,你可以创建一个脚本来管理库存和物品。 首先,在Unity中创建一个名为“InventoryManager”的C#脚本。在这个脚本中,你可以创建一个将存储在背包中的物品列表。

    2024年02月16日
    浏览(44)
  • 农场管理小程序|基于微信小程序的农场管理系统设计与实现(源码+数据库+文档)

    农场管理小程序目录 目录 基于微信小程序的农场管理系统设计与实现 一、前言 二、系统设计 三、系统功能设计  1、用户信息管理 2、农场信息管理 3、公告信息管理 4、论坛信息管理 四、数据库设计  五、核心代码   七、最新计算机毕设选题推荐 八、源码获取: 博主介

    2024年03月11日
    浏览(65)
  • java+ssm+mysql农场信息管理系统

    项目介绍: 本系统为基于jsp+ssm+mysql的农场信息管理系统,功能如下: 用户:注册登录系统,菜地信息管理,农作物信息管理,种植信息管理,客户信息管理,商家信息管理,任务信息管理; 商品信息管理,订单信息管理,用户信息管理,系统操作日志,个人信息。 包含资

    2024年02月10日
    浏览(52)
  • 【Socket】Unix环境下搭建简易本地时间获取服务

    本文搭建一个Unix环境下的、局域网内的、简易的本地时间获取服务。 主要用于验证: 当TCP连接成功后,可以在两个线程中分别进行读操作、写操作动作 当客户端自行终止连接后,服务端会在写操作时收到 SIGPIPE 信号 当客户端执行shutdown写操作后,客户端会在写操作时收到

    2024年02月04日
    浏览(34)
  • Unity AR开发环境搭建

    在上一篇文章中,我定义了各种类型的扩展现实 (XR)。 在其中,我将增强现实 (AR) 定义为:增强现实 (AR) 将数字对象置于物理世界中。 通常,该设备将配备某种类型的相机(例如智能手机),可以实时提供叠加在其上的数字对象。 AR 通常仅使用 UI 元素来允许您与数字对象进

    2024年04月22日
    浏览(58)
  • Unity 时间定时调度系统

    之前的文章也有写过时间调度系统,但是没有支持异步调度只有回调调度,而且效率和代码可读性不是很好,下面介绍一种更优质的时间调度系统 首先需要定义一个时间行为,每次延时后需要干什么,延迟的时间类型是什么都需要使用TimerAction 这个调度器是个单例,单例在此

    2024年02月03日
    浏览(46)
  • 内网环境的NTP服务搭建和应用(实现各服务器时间同步)

    NTP,“网络时间协议”(Network Time Protocol),它是一种用于在网络中同步各个设备时钟的协议。NTP通过在网络中的一组时间服务器之间传递时间信息来实现时间同步,从而确保网络中的各个设备具有相似的时间。 在内网环境中想要保持各个服务器时间一致,就需要搭建NTP服务

    2024年02月03日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包