种田游戏的综合尝试

游戏角色

详细教程

一、准备工作

1、场景重命名为Farm

2、导入资源

(1) 新建文件夹。Import Asset

(2) 导入:人物、走、跑、休息

3、设置摄像机

二、制作角色预制体

1、增加角色,命名为Player,设置材质、人类角色

2、设置角色的空闲、走、跑的材质等

3、添加Rigidbody组件,约束Position上的Y轴AZ,Rotation上的X、Z轴

4、设置Update Mode

5、新建Prefabs文件夹,将角色制成预制体

三、设置角色动画

1、新建文件夹Animation,在Animation文件夹下新建文件夹Controllers

2、在Controller文件夹下Create Animator Controller,命名为Player

3、选中Hierarchy中的人物,在Inspector面板中给Animator中的Controller选择Player

4、打开Controller文件夹下的Player,设置动画

(1) 复制mixano.com,分别重命名为Walking、Running和Idle

(2) 将复制的Walking、Running和Idle转移到Animation文件夹

(3) 分别选择Walking、Running和Idle,设置循环播放(Loop Pose也勾选上)

(4) 打开Controller文件夹下的Player,依次将Idle(默认)、Walking、Running拖入新界面

(5) 选择Parameters,添加Bool,Bool,分别命名为IsWalking,Running。不勾选(默认状态)

(6) 选中Idle到Walk的箭头,右侧点加号,出现IsWalking true,取消勾选Hash Exit Time

(7) 同样的方法设定Walking转换为Idle。将IsWalking设置为false。取消勾选Hash Exit Time

(8) 选中动画编辑器中的Idle、Walking、Running,将Speed改为2

(9) 同样的方法设置Running

四、控制角色行走

1、给角色预制体,添加Character Controller(角色控制器)组件,调节参数至笼罩全身

2、给角色预制体添加PlayerController.cs组件

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

public class PlayerController : MonoBehaviour
{
    public float turnSpeed = 20f;
    Animator animator;
    Rigidbody rb;
    Vector3 movement;
    Quaternion rotation = Quaternion.identity;
    [Header("Movement System")]
    public float walkSpeed = 1f;
    public float runSpeed = 3f;

    private void Start()
    {
        animator = GetComponent<Animator>();
        rb = GetComponent<Rigidbody>();
    }
    private void FixedUpdate()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        movement.Set(horizontal, 0f, vertical);

        bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0);
        bool hasVerticalInput = !Mathf.Approximately(vertical, 0);

        float movementSpeed = walkSpeed;

        movement.Normalize();
        Vector3 desiredForward = Vector3.RotateTowards(transform.forward, movement, turnSpeed * Time.deltaTime, 0f);
        rotation = Quaternion.LookRotation(desiredForward);

        rb.MovePosition(rb.position + movement * movementSpeed * Time.deltaTime);
    }
    private void OnAnimatorMove()
    {
        animator.SetBool("IsWalking", movement.magnitude > 0);
        rb.MoveRotation(rotation);
    }
}

3、增加奔跑

(1) 添加加速键Sprint(left shift            right shift)

(2) 回到PlayerController.cs,设置加速和奔跑

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

public class PlayerController : MonoBehaviour
{
    public float turnSpeed = 20f;
    Animator animator;
    Rigidbody rb;
    Vector3 movement;
    Quaternion rotation = Quaternion.identity;
    [Header("Movement System")]
    public float walkSpeed = 1.5f;
    public float runSpeed = 3f;

    private void Start()
    {
        animator = GetComponent<Animator>();
        rb = GetComponent<Rigidbody>();
    }
    private void FixedUpdate()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        movement.Set(horizontal, 0f, vertical);

        bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0);
        bool hasVerticalInput = !Mathf.Approximately(vertical, 0);

        float movementSpeed = walkSpeed;
        if (Input.GetButton("Sprint") && (hasHorizontalInput || hasVerticalInput))
        {
            movementSpeed = runSpeed;
        }

        movement.Normalize();
        Vector3 desiredForward = Vector3.RotateTowards(transform.forward, movement, turnSpeed * Time.deltaTime, 0f);
        rotation = Quaternion.LookRotation(desiredForward);

        rb.MovePosition(rb.position + movement * movementSpeed * Time.deltaTime);
    }
    private void OnAnimatorMove()
    {
        animator.SetBool("IsWalking", movement.magnitude > 0);
        animator.SetBool("IsRunning", Input.GetButton("Sprint") && (movement.magnitude > 0));
        rb.MoveRotation(rotation);
    }
}

(3) AI提供的优化

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float turnSpeed = 20f;
    public Animator animator;
    public Rigidbody rb;
    public Vector3 movement;
    public Quaternion rotation = Quaternion.identity;
    private float speed;
    private bool isInputUpdated;

    [Header("Movement System")]
    public float walkSpeed = 6f;
    public float runSpeed = 12f;

    void Start()
    {
        animator = GetComponent<Animator>();
        rb = GetComponent<Rigidbody>();
    }

    private void FixedUpdate()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        movement.Set(horizontal, 0f, vertical);
        movement.Normalize();

        bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0f);
        bool hasVerticalInput = !Mathf.Approximately(vertical, 0f);

        if (Input.GetButton("Sprint"))
        {
            speed = runSpeed;
            animator.SetBool("IsRunning", true);
        }
        else if (hasHorizontalInput || hasVerticalInput)
        {
            speed = walkSpeed;
            animator.SetBool("IsWalking", true);
        }
        else
        {
            speed = 0f;
            animator.SetBool("IsWalking", false);
            animator.SetBool("IsRunning", false);
        }

        Vector3 desireForward = Vector3.RotateTowards(transform.forward, movement, turnSpeed * Time.fixedDeltaTime, 0f);
        rotation = Quaternion.LookRotation(desireForward);

        isInputUpdated = true;
    }

    private void OnAnimatorMove()
    {
        if (isInputUpdated)
        {
            Vector3 movementSpeed = movement * speed;
            rb.MovePosition(rb.position + movementSpeed * Time.fixedDeltaTime);
            rb.MoveRotation(rotation);

            isInputUpdated = false; // Reset the flag after applying the input
        }
    }
}
五、相机跟随

1、调整相机的Transform

2、给Main Camera增加CameraController.cs组件 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class CameraController : MonoBehaviour
{
    public float offsetz = 6f;
    public float smoothing = 8f;
    Transform playerPos;
    void Start()
    {
        playerPos = FindObjectOfType<PlayerController>().transform;
    }
 
    void Update()
    {
        FollowPlayer();
    }
    void FollowPlayer()
    {
        Vector3 targetPos = new Vector3(playerPos.position.x, 
transform.position.y, playerPos.position.z-offsetz);
        transform.position = Vector3.Lerp(transform.position, targetPos, smoothing*Time.deltaTime);
    }
}

农田处理

详细教程

一、准备工作

1、导入资源:在Import Asset文件夹下新建文件夹Farmland

(1) 新建文件夹Dirt,移入aerial_ground相关文件

(2) 新建文件夹Tilled land,移入 brown_mud_dry相关文件

(3) 新建文件夹Watered land,移入brown_mud相关文件

2、设置一块耕地 Land

3、制备预制体 Land(标签Land)

4、在Scripts文件夹下新建Farming 文件夹,其下新建Land.cs

二、制作农田

1、设置三种状态的耕地

(1) 打开Land预制体,给它添加Land.cs组件,使不同类型的土地渲染不同的材质,赋值

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

public class Land : MonoBehaviour
{
    public enum LandStatus { Soil, Farmland, Watered }
    public LandStatus landStatus;
    public Material soilMat, farmlandMat, wateredMat;
    new Renderer renderer;

    void Start()
    {
        renderer = GetComponent<Renderer>();
        SwitchLandStatus(LandStatus.Soil);
    }
    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        landStatus = statusToSwitch;
        Material materialToSwitch = soilMat;
        switch (statusToSwitch)
        {
            case LandStatus.Soil: materialToSwitch = soilMat; break;
            case LandStatus.Farmland: materialToSwitch = farmlandMat; break;
            case LandStatus.Watered: materialToSwitch = wateredMat; break;
        }
        renderer.material = materialToSwitch;
    }
}

2、设置选中的土地块的形态

(1) 以Land为父物体,3D Object-Cube,命名Select,设置transform,移除Collider组件。

(2) 给Select制作并导入贴图,放在UI文件夹(新建)

(3) 打开Land预制体,选中Select,将贴图拖放到Select上,UI文件夹中出现新的文件夹Materials

(4) 打开文件夹Materials,选中新生成的材质球,将Rendering Mode更改为Transparent,更改Albedo的颜色,Smoothness设置为1

(5) 隐藏Select或在Land.cs的Start方法中添加select.SetActive(false);或Select(false);

3、回到Unity,复制这个Land,平铺这些田地,并转入FarmingArea空物体中

4、创建陆地:3D Object-Quad

三、人与耕地的交互

1、创建和设置检测射线

(1) 打开Player预制体,以Player为父物体,Create Empty,命名为 Interactor(发出射线方)

(2) 给Interactor添加PlayerInteraction.cs组件,设置检测射线

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

public class PlayerInteraction : MonoBehaviour
{
    PlayerController playerController;
    void Start()
    {
        playerController = transform.parent.GetComponent<PlayerController>();
    }

    void Update()
    {
        RaycastHit hit;
        if(Physics.Raycast(transform.position, Vector3.down, out hit, 1))
        {
            OnInteractableHit(hit);
        }
    }
    void OnInteractableHit(RaycastHit hit)
    {
        Collider other = hit.collider;
        Debug.Log(other.name);
    }
}

2、设置选中耕地时,出现选中状态

(1) 编辑Land.cs,增加是否被选中的方法

public GameObject select;

void Start()
{
    renderer = GetComponent<Renderer>();
    SwitchLandStatus(LandStatus.Soil);
    Select(false);
}

public void Select(bool toggle)
{
    select.SetActive(toggle);
}

(2) 赋值

(3) 编辑 PlayerInteraction.cs,区分农田和其他土地、设置选中框的显示

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

public class PlayerInteraction : MonoBehaviour
{
    PlayerController playerController;
    Land selectLand = null;
    void Start()
    {
        playerController = transform.parent.GetComponent<PlayerController>();
    }

    void Update()
    {
        RaycastHit hit;
        if (Physics.Raycast(transform.position, Vector3.down, out hit, 1))
        {
            OnInteractableHit(hit);
        }
    }
    void OnInteractableHit(RaycastHit hit)
    {
        Collider other = hit.collider;
        if (other != null && other.CompareTag("Land"))
        {
            Land land = other.GetComponent<Land>();
            if (land != null)
            {
                SelectLand(land); return;
            }
            else { Debug.Log("未选中任何田地"); }
        }
        if (selectLand != null)
        {
            DeselectLand(selectLand);
        }
    }
    void SelectLand(Land land)
    {
        if (land == null) { Debug.Log("你未选择田地"); return; }
        if (selectLand != null) { selectLand.Select(false); }
        selectLand = land;
        land.Select(true);
    }
    void DeselectLand(Land land)
    {
        if(land == null) { Debug.Log("你没有选择田地");return; }
        land.Select(false);
        selectLand = null;
    }
}

3、建立交互系统 

(1) 编辑 Land.cs,添加 Interact 方法

public void Interact()
{
   SwitchLandStatus(LandStatus.Farmland);
}

(2) 编辑 PlayerInteraction.cs,添加 Interact 方法,调用Land中的Inseract方法

public void Interact()
{
    if(selectLand != null)
    {
        selectLand.Interact();return;
    }
    Debug.Log("未站在田地上");
}

(3) 编辑 PlayerController.cs,添加 Interact 方法:当点击鼠标或按下左Ctrl键时,调用PlayerInteraction.cs中的Interact方法

PlayerInteraction playerInteraction;
void Start()
{
    animator = GetComponent<Animator>();
    rb = GetComponent<Rigidbody>();
    playerInteraction = GetComponentInChildren<PlayerInteraction>();
}
public void Update()
{
    Interact();
}
public void Interact()
{
    if (Input.GetButtonDown("Fire1"))
    {
        playerInteraction.Interact();
    }
}

农具管理 

详细教程 标题四

水壶 标题一

一、准备工作

1、点击Windows- Package Manager,安装2D Sprite(标题四)

2、导入农具资源到UI 文件夹

3、拆分蔬菜和工具

4、在Scripts下创Inventory文件夹,在该文件夹下新建ItemData.cs

5、在Assets文件下创建Data文件夹,在Data下创建两个文件夹:Items和Tools

6、在Tools文件夹新建文件夹Seeds

二、准备农具模型

1、模型下载

官网资源

模型下载

模型下载

(1) Watering Can  https://www.cgtrader.com/free-3d-models/household/other/watering-can-c76fe658-8352-4ed8-8d08-b2bc0b0af9e6

(2) hoe   https://www.cgtrader.com/items/4145952/download-page

(3) axe  https://www.cgtrader.com/free-3d-models/industrial/tool/axe-lowpoly-pbr-simple

(4) pickaxe https://www.cgtrader.com/free-3d-models/various/various-models/pickaxe-5903c86c-f84f-44bd-af34-8f7ce07666b9

(5) shovel  https://www.cgtrader.com/free-3d-models/household/household-tools/shovel-b4035fae-6192-4afe-a238-1c4ec2877a5f

2、改变轴心点的简易方法:

使用Empty GameObject作为容器:在Unity中创建一个空的GameObject,将预制体作为其子对象。然后,在父对象的Transform组件中调整其Position属性,将其移动到期望的位置。父对象的轴心点即是预制体的轴心点

3、导入模型资源

(1) 注意:下载后在各自的文件夹中将需要导入的文件夹名改为Texture

(2) 在Import Asset 文件夹分别新建Tools和Items文件夹

(3) 在Tools文件夹中分别创建WarteringCan,Axe,Pickaxe,Shovel,Hoe文件夹

(4) 导入相应农具(将含有下列资源的Textures文件夹直接拖入相应文件夹)

4、制作模型预制体

(1) Create Empty,命名工具

(2) 以步骤(1) 的物体为父物体,添加子物体农具

(3) 制作预制体

三、创建农具的类(ItemData对象)

1、编辑 ItemData.cs,设置所有农具都具备的节点

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

[CreateAssetMenu(menuName = "Items/Item")]
public class ItemData : ScriptableObject
{
    public string description;
    public Sprite thumbnail;
    public GameObject gameModel;
}

2、创建ItemData对象——卷心菜

(1) 在Items文件夹下Create-Item,命名为Cabbage

(2) 选中Cabbage,设置description为A leafy Green vegetable

3、创建工具

(1) 打开Scripts下的Inventory文件夹,新建EquipmentData.cs

using System.Collections;
using System.Collections.Generic;
using UnityEditor.Search;
using UnityEngine;

[CreateAssetMenu(menuName = ("Items/Equipment"))]
public class EquipmentData : ItemData
{
    public enum ToolType { Hoe, WateringCan, Axe, Pickaxt, Shovel}
    public ToolType Type;
}

(2) Tools文件夹Create-Items-Equipment,命名为Hoe,

(3) 同样的方法创建WarteringCan,Axe,Pickaxe,Shovel

(4) 设置它们的description

Axe:For cleaning wood obstacles

Hoe:For tilling the land

Pickaxe:For cleaning rock obstacles

Watering Can:For watering the plants to make them grow

Shovel:Clear plant material

(4) 设置它们的thumbnail:Axe:12;Hoe:13;Pickaxe:11;Shovel:10

4、设置ItemData的Game Model

5、创建种子

(1) 在Scripts下的Inventory文件夹下创建SeedData.cs

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

[CreateAssetMenu(menuName =("Items/Seed"))]

public class SeedData : ItemData
{
    public int daysToGrow;
    public ItemData cropToYield;
}

(2) Seed文件夹下创建Seed文件,命名为Cabbage Seed。description为Seeds to grow cabbages 

(3) 设置Cabbage Seed的各节点

UI面板

 

详细教程 标题六

一、准备工作

1、Create Empty,命名为Manager

(1) 打开Scripts文件夹,新建UI文件夹

(2) 在UI文件夹中,添加InventoryManager.cs和UIManager.cs

(3) 给 Manager 添加UIManager.cs组件

2、新建Canvas:以Manager为父物体,UI-Canvas,设置UI Scale Mode和Reference Resolution

3、创建背包按钮:以Canvas为父物体,创建按钮,命名为InventoryButton,大小160,144

4、以Canvas为父物体,Create Empty。命名为InventoryPanel。Alt+Strech

5、工具栏

(1) 工具栏背景:以InventoryPanel为父物体,UI-Image,命名为ToolsPanel

(2) 工具栏名称:以ToolsPanel为父物体,UI-Text,命名为Header

(3) 选中的工具槽:以ToolsPanel为父物体,UI-Image,命名为HandSlot。

(4) 工具槽预制体:

① 工具槽区域:

以ToolsPanel为父物体,Create Empty。命名为InventorySlots

添加Grid Layout Group组件

② 工具槽背景:

以InventorySlots为父物体,UI-Image

复制得到 8 个工具槽。选中InventorySlots,设置Grid Layout Group,使工具槽均匀分列两行

③ 工具槽预制体

删除复制出的Image,保留的Image重命名为InventorySlot,设置颜色AF8E60,

打开Prefabs文件夹,新建UI 文件夹,将InventorySlot制成预制体

④ 工具槽中工具缩略图

以InventorySlot(物品背景)为父物体,UI-Image,命名为ItemDisplay(物品缩略图)。

设置大小位置图片,勾选Preserve Aspect

⑤ 工具槽中的工具位:复制InventorySlot,得到8个工具位

6、物品栏(收获的物品)

(1) 复制ToolsPanel,重命名为ItemsPanel,调整到适当位置

(2) 更改文本Tools为Items

7、说明面板

(1) 背景:以InventoryPanel为父物体,UI-Image。命名为ItemInfo

(2) 说明文本(名称):以ItemInfo为父物体,UI-text,命名为ItemName

(3) 说明文本(描述):复制ItemName,命名为ItemDescription,设置大小字号等

8、隐藏InventoryPanel面板

9、创建返回按钮:复制背包按钮,重命名为ExitBtn。放入InventoryPanel面板,设置大小颜色等

10、设置按钮

(1) 编辑UIManager.cs,设置UI面板显示或隐藏

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }
    public GameObject inventoryPanle;
    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if (Instance == this) { Instance = null; }
    }
    public void ToggleInventoryPanel()
    {
        inventoryPanle.SetActive(!inventoryPanle.activeSelf);
    }
}

(2) 赋值

(3) 设置按钮:背包、返回两个按钮

二、显示物品缩略图

1、管理一个物品槽中的物品的属性和数量

(1) 打开Scripts-Inventory文件新建 ItemSlotData.cs

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

[System.Serializable]
public class ItemSlotData
{
    public ItemData itemData;
    public int quantity;
    public ItemSlotData(ItemData itemData, int quantity)
    {
        this.itemData = itemData;
        this.quantity = quantity;
        ValidateQuantity();
    }
    public ItemSlotData(ItemData itemData)
    {
        this.itemData = itemData;
        quantity = 1;
        ValidateQuantity();
    }
    public void AddQuantity()
    {
        AddQuantity(1);
    }
    public void AddQuantity(int amountToAdd)
    {
        quantity += amountToAdd;
    }
    public void Remove()
    {
        quantity--;
        ValidateQuantity();
    }
    public void ValidateQuantity()
    {
        if (quantity <= 0 || itemData ==null)
        {
            Empty();
        }
    }
    public void Empty()
    {
        itemData = null;
        quantity = 0;
    }
}

(2) 作用:根据传入的参数不同,创建不同的ItemSlotData对象,并设置该对象的数量 

2、显示物品槽中物品的缩略图的方法

(1) 在UI文件夹下新建InventorySlot.cs,给打开预制体InventorySlot,添加这个组件

(2) 编辑InventorySlot.cs,通过Display方法显示物品缩略图

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

public class InventorySlot : MonoBehaviour
{
    ItemData itemToDisplay;
    public Image itemDisplayImage;

    public enum InventoryType { Item, Tool }
    public InventoryType inventoryType;

    public void Display(ItemSlotData itemSlot)
    {
        itemToDisplay = itemSlot.itemData;
        if (itemToDisplay != null && itemToDisplay.thumbnail != null)
        {
            itemDisplayImage.sprite = itemToDisplay.thumbnail;
            itemDisplayImage.gameObject.SetActive(true);
            return;
        }
        itemDisplayImage.gameObject.SetActive(false);
        this.itemToDisplay = null;
    }
}

(3) 打开InventorySlot 预制体,赋值

(4) 在Hierarchy面板,赋值Type

3、编辑InventoryManager.cs

(1) 初始化工具槽数组和工具槽,检查选中物体类型

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

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance { get; private set; }

    [Header("Tools")]
    [SerializeField] private ItemSlotData[] toolSlots = new ItemSlotData[8];
    [SerializeField] private ItemSlotData equippedToolSlot = null;
    [Header("Items")]
    [SerializeField] private ItemSlotData[] itemSlots = new ItemSlotData[8];
    [SerializeField] private ItemSlotData equippedItemSlot = null;
    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if(Instance == this) { Instance = null; }
    }

    #region Gets and Checks
    public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
    {
        if(inventoryType == InventorySlot.InventoryType.Item) {  return itemSlots; }
        return toolSlots;
    }
    #endregion
}

(2) 赋值

4、编辑UIManager.cs

(1) 显示工具栏中工具的缩略图

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }

    public GameObject inventoryPanle;
    public InventorySlot[] toolSlots;

    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if (Instance == this) { Instance = null; }
    }
    void Start()
    {
        RenderInventory();
    }

    public void RenderInventory()
    {
        ItemSlotData[] inventoryToolSlots =InventoryManager.Instance.
GetInventorySlots(InventorySlot.InventoryType.Tool);
        RenderInventoryPanel(inventoryToolSlots,toolSlots);
    }

    void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            uiSlots[i].Display(slots[i]);
        }
    }
    public void ToggleInventoryPanel()
    {
        inventoryPanle.SetActive(!inventoryPanle.activeSelf);
        //注意增加更新物品栏内容的方法
        RenderInventory();
    }
}

(2) 赋值

5、显示物品栏(收获物品)缩略图

(1) 编辑UIManager.cs,显示物品栏的缩略图

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }
    //归类管理Unity编辑器的Inspector面板
    [Header("Inventory System")]
    public GameObject inventoryPanle;
    public InventorySlot[] toolSlots;
    //物品槽
    public InventorySlot[] itemSlots;

    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if (Instance == this) { Instance = null; }
    }
    void Start()
    {
        RenderInventory();
    }
    public void RenderInventory()
    {
        ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
        //物品槽
        ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
        RenderInventoryPanel(inventoryToolSlots, toolSlots);
        //物品槽
        RenderInventoryPanel(inventoryItemSlots, itemSlots);
    }

    void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            uiSlots[i].Display(slots[i]);
        }
    }
    public void ToggleInventoryPanel()
    {
        inventoryPanle.SetActive(!inventoryPanle.activeSelf);
        RenderInventory();
    }
}

(2) 赋值

三、显示物品名称和描述

1、设置文本

(1) 编辑UIManager.cs,设置文本应该显示的信息

//物品名称和描述文本
public Text itemNameText;
public Text itemDescriptionText;
public void DisplayItemInfo(ItemData data)
{
    if (data == null)
    {
        itemNameText.text = "";
        itemDescriptionText.text = "";
        return;
    }
    itemNameText.text = data.name;
    itemDescriptionText.text = data.description;
}

(2) 赋值

2、编辑InventorySlot.cs, 增加鼠标悬停事件

//增加接口
public class InventorySlot : MonoBehaviour,IPointerEnterHandler,IPointerExitHandler
{
    public void OnPointerEnter(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(itemToDisplay);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(null);
    }
}
四、显示物品数量

详细教程步骤二

1、添加物品数量文本

(1) 以HandSlot为父物体,UI-Text,重命名为QuantityText,设置大小位置文本字体段落颜色

(2) Apply 预制体InventorySlot的更改

2、验证物品栏槽位的有效性

(1) 编辑InventoryManager.cs

#region Inventory Slot validation
//在Unity编辑器中当脚本发生变化时自动调用,用于验证物品栏槽位的有效性
public void OnValidate()
{
    ValidateInventorySlots(itemSlots);
    ValidateInventorySlots(toolSlots);
}
//验证单个物品栏槽位的有效性。如果物品栏槽位中的物品存在但数量为0,则将数量设置为1
void ValidateInventorySlots(ItemSlotData slot)
{
    if(slot.itemData != null && slot.quantity == 0)
    {
        slot.quantity = 1;
    }
}
//用于验证物品栏槽位数组中的所有槽位的有效性。
//通过循环调用ValidateInventorySlots(ItemSlotData slot)方法来验证每个槽位的有效性
void ValidateInventorySlots(ItemSlotData[] array)
{
    foreach(ItemSlotData slot in array)
    {
        ValidateInventorySlots(slot);
    }
}
#endregion

(2) 假设各物品的初始数量(用于后续测试)

3、显示物品数量

(1) 编辑InventorySlot.cs, 增加需要显示物品的数量

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

public class InventorySlot : MonoBehaviour,IPointerEnterHandler,IPointerExitHandler
{
    ItemData itemToDisplay;
    public Image itemDisplayImage;
    //物品数量
    int quantity;
    //数量文本
    public Text quantityText;

    public enum InventoryType { Item, Tool }
    public InventoryType inventoryType;

    public void Display(ItemSlotData itemSlot)
    {
        itemToDisplay = itemSlot.itemData;
        //初始化数量
        quantity = itemSlot.quantity;
        //初始的数量文本
        quantityText.text = "";
        if (itemToDisplay != null && itemToDisplay.thumbnail != null)
        {
            itemDisplayImage.sprite = itemToDisplay.thumbnail;
            //数量文本
            if (quantity > 1)
            {
                quantityText.text = quantity.ToString();
            }
            itemDisplayImage.gameObject.SetActive(true);
            return;
        }
        itemDisplayImage.gameObject.SetActive(false);
        this.itemToDisplay = null;
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(itemToDisplay);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(null);
    }
}

 (2) 赋值

物品槽和状态栏UI

详细教程 步骤十一、步骤十四

一、准备工作

1、状态栏背景:UI-Image,命名为StatusBar,调整大小、位置等。颜色F4DDB7

2、手持物品:复制一个HandSlot,作为StatusBar的子物体

3、时间框:以StatusBar为父物体,Create Empty,命名为TimeInfo

4、天气位:复制步骤 2 中的HandSlot,作为TimeInfo的子物体,命名为Weather

5、季节和星期:以TimeInfo为父物体UI-Text,重命名为Date

6、时间:以TimeInfo为父物体UI-Text,重命名为Time

7、在UI文件夹新建 HandInventorySlot.cs 作为InventorySlot.cs的子类

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

public class HandInventorySlot : InventorySlot
{
    
}

8、装备物品槽缩略图和数量:

(1) 复制InventorySlot下的ItemDisplay,作为子物体粘贴到ToolPanel下的 HandSlot下

(2) 复制InventorySlot下的QuantityText,作为子物体粘贴到ToolPanel下的 HandSlot下

(3) 给ToolPanel下的 HandSlot  添加HandInventorySlot.cs组件,赋值,更改Inventory Type

(4) 同样的方法设置ItemPanel下的 HandSlot 

9、手持物品缩略图和数量:

(1) 以步骤2的HandSlot为父物体,UI-Image,命名为HandSlotImage

(2) 复制InventorySlot下的QuantityText,作为子物体粘贴到StatusBar下的 HandSlot下,调整大小

(3) 给StatusBar下的 HandSlot  添加HandInventorySlot.cs组件,赋值(注意更改Inventory Type)

二、装备物品槽缩略图

1、设定装备物品槽的类型

(1) 编辑 InventoryManager.cs

#region Gets and Checks
//待装备物品的类型
public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
{
    if(inventoryType == InventorySlot.InventoryType.Item)
    {
        return equippedItemSlot;
    }
    return equippedToolSlot;
}
public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
{
    if(inventoryType == InventorySlot.InventoryType.Item) {  return itemSlots; }
    return toolSlots;
}
#endregion

(2) 赋值

 

2、显示装备槽中的物品

(1) 编辑UIManager.cs

//装备物品槽中的待装备物品
public HandInventorySlot toolHandSlot;
public HandInventorySlot itemHandSlot;
public void RenderInventory()
{
    ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
    RenderInventoryPanel(inventoryToolSlots, toolSlots);
    ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
    RenderInventoryPanel(inventoryItemSlots, itemSlots);

    //待装备物品
    toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
    itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
}

(2) 赋值

三、手持槽物品缩略图

1、设定和检查手持物品槽的类型: 编辑 InventoryManager.cs

    #region Gets and Checks
    //手持槽的类型
    public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
    {
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            return equippedItemSlot.itemData;
        }
        return equippedToolSlot.itemData;
    }
    //装备槽的类型
    public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
    {
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            return equippedItemSlot;
        }
        return equippedToolSlot;
    }
    //物品栏中物品槽的类型
    public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
    {
        if(inventoryType == InventorySlot.InventoryType.Item) {  return itemSlots; }
        return toolSlots;
    }
    //工具还是种子
    public bool IsTool(ItemData item)
    {
        EquipmentData equipment = item as EquipmentData;
        if (equipment != null) { return true; }
        SeedData seed = item as SeedData;
        return seed != null;
    }
    #endregion

2、显示手持槽中的物品

(1) 编辑UIManager.cs

//手持物品
public Image toolEquipSlotImage;
public void RenderInventory()
{
    ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
    RenderInventoryPanel(inventoryToolSlots, toolSlots);
    ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
    RenderInventoryPanel(inventoryItemSlots, itemSlots);

    toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
    itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
    //手持槽物品
    ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
    if (equippedTool != null)
    {
        toolEquipSlotImage.sprite = equippedTool.thumbnail;
        toolEquipSlotImage.gameObject.SetActive(true);
        return;
    }
    toolEquipSlotImage.gameObject.SetActive(false);
}

(2) 赋值

物品交换
一、为物品分配索引值

1、分配被点击物品的索引:编辑InventorySlot.cs,把被点击物品的索引分配给this.slotIndex

//点击的物品槽的索引
int slotIndex;
//把被点击物品的索引分配给this.slotIndex
public void AssignIndex(int slotIndex)
{
    this.slotIndex = slotIndex;
}

2、为物品栏中的每一个物品分配一个索引值:

(1) 编辑UIManager.cs,创建并在Start方法中调用AssignSlotIndex()方法

void Start()
{
    RenderInventory();
    //调用方法
    AssignSlotIndex();
}
//遍历……数组,并为数组中的每个 InventorySlot 对象分配一个索引值
public void AssignSlotIndex()
{
    for(int i = 0; i < toolSlots.Length; i++)
    {
        toolSlots[i].AssignIndex(i);
        itemSlots[i].AssignIndex(i);
    }
}

(2) 索引值被传递给InventorySlot.cs中的AssignIndex(int slotIndex)方法,并在slotIndex中显示出来

二、物品槽到装备槽

1、编辑 ItemSlotData.cs

(1) 比较两个物体的itemData属性是否相同(是不是同一种物体——用于堆叠物品)

(2) 判断某itemData是否为空,可结合Empty方法,用于清空物品槽中的物品

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

[System.Serializable]
public class ItemSlotData
{
    public ItemData itemData;
    public int quantity;
    public ItemSlotData(ItemData itemData, int quantity)
    {
        this.itemData = itemData;
        this.quantity = quantity;
        ValidateQuantity();
    }
    public ItemSlotData(ItemData itemData)
    {
        this.itemData = itemData;
        quantity = 1;
        ValidateQuantity();
    }
    public ItemSlotData(ItemSlotData slotToClone)
    {
        itemData = slotToClone.itemData;
        quantity = slotToClone.quantity;
    }
    public void AddQuantity()
    {
        AddQuantity(1);
    }
    public void AddQuantity(int amountToAdd)
    {
        quantity += amountToAdd;
    }
    public void Remove()
    {
        quantity--;
        ValidateQuantity();
    }
    //比较传入的参数slotToCompare和当前ItemSlotData对象(this)的的itemData属性是否相同
    //并返回一个布尔值
    //具体作用:判断二者是否可以叠加
    //调用方法:A.Stackalbe(B);
    public bool Stackable(ItemSlotData slotToCompare)
    {
        return slotToCompare.itemData == itemData;
    }
    public void ValidateQuantity()
    {
        if (quantity <= 0 || itemData == null)
        {
            Empty();
        }
    }
    public void Empty()
    {
        itemData = null;
        quantity = 0;
    }
    //判断ItemSlotData对象的itemData属性是否为空(null)
    public bool IsEmpty()
    {
        return itemData == null;
    }
}

2、编辑 InventoryManager.cs

(1) 增加两个物品转移的方法

public void InventoryToHand(int slotIndex,InventorySlot.InventoryType inventoryType)
{
    Debug.Log("装备物品");
}
public void HandToInventory(InventorySlot.InventoryType inventoryType)
{
    Debug.Log("收回物品");
}

(2) 检查装备槽位

public void OnValidate()
{
    //检查装备槽位
    ValidateInventorySlots(equippedToolSlot);
    ValidateInventorySlots(equippedItemSlot);
    //检查物品栏中的物品槽位
    ValidateInventorySlots(itemSlots);
    ValidateInventorySlots(toolSlots);
}

(3) 依据选中槽所属的类型,将选中槽赋值给变量handToEquip

    public void InventoryToHand(int slotIndex,InventorySlot.InventoryType inventoryType)
    {
        ItemSlotData handToEquip;
        ItemSlotData[] inventoryToAlter;
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            handToEquip = equippedItemSlot;
            inventoryToAlter = itemSlots;
        }
        else
        {
            handToEquip = equippedToolSlot;
            inventoryToAlter = toolSlots;
        }
    }

(3) 增加将物品槽中的物品复制到装备槽的方法

public void EquipHandSlot(ItemData item)
{
    if (IsTool(item))
    {
        equippedToolSlot = new ItemSlotData(item);
    }
    else
    {
        equippedItemSlot = new ItemSlotData(item);
    }
}
//根据物品类型将选中物品由物品槽复制到装备槽
public void EquipHandSlot(ItemSlotData itemSlot)
{
    //获取选中物品槽的物品数据
    ItemData item = itemSlot.itemData;
    //调用IsTool方法检查物品类型
    if (IsTool(item))
    {
        //(物品是工具类型)将 itemSlot 中的物品数据复制到 equippedToolSlot 中
        equippedToolSlot = new ItemSlotData(itemSlot);
    }
    else
    {
        //如果物品不是工具类型,将 itemSlot 中的物品数据复制到 equippedItemSlot 中
        equippedItemSlot = new ItemSlotData(itemSlot);
    }
}

(4)  工具槽中的物品转移到装备槽(堆叠或转移)

    public void InventoryToHand(int slotIndex,InventorySlot.InventoryType inventoryType)
    {
        ItemSlotData handToEquip;
        ItemSlotData[] inventoryToAlter;
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            handToEquip = equippedItemSlot;
            inventoryToAlter = itemSlots;
        }
        else
        {
            handToEquip = equippedToolSlot;
            inventoryToAlter = toolSlots;
        }
        //(选中槽与装备槽物品相同)可以堆叠,同时清空选中槽物品
        if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
        {
            ItemSlotData slotToAlter = inventoryToAlter[slotIndex];
            handToEquip.AddQuantity(slotToAlter.quantity);
            slotToAlter.Empty();
        }
        //不能堆叠,选中槽物品转移到装备槽,同时清空选中槽物品
        else
        {
            //将 inventoryToAlter 数组中的指定索引 slotIndex 的值赋给临时变量 slotToEquip
            ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);
            //将 handToEquip 的值赋给 inventoryToAlter 数组中的指定索引 slotIndex
            inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);
            //临时变量中的物品复制到装备槽
            EquipHandSlot(slotToEquip);
            //清空handToEquip 的值
            handToEquip.Empty();
        }
        UIManager.Instance.RenderInventory();
    }

 

3、编辑 InventorySlot.cs,实现物品转移

(1) 增加鼠标点击事件,调用InventoryManager.cs方法

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

//增加点击事件接口
public class InventorySlot : MonoBehaviour,IPointerEnterHandler,IPointerExitHandler,IPointerClickHandler
{
    ItemData itemToDisplay;
    public Image itemDisplayImage;
    //点击的物品槽的索引
    int slotIndex;

    int quantity;
    public Text quantityText;

    public enum InventoryType { Item, Tool }
    public InventoryType inventoryType;

    public void Display(ItemSlotData itemSlot)
    {
        itemToDisplay = itemSlot.itemData;
        quantity = itemSlot.quantity;
        quantityText.text = "";
        if (itemToDisplay != null && itemToDisplay.thumbnail != null)
        {
            itemDisplayImage.sprite = itemToDisplay.thumbnail;
            if (quantity > 1)
            {
                quantityText.text = quantity.ToString();
            }
            itemDisplayImage.gameObject.SetActive(true);
            return;
        }
        itemDisplayImage.gameObject.SetActive(false);
        this.itemToDisplay = null;
    }
    public void AssignIndex(int slotIndex)
    {
        this.slotIndex = slotIndex;
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(itemToDisplay);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(null);
    }
    //点击事件(注意可重写)
    public virtual void OnPointerClick(PointerEventData eventData)
    {
        InventoryManager.Instance.InventoryToHand(slotIndex, inventoryType);
    }
}

(2) 解析:调用InventoryManager.cs方法。传入的参数:点击的物品槽索引,所处的物品栏类型

 4、重写点击事件:

(1) 目的:点击已装备槽时,调用InventoryMagager类中的HandToInventory方法

(2) 编辑HandInventorySlot.cs,传入参数为点击选中槽所属的类型

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

public class HandInventorySlot : InventorySlot
{
    public override void OnPointerClick(PointerEventData eventData)
    {
        InventoryManager.Instance.HandToInventory(inventoryType);
    }
}
三、装备槽到物品槽

1、编辑 InventoryManager.cs

(1) 增加装备槽复制到物品槽的方法

    #region 物品交换的方法
    //根据物品类型将装备槽中的物品由装备槽复制到物品槽
    public bool StackItemToInventory(ItemSlotData itemSlot, ItemSlotData[] inventoryArray)
    {
        //遍历inventoryArray(某类物品栏的物品槽数组)
        for (int i = 0; i < inventoryArray.Length; i++)
        {
            //可以堆叠(装备槽物品与物品栏中的某种物品是同种物品)
            if (inventoryArray[i].Stackable(itemSlot))
            {
                inventoryArray[i].AddQuantity(itemSlot.quantity);
                itemSlot.Empty();
                return true;
            }
        }
        return false;
    }

    public void EquipHandSlot(ItemSlotData itemSlot)
    {
        ItemData item = itemSlot.itemData;
        if (IsTool(item))
        {
            equippedToolSlot = new ItemSlotData(itemSlot);
        }
        else
        {
            equippedItemSlot = new ItemSlotData(itemSlot);
        }
    }
    #endregion

(2) 编辑物品由装备槽转移到物品槽的方法

    public void HandToInventory(InventorySlot.InventoryType inventoryType)
    {
        ItemSlotData handSlot;
        ItemSlotData[] inventoryToAlter;
        //确定点击物品的类型并赋值
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            handSlot = equippedItemSlot;
            inventoryToAlter = itemSlots;
        }
        else
        {
            handSlot = equippedToolSlot;
            inventoryToAlter = toolSlots;
        }

        if(!StackItemToInventory(handSlot, inventoryToAlter))
        {
            for(int i = 0; i < inventoryToAlter.Length; i++)
            {
                if (inventoryToAlter[i].IsEmpty())
                {
                    inventoryToAlter[i] = new ItemSlotData(handSlot);
                    handSlot.Empty();
                    break;
                }
            }
        }
        UIManager.Instance.RenderInventory();
    }

if (!StackItemToInventory(handSlot, inventoryToAlter)){}

① 先执行StackItemToInventory() 方法,返回一个bool值

② 若物品可以堆叠,则StackItemToInventory() 方法返回的值为true,

if (!StackItemToInventory(handSlot, inventoryToAlter)){}该语句不能执行

③ 若物品不能堆叠,则StackItemToInventory() 方法返回的值为false,

执行 if (!StackItemToInventory(handSlot, inventoryToAlter)){}

2、InventoryManager.cs 的完整代码

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

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance { get; private set; }

    [Header("Tools")]
    [SerializeField] private ItemSlotData[] toolSlots = new ItemSlotData[8];
    [SerializeField] private ItemSlotData equippedToolSlot = null;
    [Header("Items")]
    [SerializeField] private ItemSlotData[] itemSlots = new ItemSlotData[8];
    [SerializeField] private ItemSlotData equippedItemSlot = null;
    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if(Instance == this) { Instance = null; }
    }
    public void InventoryToHand(int slotIndex,InventorySlot.InventoryType inventoryType)
    {
        ItemSlotData handToEquip;
        ItemSlotData[] inventoryToAlter;
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            handToEquip = equippedItemSlot;
            inventoryToAlter = itemSlots;
        }
        else
        {
            handToEquip = equippedToolSlot;
            inventoryToAlter = toolSlots;
        }
        if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
        {
            ItemSlotData slotToAlter = inventoryToAlter[slotIndex];
            handToEquip.AddQuantity(slotToAlter.quantity);
            slotToAlter.Empty();
        }
        else
        {
            ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);
            inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);
            EquipHandSlot(slotToEquip);
            handToEquip.Empty();
        }
        UIManager.Instance.RenderInventory();
    }
    public void HandToInventory(InventorySlot.InventoryType inventoryType)
    {
        ItemSlotData handSlot;
        ItemSlotData[] inventoryToAlter;
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            handSlot = equippedItemSlot;
            inventoryToAlter = itemSlots;
        }
        else
        {
            handSlot = equippedToolSlot;
            inventoryToAlter = toolSlots;
        }
        //将装备槽中的物品堆叠到相应的物品槽中
        if (!StackItemToInventory(handSlot, inventoryToAlter))
        {
            for(int i = 0; i < inventoryToAlter.Length; i++)
            {
                if (inventoryToAlter[i].IsEmpty())
                {
                    inventoryToAlter[i] = new ItemSlotData(handSlot);
                    handSlot.Empty();
                    break;
                }
            }
        }
        UIManager.Instance.RenderInventory();
    }

    #region Gets and Checks
    //手持物品槽的类型
    public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
    {
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            return equippedItemSlot.itemData;
        }
        return equippedToolSlot.itemData;
    }
    //选中物品槽的类型
    public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
    {
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            return equippedItemSlot;
        }
        return equippedToolSlot;
    }
    //物品栏中物品槽的类型
    public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
    {
        if(inventoryType == InventorySlot.InventoryType.Item) {  return itemSlots; }
        return toolSlots;
    }
    //检查特定类型的槽位是否已经装备了物品
    public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
    {
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            return !equippedItemSlot.IsEmpty();
        }
        return !equippedToolSlot.IsEmpty();
    }
    //检查给定的物品是否为工具,若为工具或种子,返回为true,否则返回false
    public bool IsTool(ItemData item)
    {
        //将 item 强制转换为 EquipmentData 类型,并将结果赋值给 equipment 变量
        EquipmentData equipment = item as EquipmentData;
        if (equipment != null)
        {
            return true;
        }
        SeedData seed = item as SeedData;
        return seed != null;
    }
    #endregion

    #region 物品交换的方法
    //根据物品类型将装备槽中的物品由装备槽复制到物品槽
    public bool StackItemToInventory(ItemSlotData itemSlot, ItemSlotData[] inventoryArray)
    {
        //遍历inventoryArray(某类物品栏的物品槽数组)
        for (int i = 0; i < inventoryArray.Length; i++)
        {
            //可以堆叠(装备槽物品与物品栏中的某种物品是同种物品)
            if (inventoryArray[i].Stackable(itemSlot))
            {
                inventoryArray[i].AddQuantity(itemSlot.quantity);
                itemSlot.Empty();
                return true;
            }
        }
        return false;
    }

    //根据物品类型将选中物品由物品槽复制到装备槽
    public void EquipHandSlot(ItemSlotData itemSlot)
    {
        //获取选中物品槽的物品数据
        ItemData item = itemSlot.itemData;
        //调用IsTool方法检查物品类型
        if (IsTool(item))
        {
            //(物品是工具类型)将 itemSlot 中的物品数据复制到 equippedToolSlot 中
            equippedToolSlot = new ItemSlotData(itemSlot);
        }
        else
        {
            //如果物品不是工具类型,将 itemSlot 中的物品数据复制到 equippedItemSlot 中
            equippedItemSlot = new ItemSlotData(itemSlot);
        }
    }
    #endregion

    #region Inventory Slot validation(验证物品栏槽位的有效性)
    public void OnValidate()
    {
        //检查装备槽位
        ValidateInventorySlots(equippedToolSlot);
        ValidateInventorySlots(equippedItemSlot);
        
        ValidateInventorySlots(itemSlots);
        ValidateInventorySlots(toolSlots);
    }
    void ValidateInventorySlots(ItemSlotData slot)
    {
        if(slot.itemData != null && slot.quantity == 0)
        {
            slot.quantity = 1;
        }
    }
    void ValidateInventorySlots(ItemSlotData[] array)
    {
        foreach(ItemSlotData slot in array)
        {
            ValidateInventorySlots(slot);
        }
    }
    #endregion
}
状态栏物品和数量

1、编辑UIManager.cs

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }

    [Header("Inventory System")]
    public GameObject inventoryPanle;
    public InventorySlot[] toolSlots;
    public InventorySlot[] itemSlots;

    public HandInventorySlot toolHandSlot;
    public HandInventorySlot itemHandSlot;

    public Text itemNameText;
    public Text itemDescriptionText;
    //状态栏工具数量
    public Text toolQuantityText;

    public Image toolEquipSlotImage;

    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if (Instance == this) { Instance = null; }
    }
    void Start()
    {
        RenderInventory();
        AssignSlotIndex();
    }
    public void AssignSlotIndex()
    {
        for(int i = 0; i < toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }
    public void RenderInventory()
    {
        ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots
(InventorySlot.InventoryType.Tool);
        RenderInventoryPanel(inventoryToolSlots, toolSlots);
        ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots
(InventorySlot.InventoryType.Item);
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot
(InventorySlot.InventoryType.Tool));
        itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot
(InventorySlot.InventoryType.Item));
        //手持槽物品
        ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem
(InventorySlot.InventoryType.Tool);
        //状态栏工具数量的文本
        toolQuantityText.text = "";
        if (equippedTool != null)
        {
            toolEquipSlotImage.sprite = equippedTool.thumbnail;
            toolEquipSlotImage.gameObject.SetActive(true);
            //显示状态栏上的工具的数量
            int quantity = InventoryManager.Instance.GetEquippedSlot
(InventorySlot.InventoryType.Tool).quantity;
            if (quantity > 1)
            {
                toolQuantityText.text = quantity.ToString();
            }
            return;
        }
        toolEquipSlotImage.gameObject.SetActive(false);
    }

    void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            uiSlots[i].Display(slots[i]);
        }
    }
    public void DisplayItemInfo(ItemData data)
    {
        if (data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";
            return;
        }
        itemNameText.text = data.name;
        itemDescriptionText.text = data.description;
    }
    public void ToggleInventoryPanel()
    {
        inventoryPanle.SetActive(!inventoryPanle.activeSelf);
        RenderInventory();
    }
}

2、赋值

更换土壤状态

详细教程步骤六、2

一、手持农具

1、编辑Player预制体,添加手持物体的位置

     以mixamorig6:RightHand为父物体,Create Empty,重命名为Hand Point

2、添加农具模型

(1) 编辑InventoryManager.cs,创建并调用手持农具的方法

//手持物体的位置
public Transform handPoint;
public void InventoryToHand(int slotIndex,InventorySlot.InventoryType inventoryType)
{
    //其它代码
    //手持农具
    if (inventoryType == InventorySlot.InventoryType.Tool)
    {
        RenderHand();
    }
    UIManager.Instance.RenderInventory();
}
//手持农具的方法
public void RenderHand()
{
    if (handPoint.childCount > 0)
    {
        Destroy(handPoint.GetChild(0).gameObject);
    }
    if (SlotEquipped(InventorySlot.InventoryType.Tool))
    {
        Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Tool).gameModel, handPoint);
    }
}
//检查手持的槽位是否已经装备了物品
    public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
    {
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            return !equippedItemSlot.IsEmpty();
        }
        return !equippedToolSlot.IsEmpty();
    }

(2) 解析

public bool IsEmpty()
{
    return itemData == null;
}

equippedToolSlot.IsEmpty() :判断 equippedToolSlot 是否为空,如果为空,IsEmpty()返回 true

public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
{
    return !equippedToolSlot.IsEmpty();
}

!equippedToolSlot.IsEmpty() :取反。

如果equippedToolSlot为空,IsEmpty()返回 true,SlotEquipped()返回false,即:槽位未装备物品

if (SlotEquipped(InventorySlot.InventoryType.Tool))
{
    Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Tool).gameModel,handPoint);
}

if (SlotEquipped(InventorySlot.InventoryType.Tool)) :如果槽位装备物品,在手持位生成该物品模型

(3) 赋值

3、装备和卸下农具

(1) 编辑InventoryManager.cs

    public void HandToInventory(InventorySlot.InventoryType inventoryType)
    {
        //其它代码
        //卸下农具
        if(inventoryType == InventorySlot.InventoryType.Tool)
        {
            RenderHand();
        }
        UIManager.Instance.RenderInventory();
    }
二、更改土壤状态

1、编辑Land.cs

    public void Interact()
    {
        //根据点击事件决定土壤状态
        ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
        if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
        {
            return;
        }
        EquipmentData equipmentTool = toolSlot as EquipmentData;
        if (equipmentTool != null)
        {
            EquipmentData.ToolType toolType = equipmentTool.Type;
            switch (toolType)
            {
                case EquipmentData.ToolType.Hoe: SwitchLandStatus(LandStatus.Farmland); break;
                case EquipmentData.ToolType.WateringCan: 
                    if (landStatus == LandStatus.Farmland||landStatus ==LandStatus.Watered)
                    {
                        SwitchLandStatus(LandStatus.Watered);
                    }
                    break;
            }
        }
    }

时间管理系统

详细教程 步骤二

一、设置时间逻辑

1、设定年季周、日时分:Scripts文件夹下创建Time 文件夹,其下创建GameTimestamp.cs,

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class GameTimestamp
{
    public int year;
    public enum Season { Spring, Summer, Fall, Winter }
    public Season season;
    public enum DayOfTheWeek { Saturday, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday }
    public int day, hour, minute;

    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }
    public void UpdateClock()
    {
        minute++;
        if (minute >= 60) { minute = 0; hour++; }
        if (hour >= 24) { hour = 0; day++; }
        if (day > 30)
        {
            day = 1;
            if (season == Season.Winter)
            {
                season = Season.Spring;
                year++;
            }
            else
            {
                season++;
            }
        }
    }
    public GameTimestamp(GameTimestamp timestamp)
    {
        this.year = timestamp.year;
        this.season = timestamp.season;
        this.day = timestamp.day;
        this.hour = timestamp.hour;
        this.minute = timestamp.minute;
    }

    public static int HoursToMinutes(int hour) { return hour * 60; }
    public static int DaysToHours(int day) { return day * 24; }
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30;
    }
    public static int YearsToDays(int year) { return year * 4 * 30; }
    public DayOfTheWeek GetDayOfTheWeek()
    {
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;
        int dayIndex = daysPassed % 7;
        return (DayOfTheWeek)dayIndex;
    }
}

AI的建议

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

[Serializable]
public class GameTimestamp
{
    // 时间的定义
    public enum Season { Spring, Summer, Autumn, Winter }

    public int hour;
    public Season season;
    public int day;
    public int month;
    public int year;

    public int minute;

    public GameTimestamp(int hour, Season season, int day, int month, int year)
    {
        this.hour = hour;
        this.season = season;
        this.day = day;
        this.month = month;
        this.year = year;
    }

    public void UpdateClock()
    {
        minute++;
        if (minute >= 60)
        {
            minute = 0;
            hour++;
            if (hour >= 24)
            {
                hour = 0;
                day++;
                if (day > GetDaysInMonth(month))
                {
                    day = 1;
                    month++;
                    if (month > 12){ month = 1;year++; }
                }
            }
        }
    }

    private int GetDaysInMonth(int month)
    {
        switch (month)
        {
            case 2:
                return 28;
            case 4:
            case 6:
            case 9:
            case 11:
                return 30;
            default:
                return 31;
        }
    }

    public static int HoursToMinutes(int hours)
    {
        return hours * 60;
    }
}

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [SerializeField]
    private GameTimestamp timestamp;

    public float timeScale = 1.0f;

    //太阳的位置
    [Header("Day and Night cycle")]
    public Transform sunTransform;

    private void Awake()
    {
        if (Instance != null && Instance != this){ Destroy(gameObject); }
        else { Instance = this; }
    }

    private void OnDestroy()
    {
        if (Instance == this) { Instance = null; }
    }

    void Start()
    {
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());
    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1f / timeScale);
        }
    }

    public void Tick()
    {
        timestamp.UpdateClock();
        //游戏运行的时间转换成分钟数
        int timeInminutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;
        //太阳与地平线间的夹角
        float sunAngle = 360f / (24f * 60f) * timeInminutes - 90;
        //设置太阳的欧拉角(在三维空间中旋转的向量)
        sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
    }
}

2、时间流逝

(1) 给Manager添加TimeManager.cs组件

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

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }
    [SerializeField]
    public GameTimestamp timestamp;
    public float timeScale = 1.0f;

    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if (Instance == this) { Instance = null; }
    }
    void Start()
    {
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());
    }
    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
    }
    public void Tick()
    {
        timestamp.UpdateClock();
    }
}

(2) 流逝逻辑:每隔1秒钟,minute ++;

二、时间与光照

1、编辑TimeManager.cs,设置太阳旋转

//太阳的位置
[Header("Day and Night cycle")]
public Transform sunTransform;

void UpdateSunMovement()
{
    //游戏运行的时间转换成分钟数
    int timeInminutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;
    //太阳与地平线间的夹角
    float sunAngle = 360f / (24f * 60f) * timeInminutes - 90;
    //设置太阳的欧拉角(在三维空间中旋转的向量)
    sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
}
public void Tick()
{
    timestamp.UpdateClock();
    UpdateSunMovement();
}

2、赋值

3、测试

(1) 编辑PlayerController.cs

public void Update()
{
    Interact();
    //时间加速
    if(Input.GetKey(KeyCode.RightBracket))
    {
        TimeManager.Instance.Tick();
    }
}

(2) 测试:按下右方括号,时间加速

三、时间管理

1、时间接口:新建ITimeTracker.cs

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

public interface ITimeTracker
{
    void ClockUpdate(GameTimestamp timestamp);
}

2、实时更新时间,并将时间信息传递给已注册的时间跟踪器:编辑TimeManager.cs

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

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }
    //内部时钟
    [Header("Internal Clock")]
    [SerializeField]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    [Header("Day and Night cycle")]
    public Transform sunTransform;
    //存储所有已注册的时间跟踪器
    List<ITimeTracker> listeners = new List<ITimeTracker>();

    public void Tick()
    {
        timestamp.UpdateClock();
        //遍历所有已注册的时间跟踪器
        foreach(ITimeTracker listener in listeners)
        {
            listener.ClockUpdate(timestamp);
        }
        UpdateSunMovement();
    }
    //注册时间跟踪器,并添加到listerns列表中
    public void RegisterTracker(ITimeTracker listener)
    {
        listeners.Add(listener);
    }
    //注销时间跟踪器,并从列表移除
    public void UnregisterTracker(ITimeTracker listener)
    {
        listeners.Remove(listener);
    }
}
四、显示时间

1、编辑UIManager.cs,更新状态栏上的时间

//接口
public class UIManager : MonoBehaviour, ITimeTracker
//状态栏
[Header("Status Bar")]
public Image toolEquipSlotImage;
public Text toolQuantityText;
//时间文本
public Text timeText;
public Text dateText;
void Start()
{
    RenderInventory();
    AssignSlotIndex();
    //注册时间跟踪器
    TimeManager.Instance.RegisterTracker(this);
}
//处理UI时间回调
public void ClockUpdate(GameTimestamp timestamp)
{
    int hours = timestamp.hour;
    int minutes = timestamp.minute;

    string prefix = "AM";
    if (hours > 12) { prefix = "PM"; hours -= 12; }
    //timeText.text = $"{prefix} {hours:00} :{minutes:00}";
    timeText.text = prefix + hours + ":" + minutes.ToString("00");
    int day = timestamp.day;
    string season = timestamp.season.ToString();
    string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();

    //dateText.text = $"{season} {day} ({dayOfTheWeek})";
    dateText.text = season + " " + day + " (" + dayOfTheWeek + ")";
}

2、赋值

五、土壤状态随时间改变

1、比较两个时间戳:编辑GameTimestamp.cs

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

[Serializable]
public class GameTimestamp
{
    public int year;
    public enum Season { Spring, Summer, Fall, Winter }
    public Season season;
    public enum DayOfTheWeek { Saturday, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday }
    public int day, hour, minute;

    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }
    public void UpdateClock()
    {
        minute++;
        if (minute >= 60) { minute = 0; hour++; }
        if (hour >= 24) { hour = 0; day++; }
        if (day > 30)
        {
            day = 1;
            if (season == Season.Winter)
            {
                season = Season.Spring;
                year++;
            }
            else{ season++; }
        }
    }
    public GameTimestamp(GameTimestamp timestamp)
    {
        this.year = timestamp.year;
        this.season = timestamp.season;
        this.day = timestamp.day;
        this.hour = timestamp.hour;
        this.minute = timestamp.minute;
    }

    public static int HoursToMinutes(int hour) { return hour * 60; }
    public static int DaysToHours(int day) { return day * 24; }
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30;
    }
    public static int YearsToDays(int year) { return year * 4 * 30; }
    public DayOfTheWeek GetDayOfTheWeek()
    {
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;
        int dayIndex = daysPassed % 7;
        return (DayOfTheWeek)dayIndex;
    }
    //比较两个时间戳
    public static int CompareTimestamp(GameTimestamp timestamp1, GameTimestamp timestamp2)
    {
        int timestamp1Hours = DaysToHours(YearsToDays(timestamp1.year))
                            + DaysToHours(SeasonsToDays(timestamp1.season))
                            + DaysToHours(timestamp1.day) 
                            + timestamp1.hour;
        int timestamp2Hours = DaysToHours(YearsToDays(timestamp2.year))
                            + DaysToHours(SeasonsToDays(timestamp2.season))
                            + DaysToHours(timestamp2.day)
                            + timestamp2.hour;
        int differnce = timestamp2Hours - timestamp1Hours;
        //返回 difference 的绝对值
        return Mathf.Abs(differnce);
    }
}

2、获取时间戳(timestamp):编辑TimeManager.cs

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

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }
    [SerializeField]
    [Header("Internal Clock")]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    [Header("Day and Night cycle")]
    public Transform sunTransform;
    List<ITimeTracker> listeners = new List<ITimeTracker>();

    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); }
        else { Instance = this; }
    }
    private void OnDestroy()
    {
        if (Instance == this) { Instance = null; }
    }

    void Start()
    {
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());
    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
    }
    void UpdateSunMovement()
    {
        int timeInminutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;
        float sunAngle = 360f / (24f * 60f) * timeInminutes - 90;
        sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
    }
    public void Tick()
    {
        timestamp.UpdateClock();
        foreach(ITimeTracker listener in listeners)
        {
            listener.ClockUpdate(timestamp);
        }
        UpdateSunMovement();
    }
    public void RegisterTracker(ITimeTracker listener)
    {
        listeners.Add(listener);
    }
    public void UnregisterTracker(ITimeTracker listener)
    {
        listeners.Remove(listener);
    }
    //获取新的时间戳
    public GameTimestamp GetGameTimestamp()
    {
        return new GameTimestamp(timestamp);
    }
}

3、土壤状态的改变

(1) 编辑Land.cs,

//接口
public class Land : MonoBehaviour,ITimeTracker
{
    //灌溉时间
    GameTimestamp timeWatered;

    void Start()
    {
        renderer = GetComponent<Renderer>();
        SwitchLandStatus(LandStatus.Soil);
        Select(false);
        //注册时间戳
        TimeManager.Instance.RegisterTracker(this);
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        landStatus = statusToSwitch;
        Material materialToSwitch = soilMat;
        switch (statusToSwitch)
        {
            case LandStatus.Soil: materialToSwitch = soilMat; break;
            case LandStatus.Farmland: materialToSwitch = farmlandMat; break;
            case LandStatus.Watered: 
                materialToSwitch = wateredMat; 
                //灌溉的时间戳
                timeWatered = TimeManager.Instance.GetGameTimestamp();
                break;
        }
        renderer.material = materialToSwitch;
    }
    //灌溉后土壤状态的变化
    public void ClockUpdate(GameTimestamp timestamp)
    {
        if(landStatus == LandStatus.Watered)
        {
            int hoursElapsed = GameTimestamp.CompareTimestamp(timeWatered, timestamp);
            if(hoursElapsed > 24)
            {
                SwitchLandStatus(LandStatus.Farmland);
            }
        }
    }
}

作物管理

详细教程

一、准备工作

1、制作Cabbage预制体,并给预制体Cabbage添加Mesh Collider,勾选Convex和Is Trigger

2、打开Asset-Data-Items,选择Cabbage,设置GameModle

3、素材准备

(1) 导入Cartoon_Farm_Crops(来自Unity官网)

(2) 把Standard Asset文件夹拖到Cartoon_Farm_Crops文件夹中

(3) 把Cartoon_Farm_Crops文件夹转移到Assets-Import Assets文件夹中

3、准备幼苗预制体

(1) 打开Assets-Import Assets-Cartoon_Farm_Crops-Prefabs-Standard文件夹

(2) 复制Carrot_Plant粘贴到Asset-Prefabs文件夹中。重命名为Cabbage Seedling

二、制作卷心菜

1、制作种子预制体

(1) Create Empty,重命名为Crop,Reset它的Transform

(2) 给Crop添加子物体Dirt_Pile、Cabbage Seeding和Cabbage,表示卷心菜的三个阶段

(3) 将Dirt_Pile重命名为Seed,将Crop制成预制体,把相关的预制体都放在新建的文件夹Crops中

(4) 打开Crop预制体,删除子物体Seed的Mesh Collider组件

(5) 删除Hierarchy面板上的Crop

2、设置Item对象Cabbage Seed(卷心菜)

(1) 编辑SeedData.cs,增加卷心菜的幼苗状态

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

[CreateAssetMenu(menuName =("Items/Seed"))]

public class SeedData : ItemData
{
    public int daysToGrow;
    public ItemData cropToYield;
    //幼苗
    public GameObject seedling;
}

(2) 保留预制体Crop的子物体中的Seed,删除预制体Crop下的其他子物体

3、作物生长的状态

(1) 给预制体Crop添加CropBehaviour.cs组件

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

public class CropBehaviour : MonoBehaviour
{
    SeedData seedToGrow;
    [Header("Stages of Life")]
    public GameObject seed;
    private GameObject seedling;
    private GameObject harvestable;
    public CropStage cropStage;
    public enum CropStage { Seed, Seedling, Havestable }

    public void Plant(SeedData seedToGrow)
    {
        this.seedToGrow = seedToGrow;
        seedling = Instantiate(seedToGrow.seedling, transform);
        ItemData CropToYield = seedToGrow.cropToYield;
        harvestable = Instantiate(CropToYield.gameModel, transform);
        SwitchState(CropStage.Seed);
    }
    void SwitchState(CropStage stateToSwitch)
    {
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        switch (stateToSwitch)
        {
            case CropStage.Seed: seed.SetActive(true); break;
            case CropStage.Seedling: seedling.SetActive(true); break;
            case CropStage.Havestable: harvestable.SetActive(true); break;
        }
        cropStage = stateToSwitch;
    }
}

(2) 赋值

三、制作番茄

1、ItemData对象——番茄种子

(1) 打开Data-Tools-Seeds文件夹,Create-Items-Seed。命名为Tomato Seed

(2) 编辑 SeedData.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName =("Items/Seed"))]
public class SeedData : ItemData
{
    public int daysToGrow;
    public ItemData cropToVield;
    public GameObject seedling;
    //重新生长
    [Header("Regrowable")]
    public bool regrowable;
    public int daysTogrow;
}

(3) 设置ItemData对象番茄种子的属性(Seeds to grow Tomato)

2、果实

(1) 预制体

① 将Tomato_Fruit拖放到Hierarchy,更改标签为Item,Layer为Item,

     重置Transform,旋转 x=-90。 名Tomato

② 将Tomato制成预制体,删除Hierarchy面板上的Tomato

③ 勾选Mesh Collide中的 Convex和Is Trigger

(2) ItemData对象 :

① 打开Data-Item文件夹,Create-Items-Item ,创建一个新项目 Tomato Crop,

The crop the player will harvest first

② 打开Data-Item文件夹,Create-Items-Item ,创建 Tomato(Technically a fruit)

③ 打开Data-Tools-Seed文件夹,设置Tomato Seed

(3) 编辑 Tomato预制体,添加InteractableObject.cs组件,赋值

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

public class InteractableObject : MonoBehaviour
{
    public ItemData item;
    public virtual void Pickup()
    {
        InventoryManager.Instance.EquipHandSlot(item);
        InventoryManager.Instance.RenderHand();
        Destroy(gameObject);
    }
}

3、幼苗预制体

(1) 打开Import Asset/Cartoon_Farm_Crops/Prefabs/Standard文件夹

(2) 将Tomato_Plant拖放到Hierarchy,重命名为Tomato Seedling

(3) 移除Tomato_Seedling上的Mesh Collider。制成预制体

(4) 设置ItemData对象 Tomato Seed(番茄种子)的幼苗属性

4、成熟植株预制体

(1) 复制Hierarchy面板上的Tomato_Seedling,重命名为Tomato Harvestable,制成预制体

(2) 编辑Tomato Harvestable预制体

① 添加子物体Tomato

② 更改标签和Layer为 Item

(3) 为Tomato Harvestable预制体添加 Mesh Collider组件

(4) 删除面板上的幼苗植株果实

5、设置ItemData对象番茄种子和番茄

四、播种

1、编辑Land.cs

//种子
[Header("Crops")]
public GameObject cropPrefab;
CropBehaviour cropPlanted = null;
public void Interact()
{
    ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
    if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
    {
        return;
    }
    EquipmentData equipmentTool = toolSlot as EquipmentData;
    if (equipmentTool != null)
    {
        EquipmentData.ToolType toolType = equipmentTool.Type;
        switch (toolType)
        {
            case EquipmentData.ToolType.Hoe: SwitchLandStatus(LandStatus.Farmland); break;
            case EquipmentData.ToolType.WateringCan: SwitchLandStatus(LandStatus.Watered); break;
        }
        //返回
        return;
    }
    //播种
    SeedData seedTool = toolSlot as SeedData;
    if (seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null)
    {
        GameObject cropObject = Instantiate(cropPrefab, transform);
        cropObject.transform.position =
            new Vector3(transform.position.x, 0f, transform.position.z);
        cropPlanted = cropObject.GetComponent<CropBehaviour>();
        cropPlanted.Plant(seedTool);
    }
}

2、赋值

五、生长

1、编辑CropBehaviour.cs

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

public class CropBehaviour : MonoBehaviour
{
    SeedData seedToGrow;
    [Header("Stages of Life")]
    public GameObject seed;
    private GameObject seedling;
    private GameObject harvestable;
    public CropStage cropStage;
    public enum CropStage { Seed, Seedling, Havestable }
    //生长
    int growth;
    int maxGrowth;

    public void Plant(SeedData seedToGrow)
    {
        this.seedToGrow = seedToGrow;
        seedling = Instantiate(seedToGrow.seedling, transform);
        ItemData CropToYield = seedToGrow.cropToYield;
        harvestable = Instantiate(CropToYield.gameModel, transform);
        //生长时间
        int hoursToGrow = GameTimestamp.DaysToHours(seedToGrow.daysToGrow);
        maxGrowth = GameTimestamp.HoursToMinutes(hoursToGrow);

        SwitchState(CropStage.Seed);
    }
    void SwitchState(CropStage stateToSwitch)
    {
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        switch (stateToSwitch)
        {
            case CropStage.Seed: seed.SetActive(true); break;
            case CropStage.Seedling: seedling.SetActive(true); break;
            case CropStage: harvestable.SetActive(true); break;
        }
        cropStage = stateToSwitch;
    }
    //生长
    public void Grow()
    {
        growth++;
        //幼苗
        if (growth >= maxGrowth / 2 && cropStage == CropStage.Seed)
        {
            SwitchState(CropStage.Seedling);
        }
        //成熟
        if (growth >= maxGrowth && cropStage == CropStage.Seedling)
        {
            SwitchState(CropStage.Havestable);
        }
    }
}

2、 编辑Land.cs,灌溉后作物生长

    public void ClockUpdate(GameTimestamp timestamp)
    {
        if (landStatus == LandStatus.Watered)
        {
            int hoursElapsed = GameTimestamp.CompareTimestamp(timeWatered, timestamp);
            //作物生长
            if (cropPlanted != null)
            {
                cropPlanted.Grow();
            }
            if (hoursElapsed > 24)
            {
                SwitchLandStatus(LandStatus.Farmland);
            }
        }
    }
六、收获果实后幼苗再生

1、创建一个具有可重生收获行为的对象

(1) 打开Farming文件夹,新建RegrowableHarvestBehaviour.cs

(2) 编辑Tomato Harvestable预制体,添加RegrowableHarvestBehaviour.cs组件

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

public class RegrowableHarvestBehaviour : InteractableObject
{
    CropBehaviour parentCrop;
    public void SetParent(CropBehaviour parentCrop)
    {
        this.parentCrop = parentCrop;
    }
    public override void Pickup()
    {
        InventoryManager.Instance.EquipHandSlot(item);
        InventoryManager.Instance.RenderHand();
    }
}

(3) 赋值

2、将植株重置为幼苗状态

(1) 编辑CropBehaviour.cs

    public void Plant(SeedData seedToGrow)
    {
        //其他代码
        //第一次种植时,检查它是否能生长
        if (seedToGrow.regrowable)
        {
            RegrowableHarvestBehaviour regrowableHarvest = harvestable.GetComponent<RegrowableHarvestBehaviour>();
            // 将可再生收获行为的父物体设置为当前物体 
            regrowableHarvest.SetParent(this);
        }
        SwitchStage(CropStage.Seed);
    }
    void SwitchStage(CropStage stateToSwitch)
    {
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        //其他代码
        switch (stateToSwitch)
        {         
            case CropStage.Havestable:
                harvestable.SetActive(true);
                //如果种子不可再生,将可收获的种子与该作物游戏对象分离并销毁它
                if (!seedToGrow.regrowable)
                {
                    harvestable.transform.parent = null;
                    Destroy(gameObject);
                }
                break;
        }
        cropStage = stateToSwitch;
    }
    //当玩家收获可再生作物时调用。将植物状态重置为幼苗
    public void Regrow()
    {
        int hoursToRegrow = GameTimestamp.DaysToHours(seedToGrow.daysToGrow);
        growth = maxGrowth -GameTimestamp.HoursToMinutes(hoursToRegrow);
        //切换回幼苗状态
        SwitchStage(CropStage.Seedling);
    }

(2) 编辑RegrowableHarvestBehaviour.cs

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

public class RegrowableHarvestBehaviour : InteractableObject
{
    CropBehaviour parentCrop;
    public void SetParent(CropBehaviour parentCrop)
    {
        this.parentCrop = parentCrop;
    }
    public override void Pickup()
    {
        InventoryManager.Instance.EquipHandSlot(item);
        InventoryManager.Instance.RenderHand();
        //将亲本作物重置为幼苗状态
        parentCrop.Regrow();
    }
}
七、枯萎

详细教程 步骤十一

1、制作枯萎的植物的材质

(1) 打开Assets-Import Assets-Farmland-Dirt-Materials,复制Dirt材质,重命名为Wilted Plant

(2) 更改Wilted Plant材质的颜色9A2603,去掉光滑度

2、制作枯萎的植物:编辑Crop预制体

(1) 打开Assets-Import Assets-Cartoon_Farm_Crops-Prefabs-Standard文件夹,将Pumpkin_Plant作为子物体拖放到Crop下,重命名为wilted,将Wilted Plant材质设置给Wilted

(2) 设置它的transform

3、设置枯萎系统:

(1) 编辑CropBehaviour.cs

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

public class CropBehaviour : MonoBehaviour
{
    SeedData seedToGrow;
    [Header("Stages of Life")]
    public GameObject seed;
    private GameObject seedling;
    private GameObject harvestable;
    //枯萎状态的植株
    public GameObject wilted;
    //枯萎状态
    public enum CropStage { Seed, Seedling, Havestable, Wilted }
    public CropStage cropStage;

    int growth;
    int maxGrowth;
    //正常生长的最大时间(48小时)
    int maxHealth = GameTimestamp.HoursToMinutes(48);
    int health;
    public void Plant(SeedData seedToGrow)
    {
        this.seedToGrow = seedToGrow;
        seedling = Instantiate(seedToGrow.seedling, transform);
        ItemData cropToYield = seedToGrow.cropToVield;
        harvestable = Instantiate(cropToYield.gameModel, transform);

        int hoursToGrow = GameTimestamp.DaysToHours(seedToGrow.daysToGrow);
        maxGrowth = GameTimestamp.HoursToMinutes(hoursToGrow);

        //第一次种植时,检查它是否能生长
        if (seedToGrow.regrowable)
        {
            RegrowableHarvestBehaviour regrowableHarvest = harvestable.GetComponent<RegrowableHarvestBehaviour>();
            // 将可再生收获行为的父物体设置为当前物体 
            regrowableHarvest.SetParent(this);
        }
        SwitchStage(CropStage.Seed);
    }
    void SwitchStage(CropStage stateToSwitch)
    {
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        //枯萎状态
        wilted.SetActive(false);
        switch (stateToSwitch)
        {
            case CropStage.Seed: seed.SetActive(true); break;
            case CropStage.Seedling:
                seedling.SetActive(true);
                //48小时的正常生长时间
                health = maxHealth;
                break;

            case CropStage.Havestable:
                harvestable.SetActive(true);
                //如果种子不可再生,将可收获的种子与该作物游戏对象分离并销毁它
                if (!seedToGrow.regrowable)
                {
                    harvestable.transform.parent = null;
                    Destroy(gameObject);
                }
                break;
            //枯萎植株选择
            case CropStage Wilted: wilted.SetActive(true); break;
        }
        cropStage = stateToSwitch;
    }
    public void Grow()
    {
        growth++;
        //枯萎前的健康时长
        if (health < maxHealth)
        {
            health++;
        }

        if (growth >= maxGrowth / 2 && cropStage == CropStage.Seed)
        {
            SwitchStage(CropStage.Seedling);
        }
        if (growth >= maxGrowth && cropStage == CropStage.Seedling)
        {
            SwitchStage(CropStage.Havestable);
        }
    }
    //枯萎逻辑
    public void Wilted()
    {
        health--;
        if (health <= 0 && cropStage != CropStage.Seed)
        {
            SwitchStage(CropStage.Wilted);
        }
    }
    //当玩家收获可再生作物时调用。将植物状态重置为幼苗
    public void Regrow()
    {
        int hoursToRegrow = GameTimestamp.DaysToHours(seedToGrow.daysToGrow);
        growth = maxGrowth -GameTimestamp.HoursToMinutes(hoursToRegrow);
        //切换回幼苗状态
        SwitchStage(CropStage.Seedling);
    }
}

(2) 赋值

4、编辑Land.cs

    public void ClockUpdate(GameTimestamp timestamp)
    {
        if (landStatus == LandStatus.Watered)
        {
            int hoursElapsed = GameTimestamp.CompareTimestamp(timeWatered, timestamp);
            if (cropPlanted != null)
            {
                cropPlanted.Grow();
            }
            if (hoursElapsed > 24)
            {
                SwitchLandStatus(LandStatus.Farmland);
            }
        }
        //枯萎
        if (landStatus != LandStatus.Watered && cropPlanted != null)
        {
            if(cropPlanted.cropStage !=CropBehaviour.CropStage.Seed)
            {
                cropPlanted.Wilted();
            }
        }
    }
八、收割后重新生长

1、编辑SeedData.cs

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

[CreateAssetMenu(menuName =("Items/Seed"))]

public class SeedData : ItemData
{
    public int daysToGrow;
    public ItemData cropToYield;
    public GameObject seedling;
    //重新生长
    [Header("Regrowable")]
    public bool regrowable;
    public int daysTogrow;
}

农田交互

一、收获

1、增加提示文本

2、编辑PlayerInteraction.cs

六、收割

1、销毁作物:编辑CropBehaviour.cs

    void SwitchState(CropStage stateToSwitch)
    {
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        switch (stateToSwitch)
        {
            case CropStage.Seed: seed.SetActive(true); break;
            case CropStage.Seedling: seedling.SetActive(true); break;
            case CropStage: harvestable.SetActive(true);
                //收割
                harvestable.transform.parent = null;
                Destroy(gameObject);
                break;
        }
        cropStage = stateToSwitch;
    }

 2、收获作物:编辑InventoryManager.cs

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

public class InventoryManager : MonoBehaviour
{    
    public void InventoryToHand(int slotIndex,InventorySlot.InventoryType inventoryType)
    {
        //其他代码##

        //手持收获物
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            RenderHand();
        }
        UIManager.Instance.RenderInventory();
    }
    public void HandToInventory(InventorySlot.InventoryType inventoryType)
    {
        //其他代码##

        //手持收获物
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            RenderHand();
        }
        UIManager.Instance.RenderInventory();
    }
    public void RenderHand()
    {
        //其他代码

        //收获物
        if (SlotEquipped(InventorySlot.InventoryType.Item))
        {
            Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Item).gameModel, handPoint);
        }
    }
}

3、拾起作物

(1) 编辑InventoryManager.cs,复制物体

#region 物品交换的方法
//根据物品类型将选中物体复制到对应位置
public void EquipHandSlot(ItemData item)
{
    if (IsTool(item))
    {
        equippedToolSlot = new ItemSlotData(item);
    }
    else
    {
        equippedItemSlot = new ItemSlotData(item);
    }
}
//其他代码
#endregion

 (2) 创建拾取方法:

① 在Scripts-Inventory文件夹下新建InteractableObject.cs

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

public class InteractableObject : MonoBehaviour
{
    public ItemData item;
    public virtual void Pickup()
    {
        InventoryManager.Instance.EquipHandSlot(item);
    }
}

② 给预制体Cabbage和Tomato,添加InteractableObject.cs组件,赋值,添加标签Item

农田交互

:编辑SeedData.cs

一、收获

1、增加提示文本

2、编辑PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{
    PlayerController playerController;
    Land selectLand = null;
    //选中的待收获作物
    InteractableObject selectInteractable = null;
    //提示文本
    public Text textComponent;
    void Start()
    {
        playerController = transform.parent.GetComponent<PlayerController>();
    }

    void Update()
    {
        RaycastHit hit;
        if (Physics.Raycast(transform.position, Vector3.down, out hit, 1))
        {
            OnInteractableHit(hit);
        }
    }
    void OnInteractableHit(RaycastHit hit)
    {
        Collider other = hit.collider;
        if (other != null && other.CompareTag("Land"))
        {
            Land land = other.GetComponent<Land>();
            if (land != null)
            {
                SelectLand(land); return;
            }
            else { Debug.Log("未选中任何田地"); }
        }
        //选中的待收获作物
        if (other.CompareTag("Item"))
        {
            selectInteractable = other.GetComponent<InteractableObject>();
            return;
        }
        if (selectInteractable != null)
        {
            selectInteractable = null;
        }
        if (selectLand != null)
        {
            DeselectLand(selectLand);
        }
    }
    public void Interact()
    {
        textComponent.text = "";
        //禁用工具
        if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
        {
            textComponent.text = "你腾不出手来!";
            return;
        }
        if (selectLand != null)
        {
            selectLand.Interact(); return;
        }
        Debug.Log("未站在田地上");
    }
    void SelectLand(Land land)
    {
        if (land == null) { Debug.Log("你未选择田地"); return; }
        if (selectLand != null) { selectLand.Select(false); }
        selectLand = land;
        land.Select(true);
    }
    void DeselectLand(Land land)
    {
        if (land == null) { Debug.Log("你没有选择田地"); return; }
        land.Select(false);
        selectLand = null;
    }
    //收割交互
    public void ItemIneract()
    {
        if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
        {
            InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
            return;
        }
        //拾取作物
        if (selectInteractable != null)
        {
            selectInteractable.Pickup();
        }
    }
}

2、编辑PlayerController.cs,点击 左Alt键,物品返回物品栏

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    PlayerInteraction playerInteraction;
    public float turnSpeed = 20f;
    Animator animator;
    bool isWalking = false;
    bool isRunning = false;

    Rigidbody rb;
    Vector3 movement;
    Quaternion rotation = Quaternion.identity;

    private float speed;
    [Header("Movement System")]
    public float walkSpeed = 6f;
    public float runSpeed = 12f;

    void Start()
    {
        animator = GetComponent<Animator>();
        rb = GetComponent<Rigidbody>();
        playerInteraction = GetComponentInChildren<PlayerInteraction>();
    }
    private void FixedUpdate()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        movement.Set(horizontal, 0f, vertical);
        movement.Normalize();

        bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0f);
        bool hasVerticalInput = !Mathf.Approximately(vertical, 0f);

        if (Input.GetButton("Sprint"))
        {
            speed = runSpeed;
            isRunning = true;
        }
        else if (hasHorizontalInput || hasVerticalInput)
        {
            isWalking = true;
            isRunning = false;
            speed = walkSpeed;
        }
        else
        {
            isWalking = false;
            isRunning = false;
            speed = 0f;
        }
        Vector3 desireForward = Vector3.RotateTowards(transform.forward, movement, turnSpeed * Time.deltaTime, 0f);
        rotation = Quaternion.LookRotation(desireForward);
    }
    public void Update()
    {
        Interact();
        if (Input.GetKey(KeyCode.RightBracket))
        {
            TimeManager.Instance.Tick();
        }
    }
    public void Interact()
    {
        if (Input.GetButtonDown("Fire1"))
        {
            playerInteraction.Interact();
        }
        //左Alt
        if (Input.GetButtonDown("Fire2"))
        {
            playerInteraction.ItemIneract();
        }
    }

    private void OnAnimatorMove()
    {
        animator.SetBool("IsWalking", isWalking);
        animator.SetBool("IsRunning", isRunning);
        Vector3 movementSpeed = movement * speed;
        rb.MovePosition(rb.position + movementSpeed * animator.deltaPosition.magnitude);
        rb.MoveRotation(rotation);
    }
}
二、禁止角色上升

1、编辑预制体Cabbage和Player,分别为二者添加新Layer

2、禁用Item与Player层的交互

(1) 

(2)

(3)

(4)

(1) 

(2)

(3)

(4)

(4)

(4)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值