Article directory
- Preface
- material
- 1. Construction system
- 2. Inventory system
-
- 1. Simple drawing UI
- 2. Zero code to control the opening and closing of the backpack
- 3. Implement dragging of items
- 4. Drag and drop items
- 5. Define item attributes
- 6. Find an empty spot in the inventory
- 7. Determination of full inventory
- 8. Item quantity display
- 9. Item stacking
- 10. Shortcut bar item selection
- 11. Select tool functions
- 12. Delete items using items
- 3. Integration of construction system and inventory system
- final effect
- Source code
- end
Preface
This article implements a demo of a Tyrelia-like game, which mainly includes the classic inventory system and construction system.
Note: The article is mainly divided into three parts: construction system, inventory system and combination of construction system and inventory system. The construction system and inventory system are implemented independently of each other and can be extracted and used separately.
Let’s take a look at the final effect first
material
figure
https://assetstore.unity.com/packages/2d/characters/warrior-free-asset-195707
tiles
https://assetstore.unity.com/packages/2d/environments/platform-tile-pack-204101
other
1. Construction system
1. Define item categories
Game item base class
using UnityEngine;
using UnityEngine.Tilemaps;
// 创建一个 ScriptableObject,用于表示游戏物品
[CreateAssetMenu(menuName = "GameObject/Item")]
public class Item : ScriptableObject
{
public TileBase tile; // 物品对应的瓦片
public Sprite image; // 物品的图像
public ItemType type; // 物品的类型
public ActionType actionType; // 物品的动作类型
public Vector2Int range = new Vector2Int(5, 4); // 物品的范围,默认为 5x4
}
// 定义枚举类型 ItemType,表示物品的类型
public enum ItemType
{
BuildingBlock, // 建筑块物品类型
Tool // 工具物品类型
}
// 定义枚举类型 ActionType,表示动作的类型
public enum ActionType
{
Dig, // 挖掘动作类型
Mine // 开采动作类型
}
using UnityEngine;
// 创建一个继承自 RuleTile 的自定义规则瓦片
[CreateAssetMenu(menuName = "Tiles/Custom Rule Tile")]
public class RuleTileWithData : RuleTile
{
public Item item; // 规则瓦片对应的物品数据
}
ps: The significance of RuleTileWithData is to extend Unity's own RuleTile class, allowing us to associate additional item data (Item) in rule tiles. The advantage of this is that we can directly obtain the item information associated with a specific tile in a map using regular tiles, without the need for additional lookups or maintenance of data structures.
Add game items and RuleTileWithData
Mining is the same
2. Draw a map
Simply draw a map
3. Implement tile selection effect
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class BuildingSystem : MonoBehaviour
{
[SerializeField] private Item item; // 当前选中的物品
[SerializeField] private TileBase highlightTile; // 高亮显示瓦片所使用的 TileBase
[SerializeField] private Tilemap mainTilemap; // 主要的地图瓦片对象
[SerializeField] private Tilemap tempTilemap; // 临时地图瓦片对象,用于显示高亮瓦片
private Vector3Int highlightedTilePos; // 高亮显示的瓦片在网格中的位置
private bool highlighted; // 是否在高亮显示
private void Update()
{
// 如果当前有选中的物品,则在 Update 方法中更新高亮显示
if (item != null)
{
HighlightTile(item);
}
}
private Vector3Int GetMouseOnGridPos()
{
// 获取鼠标在当前网格中的位置
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3Int mouseCellPos = mainTilemap.WorldToCell(mousePos);
mouseCellPos.z = 0;
return mouseCellPos;
}
private void HighlightTile(Item currentItem)
{
// 获取鼠标在当前网格中的位置
Vector3Int mouseGridPos = GetMouseOnGridPos();
// 如果当前高亮显示的瓦片位置不等于鼠标位置,则更新高亮显示
if (highlightedTilePos != mouseGridPos)
{
// 清除之前高亮显示的瓦片
tempTilemap.SetTile(highlightedTilePos, null);
// 获取当前位置的瓦片,并在临时地图瓦片对象上高亮显示
TileBase tile = mainTilemap.GetTile(mouseGridPos);
if (tile)
{
tempTilemap.SetTile(mouseGridPos, highlightTile);
highlightedTilePos = mouseGridPos;
highlighted = true;
}
else
{
highlighted = false;
}
}
}
}
Main Tilemap draws the map, Temp Tilemap is used to display the check box
Mount script, configuration parameters
Effect
4. Limit tile selection
According to the range in the Item, the control tile can only be selected in a certain area of the character.
ModifyBuildingSystem
private Vector3Int playerPos; //玩家位置
//。。。
private void Update()
{
playerPos = mainTilemap.WorldToCell(transform.position);
// 如果当前有选中的物品,则在 Update 方法中更新高亮显示
if (item != null)
{
HighlightTile(item);
}
}
private void HighlightTile(Item currentItem)
{
// 获取鼠标在当前网格中的位置
Vector3Int mouseGridPos = GetMouseOnGridPos();
// 如果当前高亮显示的瓦片位置不等于鼠标位置,则更新高亮显示
if (highlightedTilePos != mouseGridPos)
{
// 清除之前高亮显示的瓦片
tempTilemap.SetTile(highlightedTilePos, null);
// 检查鼠标位置与玩家位置是否在范围内
if (InRange(playerPos, mouseGridPos, (Vector3Int)currentItem.range))
{
// 获取鼠标位置上的瓦片,并检查条件 GetTile获取指定坐标格子瓦片
if (CheckCondition(mainTilemap.GetTile<RuleTileWithData>(mouseGridPos), currentItem))
{
// 在临时地图瓦片对象上高亮显示瓦片
tempTilemap.SetTile(mouseGridPos, highlightTile);
highlightedTilePos = mouseGridPos;
highlighted = true;
}
else
{
highlighted = false;
}
}
else
{
highlighted = false;
}
}
}
//判断鼠标位置与玩家位置是否在范围内
private bool InRange(Vector3Int positionA, Vector3Int positionB, Vector3Int range)
{
// 判断两个位置之间的距离是否在范围内
Vector3Int distance = positionA - positionB;
if (Math.Abs(distance.x) >= range.x || Math.Abs(distance.y) >= range.y)
{
return false;
}
return true;
}
//检查瓦片与当前物品的条件是否匹配
private bool CheckCondition(RuleTileWithData tile, Item currentItem)
{
// 检查瓦片与当前物品的条件是否匹配
if (currentItem.type == ItemType.BuildingBlock)
{
if (!tile)
{
return true;
}
}
else if (currentItem.type == ItemType.Tool)
{
if (tile)
{
if (tile.item.actionType == currentItem.actionType)
{
return true;
}
}
}
return false;
}
Effect
5. Place items function
BuildingSystem new features
private void Update()
{
if (Input.GetMouseButtonDown(0))// 当玩家按下左键时
{
if (highlighted)
{
if (item.type == ItemType.BuildingBlock)// 如果当前选中的物品是建筑方块
{
Build(highlightedTilePos, item);// 放置方块
}
}
}
}
// 放置方块
private void Build(Vector3Int position, Item itemToBuild)
{
tempTilemap.SetTile(position, null);// 清除临时 Tilemap 上的方块
highlighted = false;// 取消高亮状态
mainTilemap.SetTile(position, itemToBuild.tile);// 在主 Tilemap 上放置方块
}
For testing, first change the item type to BuildingBlock
Effect
6. Clear items
private void Update()
{
if (Input.GetMouseButtonDown(0))// 当玩家按下左键时
{
if (highlighted)
{
if (item.type == ItemType.BuildingBlock)// 如果当前选中的物品是建筑方块
{
Build(highlightedTilePos, item);// 放置方块
}
else if (item.type == ItemType.Tool)// 如果当前选中的物品是工具
{
Destroy(highlightedTilePos);// 移除方块
}
}
}
}
// 移除方块以及生成相应物品
private void Destroy(Vector3Int position)
{
tempTilemap.SetTile(position, null);// 清除临时 Tilemap 上的方块
highlighted = false;// 取消高亮状态
RuleTileWithData tile = mainTilemap.GetTile<RuleTileWithData>(position);// 获取当前位置上的方块数据
mainTilemap.SetTile(position, null);// 在主 Tilemap 上移除方块
}
For testing, first change the item type to Tool
Effect
7. Generate and pick up items function
Added Loot prefab for displaying items
Added script Loot
using System.Collections;
using UnityEngine;
public class Loot : MonoBehaviour
{
[SerializeField] private SpriteRenderer sr; // 用于显示物品图像的组件
[SerializeField] private new BoxCollider2D collider; // 触发器组件
[SerializeField] private float moveSpeed; // 拾取时的移动速度
private Item item; // 表示此物品的数据模型
// 初始化物品
public void Initialize(Item item)
{
this.item = item;
sr.sprite = item.image; // 显示物品图像
}
// 当进入触发器时执行的逻辑
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
StartCoroutine(MoveAndCollect(other.transform)); // 开始移动并拾取物品
}
}
// 移动并拾取物品的逻辑
private IEnumerator MoveAndCollect(Transform target)
{
Destroy(collider); // 拾取后销毁触发器
while (transform.position != target.position)
{
transform.position = Vector3.MoveTowards(transform.position, target.position, moveSpeed * Time.deltaTime); // 向目标移动
yield return 0;
}
Destroy(gameObject); // 拾取完成后销毁物品对象
}
}
Mount the script and configure parameters
Modify BuildingSystem to generate items
[SerializeField] private GameObject lootPrefab;// 拾取物品时生成的对象
// 移除方块以及生成相应物品
private void Destroy(Vector3Int position)
{
//。。。
Vector3 pos = mainTilemap.GetCellCenterWorld(position);// 获取方块中心的世界坐标
GameObject loot = Instantiate(lootPrefab, pos, Quaternion.identity);// 创建拾取物品
loot.GetComponent<Loot>().Initialize(tile.item);// 初始化拾取物品数据
}
Remember to mount the prefabricated body and modify the Player tag to
run the effect
. For better effects, you can remove the direct collision of items and reduce the size of the generated object
.
2. Inventory system
1. Simple drawing UI
I won’t go into detail about UI drawing here to save everyone’s time. I’ve talked about it many times in previous articles. If you don’t understand, you can read my previous articles.
Hierarchy
Effect
2. Zero code to control the opening and closing of the backpack
Click on the backpack to display the backpack and hide the button.
Click on the background to hide the backpack and turn on the button.
Effect
3. Implement dragging of items
Drag and drop function
Add an item prefab to the item slot subset and mount the new script InventoryItem
InventoryItem script
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class InventoryItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("UI")]
[HideInInspector] public Image image; // 物品的图像组件
[HideInInspector] public Transform parentAfterDrag; // 记录拖拽前的父级位置
//开始拖拽时调用
public void OnBeginDrag(PointerEventData eventData)
{
image = transform.GetComponent<Image>();
image.raycastTarget = false; // 禁用射线检测,防止拖拽物体遮挡其他UI元素的交互
parentAfterDrag = transform.parent; // 记录拖拽前的父级位置
//transform.root 是用来获取当前对象的根物体,这里及是Canvas
transform.SetParent(transform.root); // 设置拖拽物体的父级为Canvas,以保证拖拽物体在最上层显示
}
//拖拽过程中调用
public void OnDrag(PointerEventData eventData)
{
transform.position = Input.mousePosition; // 将拖拽物体的位置设置为鼠标的当前位置
}
//拖拽结束时调用
public void OnEndDrag(PointerEventData eventData)
{
image.raycastTarget = true; // 启用射线检测
transform.SetParent(parentAfterDrag); // 恢复拖拽结束后物品的父级位置
}
}
Effect
Drag and drop recovery problem
You will find that the item does not return to its original position after dragging, even if we have restored the parent position of the item after dragging.
This is because we have not restored the position of the item. Here we can add a Grid Layout Group component to the parent of the item, that is, the item slot, to forcefully define the layout position of the child object. Running
effect
4. Drag and drop items
Added InventorySlot script, mounted in the item slot
using UnityEngine;
using UnityEngine.EventSystems;
public class InventorySlot : MonoBehaviour, IDropHandler
{
// 在拖拽物体放置在目标对象上时被调用
public void OnDrop(PointerEventData eventData)
{
//检查背包槽是否没有子物体(即没有物品),只有背包槽为空才能放置物品。
if (transform.childCount == 0)
{
//从拖拽事件的 pointerDrag 对象中获取拖拽的物品
InventoryItem inventoryItem = eventData.pointerDrag.GetComponent<InventoryItem>();
inventoryItem.parentAfterDrag = transform;
}
}
}
Effect
5. Define item attributes
using UnityEngine;
using UnityEngine.Tilemaps;
// 创建一个 ScriptableObject,用于表示游戏物品
[CreateAssetMenu(menuName = "GameObject/Item")]
public class Item : ScriptableObject
{
[Header("游戏内")]
public TileBase tile; // 物品对应的瓦片
public ItemType type; // 物品的类型
public ActionType actionType; // 物品的动作类型
public Vector2Int range = new Vector2Int(5, 4); // 物品的范围,默认为 5x4
[Header("UI内")]
public bool stackable = true;//是否可叠起堆放的,默认是
[Header("两者")]
public Sprite image; // 物品的图像
}
// 定义枚举类型 ItemType,表示物品的类型
public enum ItemType
{
BuildingBlock, // 建筑块物品类型
Tool // 工具物品类型
}
// 定义枚举类型 ActionType,表示动作的类型
public enum ActionType
{
Dig, // 挖掘动作类型
Mine // 开采动作类型
}
Create several different items,
modify InventoryItem, and initialize different props
public Item item;
private void Start()
{
image = transform.GetComponent<Image>();
InitialiseItem(item);
}
public void InitialiseItem(Item newItem)
{
image.sprite = newItem.image;
}
For testing purposes, we configure several different item
effects
6. Find an empty spot in the inventory
In actual use, we definitely cannot configure different items through mounting, so we make modifications, wait for subsequent use, and hide the items.
[HideInInspector] public Image image; // 物品的图像组件
[HideInInspector] public Item item;
[HideInInspector] public Transform parentAfterDrag; // 记录拖拽前的父级位置
private void Start()
{
image = transform.GetComponent<Image>();
}
public void InitialiseItem(Item newItem)
{
item = newItem;
image.sprite = newItem.image;
}
Added InventoryManager code to find free locations in the inventory and add items
using UnityEngine;
public class InventoryManager : MonoBehaviour
{
public InventorySlot[] inventorySlots; // 背包槽数组
public GameObject inventoryItemPrefab; // 物品预制体
private void Start()
{
//判断inventorySlots是否为空
if (inventorySlots.Length <= 0)
{
Debug.Log("背包槽数组没有配置,请先配置好!");
return;
}
}
// 添加物品到背包
public void AddItem(Item item)
{
for (int i = 0; i < inventorySlots.Length; i++)
{
InventorySlot slot = inventorySlots[i]; // 获取当前遍历的背包槽
InventoryItem itemInSlot = slot.GetComponentInChildren<InventoryItem>(); // 在背包槽中查找是否已经存在物品
if (itemInSlot == null) // 如果背包槽内没有物品
{
SpawnNewItem(item, slot); // 生成新的物品到这个背包槽中
return;
}
}
}
// 生成新的物品到背包槽中
void SpawnNewItem(Item item, InventorySlot slot)
{
GameObject newItemGo = Instantiate(inventoryItemPrefab, slot.transform); // 实例化物品预制体并设置父级为当前的背包槽
InventoryItem inventoryItem = newItemGo.GetComponent<InventoryItem>(); // 获取生成物品的 InventoryItem 组件
inventoryItem.InitialiseItem(item); // 初始化物品信息
}
}
Added InventoryManager empty node, mounting script, binding and mounting all item slots
Added Test test script for testing the function of adding items
using UnityEngine;
public class Test : MonoBehaviour
{
public InventoryManager inventoryManager;
public Item[] itemsToPickup;
public void PickupItem(int id)
{
inventoryManager.AddItem(itemsToPickup[id]);
}
}
Add a new test panel to mount the test script, and add several new buttons to test
the effect
7. Determination of full inventory
There is a problem before. If our inventory is full, the picked items will disappear. At this time, we need to modify the AddItem method of InventoryManager to return to the state of adding items.
// 添加物品到背包
public bool AddItem(Item item)
{
for (int i = 0; i < inventorySlots.Length; i++)
{
InventorySlot slot = inventorySlots[i]; // 获取当前遍历的背包槽
InventoryItem itemInSlot = slot.GetComponentInChildren<InventoryItem>(); // 在背包槽中查找是否已经存在物品
if (itemInSlot == null) // 如果背包槽内没有物品
{
SpawnNewItem(item, slot); // 生成新的物品到这个背包槽中
return true;
}
}
return false;
}
Modify the Test code synchronously and determine whether the item is added successfully based on the return value.
public void PickupItem(int id)
{
bool result = inventoryManager.AddItem(itemsToPickup[id]);
if (result == true)
{
Debug.Log("添加物品");
}
else
{
Debug.Log("添加物品失败,库存已满");
}
}
Effect
8. Item quantity display
Add a Text text to the subset of items to display the number of items, and add the Canvas Group component. Set the blocksRaycasts property of this component to false, which means that during the entire process when we first start dragging, the mouse will not move again. Treat this UI item as a blocker, including all UI objects of its sub-objects
And modify the InventoryItem item script
[HideInInspector] public GameObject countText; // 数量文本
[HideInInspector] public int count = 1; //默认数量
public void InitialiseItem(Item newItem)
{
countText = transform.GetChild(0).gameObject;
item = newItem;
image.sprite = newItem.image;
RefreshCount();
}
public void RefreshCount()
{
countText.GetComponent<TextMeshProUGUI>().text = count.ToString();
}
Effect
If the calculation is 1, we can choose to hide the quantity display, so the effect will be better
public void RefreshCount()
{
countText.GetComponent<TextMeshProUGUI>().text = count.ToString();
//控制数量显示隐藏 大于1才显示
bool textActive = count > 1;
countText.gameObject.SetActive(textActive);
}
Effect
Randomly add quantity, test
public void InitialiseItem(Item newItem)
{
countText = transform.GetChild(0).gameObject;
item = newItem;
image.sprite = newItem.image;
count = Random.Range(1, 4);//随机添加物品数量测试
RefreshCount();
}
Effect
9. Item stacking
Modify InventoryManager
public int maxStackedItems = 4; //最大堆叠数量,默认4
// 添加物品到背包
public bool AddItem(Item item)
{
for (int i = 0; i < inventorySlots.Length; i++)
{
InventorySlot slot = inventorySlots[i]; // 获取当前遍历的背包槽
InventoryItem itemInSlot = slot.GetComponentInChildren<InventoryItem>(); // 在背包槽中查找是否已经存在物品
if (itemInSlot == null) // 如果背包槽内没有物品
{
SpawnNewItem(item, slot); // 生成新的物品到这个背包槽中
return true;
}
else if (itemInSlot.item == item && itemInSlot.count < maxStackedItems && itemInSlot.item.stackable == true)
{
itemInSlot.count++;//添加数量
itemInSlot.RefreshCount();
return true;
}
}
return false;
}
Effect
10. Shortcut bar item selection
We provide selected visual effects by modifying the background color of the selected items.
Modify the InventorySlot code.
private Image image;
public Color selectedColor, notSelectedColor;
private void Awake()
{
image = GetComponent<Image>();
Deselect();// 初始化时取消选中
}
//选择该槽位颜色修改
public void Select()
{
image.color = selectedColor;
}
//取消选择该槽位颜色修改
public void Deselect()
{
image.color = notSelectedColor;
}
Modify InventoryManager
int selectedSlot = -1;
private void Start()
{
ChangeSelectedSlot(0);//默认选中第一个槽
}
void ChangeSelectedSlot(int newValue)
{
if (selectedSlot >= 0)
{
inventorySlots[selectedSlot].Deselect();// 取消之前选中的槽位
}
inventorySlots[newValue].Select();// 选择新的槽位
selectedSlot = newValue;// 更新选中的槽位索引
}
Effect
1-6 keyboard numbers to achieve switching
Modify InventoryManager code
private void Update(){
if (Input.GetKeyDown (KeyCode.Alpha1))
ChangeSelectedSlot(0);
else if (Input.GetKeyDown(KeyCode.Alpha2))
ChangeSelectedSlot(1);
else if (Input.GetKeyDown(KeyCode.Alpha3))
ChangeSelectedSlot(2);
else if (Input.GetKeyDown(KeyCode.Alpha4))
ChangeSelectedSlot(3);
else if (Input.GetKeyDown(KeyCode.Alpha5))
ChangeSelectedSlot(4);
else if (Input.GetKeyDown(KeyCode.Alpha6))
ChangeSelectedSlot(5);
else if (Input.GetKeyDown(KeyCode.Alpha7))
ChangeSelectedSlot(6);
}
Optimize code
private void Update()
{
if (Input.inputString != null)
{
bool isNumber = int.TryParse(Input.inputString, out int number);
if (isNumber & number > 0 & number < 8) ChangeSelectedSlot(number - 1);
}
}
Effect
11. Select tool functions
InventoryManager adds a new method for selecting items
// 获取当前选中物品
public Item GetSelectedItem(){
if (inventorySlots.Length > 0)
{
InventorySlot slot = inventorySlots[selectedSlot];// 获取当前选中槽位
InventoryItem itemInSlot = slot.GetComponentInChildren<InventoryItem>();// 获取槽位上的物品
if (itemInSlot != null) return itemInSlot.item;// 如果有物品,则返回物品
}
return null;//如果没有选中物品则返回null
}
Test printing in Test script
//获取当前选中物品并打印输出
public void GetSelectedItem()
{
Item receivedItem = inventoryManager.GetSelectedItem();//获取当前选中物品
if (receivedItem != null)
{
Debug.Log("选中物品:" + receivedItem);
}
else
{
Debug.Log("没有选中物品!");
}
}
Add button test
12. Delete items using items
Modify the InventoryManagerGetselectedItem method
// 获取当前选中物品
public Item GetSelectedItem(bool use)
{
if (inventorySlots.Length > 0){
InventorySlot slot = inventorySlots[selectedSlot];// 获取当前选中槽位
InventoryItem itemInSlot = slot.GetComponentInChildren<InventoryItem>();// 获取槽位上的物品
if (itemInSlot != null)
{
Item item = itemInSlot.item;
//是否使用物品
if (use == true)
{
itemInSlot.count--;//减少库存
if (itemInSlot.count <= 0)
{
Destroy(itemInSlot.gameObject);//删除物品
}
else
{
itemInSlot.RefreshCount();//更新数量文本显示
}
}
return item;
}
}
return null;//如果没有选中物品则返回null
}
Test new method test
//使用物品测试
public void UseSelectedItem()
{
Item receivedItem = inventoryManager.GetSelectedItem(true);//获取当前使用的物品
if (receivedItem != null)
{
Debug.Log("使用物品:" + receivedItem);
}
else
{
Debug.Log("没有可使用的物品!");
}
}
Effect
3. Integration of construction system and inventory system
Move all the UI of the inventory system into the construction system and reset the item slot information
Modify the InventoryManager so that when the configuration starts, the item information of the item is displayed by default.
public Item[] startItems; //默认物品列表
private void Start()
{
ChangeSelectedSlot(0);//默认选中第一个槽
foreach (var item in startItems){
AddItem(item);
}
}
Here I configure two tools by default
Modify InventoryManager to be a singleton to facilitate calling from other places
public static InventoryManager instance;
void Awake(){
instance = this;
}
Modify the BuildingSystem to obtain the currently selected items
// [SerializeField] private Item item; // 当前选中的物品
private void Update()
{
Item item = InventoryManager.instance.GetSelectedItem(false);
}
Collect items and modify Loot code
// 当进入触发器时执行的逻辑
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
bool canAdd = InventoryManager.instance.AddItem(item);
if (canAdd)
{
StartCoroutine(MoveAndCollect(other.transform));// 开始移动并拾取物品
}
}
}
Use reduced items and modify the BuildingSystem code
// 放置方块
private void Build(Vector3Int position, Item itemToBuild)
{
InventoryManager.instance.GetSelectedItem(true);
tempTilemap.SetTile(position, null);// 清除临时 Tilemap 上的方块
highlighted = false;// 取消高亮状态
mainTilemap.SetTile(position, itemToBuild.tile);// 在主 Tilemap 上放置方块
}
final effect
Source code
I'll put it up after I've sorted it out.
end
Gifts of roses, hand a fragrance! If the content of the article is helpful to you, please don't be stingy with your 点赞评论和关注
feedback so that I can receive feedback as soon as possible. Your every feedback 支持
is the biggest motivation for me to continue creating. The more likes, the faster the updates will be! Of course, if you find 存在错误
something in the article 更好的解决方法
, please feel free to comment and send me a private message!
Okay, I am 向宇
, https://xiangyu.blog.csdn.net
A developer who has been working quietly in a small company recently started studying Unity by himself out of interest. If you encounter any problems, you are also welcome to comment and send me a private message. Although I may not necessarily know some of the problems, I will check the information from all parties and try to give the best suggestions. I hope to help more people who want to learn programming. People, encourage each other~