[Unity Practical Combat] Empuja manualmente un sistema de inventario, muy adecuado para juegos como RPG, Roguelike y Stardew Valley

Prefacio

De hecho, he hecho el sistema de inventario de mochilas muchas veces antes, hay innumerables formas de implementar el sistema de mochila y todos los continentes conducen a Roma. Porque el sistema de mochila es muy común y crucial en los juegos. Creo que las nuevas investigaciones sobre implementación siempre nos brindarán diferentes inspiraciones, por lo que siempre prestaré atención a las diferentes formas de implementarlas y espero que usted también lo haga.

Si estás interesado en el sistema de mochila implementado anteriormente, puedes echarle un vistazo a:
Una réplica del juego de supervivencia y construcción Terraria, que incluye un sistema de construcción y un sistema de inventario. Desde
cero, puedes crear un sistema de mochila de inventario.
Desde cero , puedes crear un sistema de mochila.

Primero echemos un vistazo al efecto final.
Insertar descripción de la imagen aquí

material

https://cupnooble.itch.io
Insertar descripción de la imagen aquí

comenzar

Configurar información diferente del artículo

Agregar script al menú de elementos personalizados

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

namespace InventorySystem
{
    
    
    [CreateAssetMenu(menuName = "库存/物品定义", fileName = "新物品定义")]
    public class ItemDefinition : ScriptableObject
    {
    
    
        [SerializeField]
        private string _name;  // 物品名称
        [SerializeField]
        private bool _isStackable;  // 是否可堆叠
        [SerializeField]
        private Sprite _inGameSprite;  // 游戏内显示的精灵图像
        [SerializeField]
        private Sprite _uiSprite;  // UI界面显示的精灵图像

        public string Name => _name;  // 获取物品名称
        public bool IsStackable => _isStackable;  // 获取是否可堆叠
        public Sprite InGameSprite => _inGameSprite;  // 获取游戏内显示的精灵图像
        public Sprite UiSprite => _uiSprite;  // 获取UI界面显示的精灵图像
    }
}

Crea varios elementos diferentes.
Insertar descripción de la imagen aquí

Crear una instancia de un elemento

Pila de elementos, limite la cantidad de elementos establecidos

using System;
using UnityEngine;

namespace InventorySystem
{
    
    
    [Serializable]
    public class ItemStack
    {
    
    
        [SerializeField]
        private ItemDefinition _item;  // 物品定义
        [SerializeField]
        private int _numberOfItems;  // 物品数量

        public bool IsStackable => _item.IsStackable;  // 是否可堆叠
        public ItemDefinition Item => _item;  // 物品定义
        public int NumberOfItems
        {
    
    
            get => _numberOfItems;
            set
            {
    
    
                value = value < 0 ? 0 : value;  // 确保物品数量不小于零
                _numberOfItems = IsStackable ? value : 1;   // 如果不可堆叠,将物品数量限制在 1 以内
            }
        }

		//构造方法
        public ItemStack(ItemDefinition item, int numberOfItems)
        {
    
    
            _item = item;
            NumberOfItems = numberOfItems;
        }
    }
}

Guión de montaje del artículo

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

namespace InventorySystem
{
    
    
    public class GameItem : MonoBehaviour
    {
    
    
        [SerializeField]
        private ItemStack _stack;  // 物品堆栈
        private SpriteRenderer _spriteRenderer;  // 精灵渲染器

        // 当对象检验时被调用,只要程序员在这个对象上修改了一个字段,Unity就会自动地调用这个函数。
        private void OnValidate()
        {
    
    
            SetupGameObject();  // 设置游戏对象
        }

        // 设置游戏对象
        private void SetupGameObject()
        {
    
    
            if (_stack.Item == null) return;  // 如果物品为空,返回
            SetGameSprite();  // 设置游戏精灵
            AdjustNumberOfItems();  // 调整物品数量
            UpdateGameObjectName();  // 更新游戏对象名称
        }

        // 设置游戏精灵
        private void SetGameSprite()
        {
    
    
            _spriteRenderer = GetComponent<SpriteRenderer>();
            _spriteRenderer.sprite = _stack.Item.InGameSprite;
        }

        // 更新游戏对象名称
        private void UpdateGameObjectName()
        {
    
    
            var name = _stack.Item.Name;
            var number = _stack.IsStackable ? _stack.NumberOfItems.ToString() : "ns";  // 判断物品是否可堆叠
            gameObject.name = $"{
      
      name} ({
      
      number})";  // 修改游戏对象名称
        }

        // 调整物品数量
        private void AdjustNumberOfItems()
        {
    
    
            _stack.NumberOfItems = _stack.NumberOfItems;
        }
    }
}

Montar
Insertar descripción de la imagen aquí
efecto de script
Insertar descripción de la imagen aquí

Recoger artículos

Modifique GameItem y agregue un nuevo método de selección

//拾取物品方法
public ItemStack Pick()
{
    
    
    Destroy(gameObject);
    return _stack;
}

Script de detección de colisiones de montaje de caracteres

namespace InventorySystem
{
    
    
    public class ItemCollisionHandler : MonoBehaviour
    {
    
    
        private void OnTriggerEnter2D(Collider2D col)
        {
    
    
            // 检测碰撞的对象是否拥有 GameItem 组件
            var gameItem = col.GetComponent<GameItem>();
            if (gameItem == null) return;
            gameItem.Pick();
        }
    }
}

Efecto
Insertar descripción de la imagen aquí

Inventario, tamaño del inventario

Ranura de inventario

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

namespace InventorySystem
{
    
    
    [System.Serializable]
    public class InventorySlot
    {
    
    
        // 物品槽状态改变事件
        public event EventHandler<(ItemStack,bool)> StatChanged;

        public ItemDefinition Item{
    
    
            get => _state.Item;
        }
        // 物品堆栈状态
        [SerializeField]
        private ItemStack _state;

        // 物品槽是否处于激活状态
        private bool _active;

        // 物品堆栈状态属性
        public ItemStack State
        {
    
    
            get => _state;
            set
            {
    
    
                _state = value;
                NotifyAboutStateChange();
            }
        }

        // 物品槽激活状态属性
        public bool Active
        {
    
    
            get => _active;
            set
            {
    
    
                _active = value;
                NotifyAboutStateChange();
            }
        }

        // 物品数量属性
        public int NumberOfItems
        {
    
    
            get => _state.NumberOfItems;
            set
            {
    
    
                _state.NumberOfItems = value;
                NotifyAboutStateChange();
            }
        }

        // 判断物品槽是否有物品(即物品堆栈中的物品不为空)
        public bool HasItem => _state?.Item != null;

        // 清空物品槽
        public void Clear()
        {
    
    
            State = null;
        }

        // 通知物品槽状态改变
        private void NotifyAboutStateChange()
        {
    
    
            StatChanged?.Invoke(this, (_state, _active));
        }
    }
}
namespace InventorySystem
{
    
    
    public class Inventory : MonoBehaviour
    {
    
    
        [SerializeField]
        private int _size = 8; // 背包大小
        [SerializeField]
        private List<InventorySlot> _slots; // 物品槽列表

        private void OnValidate()
        {
    
    
            AdjustSize(); // 检查并调整背包大小
        }

        /// <summary>
        /// 检查并调整背包大小
        /// </summary>
        private void AdjustSize()
        {
    
    
            _slots ??= new List<InventorySlot>(); // 如果物品槽列表为空,则初始化为一个空的列表
            if (_slots.Count > _size) // 如果物品槽数量大于背包大小
                _slots.RemoveRange(_size, _slots.Count - _size); // 移除多余的物品槽
            if (_slots.Count < _size) // 如果物品槽数量小于背包大小
                _slots.AddRange(new InventorySlot[_size - _slots.Count]); // 添加空的物品槽,直到数量达到背包大小
        }
    }
}

Efecto
Insertar descripción de la imagen aquí

Encontrar el inventario y poder agregar artículos.

Modificar código de inventario

/// <summary>
/// 判断背包是否已满
/// </summary>
public bool IsFull()
{
    
    
    return _slots.Count(slot => slot.HasItem) >= _size; // 如果已占用的物品槽数量大于等于背包大小,则背包已满
}

/// <summary>
/// 判断是否可以接收物品堆叠
/// </summary>
/// <param name="itemStack">物品堆叠</param>
/// <returns>如果可以接收,则返回true;否则返回false</returns>
public bool CanAcceptItem(ItemStack itemStack)
{
    
    
    var slotWithStackableItem = FindSlot(itemStack.Item, onlyStackable: true); // 查找可以堆叠的物品槽
    return !IsFull() || slotWithStackableItem != null; // 如果背包未满或找到了一个可以堆叠的物品槽,就可以接收该物品
}

/// <summary>
/// 查找具有指定物品的物品槽
/// </summary>
/// <param name="item">物品定义</param>
/// <param name="onlyStackable">是否只查找可堆叠的物品槽</param>
/// <returns>如果找到,返回具有指定物品的物品槽;否则返回null</returns>
private InventorySlot FindSlot(ItemDefinition item, bool onlyStackable = false)
{
    
    
    // 遍历物品槽列表,找到第一个物品槽的Item属性等于指定物品的Item属性,并且该物品符合是否只查找可堆叠的物品槽的要求
    return _slots.FirstOrDefault(slot => slot.Item == item && (item.IsStackable || !onlyStackable));
}

/// <summary>
/// 向背包中添加物品堆叠
/// </summary>
/// <param name="itemStack">物品堆叠</param>
/// <returns>实际添加到背包中的物品堆叠</returns>
public ItemStack AddItem(ItemStack itemStack)
{
    
    
    var relevantSlot = FindSlot(itemStack.Item, true); // 查找具有相同物品的物品槽
    if (IsFull() && relevantSlot == null) // 如果背包已满并且无法找到可以堆叠的物品槽,就抛出异常
    {
    
    
        throw new InventoryException(InventoryOperation.Add, "Inventory is full");
    }
    if (relevantSlot != null) // 如果找到了可以堆叠的物品槽
    {
    
    
        relevantSlot.NumberOfItems += itemStack.NumberOfItems; // 合并物品堆叠数量
    }
    else // 如果没有找到可以堆叠的物品槽
    {
    
    
        relevantSlot = _slots.First(slot => !slot.HasItem); // 找到第一个空的物品槽
        relevantSlot.State = itemStack; // 设置该物品槽的物品堆叠为指定的物品堆叠
    }
    return relevantSlot.State; // 返回实际添加到背包中的物品堆叠
}

Clase de excepción de mochilaInventoryException

using System;

namespace InventorySystem
{
    
    
    // 背包操作枚举类型
    public enum InventoryOperation
    {
    
    
        Add,// 添加物品
        Remove// 移除物品
    }

    // 背包异常类
    public class InventoryException : Exception
    {
    
    
        public InventoryOperation Operation {
    
     get; }// 引发异常时的背包操作
        public InventoryException(InventoryOperation operation, string msg) : base($"{
      
      operation} Error: {
      
      msg}")// 构造函数
        {
    
    
            Operation = operation;
        }
    }
}

Modificar el código ItemCollisionHandler

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
    
    
    public class ItemCollisionHandler : MonoBehaviour
    {
    
    
        private Inventory _inventory;
        private void Awake()
        {
    
    
            _inventory = GetComponentInParent<Inventory>();
        }
        private void OnTriggerEnter2D(Collider2D col)
        {
    
    
            // 检测碰撞的对象是否拥有 GameItem 组件
            var gameItem = col.GetComponent<GameItem>();
            if (gameItem == null) return;
            _inventory.AddItem(gameItem.Pick());
        }

    }
}

Efecto
Insertar descripción de la imagen aquí

Problema de inventario completo resuelto

Modificar elemento de juego

public ItemStack Stack =>_stack;

Revisar

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
    
    
    public class ItemCollisionHandler : MonoBehaviour
    {
    
    
        private Inventory _inventory;
        private void Awake()
        {
    
    
            _inventory = GetComponentInParent<Inventory>();
        }
        private void OnTriggerEnter2D(Collider2D col)
        {
    
    
            // 检测碰撞的对象是否拥有 GameItem 组件
            var gameItem = col.GetComponent<GameItem>();
            if (gameItem == null || !_inventory.CanAcceptItem(gameItem.Stack)) return;
            _inventory.AddItem(gameItem.Pick());
        }

    }
}

Script de interfaz de usuario de inventario

Dibujar ranura ui
Insertar descripción de la imagen aquí

Grosor y contorno del texto
Insertar descripción de la imagen aquí
Se agregó el script UI_InventorySlot

public class UI_InventorySlot : MonoBehaviour
{
    
    
}

Se agregó el script UI_Inventory

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

namespace InventorySystem.UI
{
    
    
    // UI物品栏界面类
    public class UI_Inventory : MonoBehaviour
    {
    
    
        // 物品槽预制体
        [SerializeField]
        private GameObject _inventorySlotPrefab;
        // 对应的物品栏
        [SerializeField]
        private Inventory _inventory;
        // 物品槽列表
        [SerializeField]
        private List<UI_InventorySlot> _slots;

        // 物品栏属性
        public Inventory Inventory => _inventory;

        // 初始化物品栏UI
        [ContextMenu("初始化库存")]
        private void InitializeInventoryUi()
        {
    
    
            if (_inventory == null || _inventorySlotPrefab == null)
                return;

            _slots = new List<UI_InventorySlot>(_inventory.Size);
            for (var i = 0; i < _inventory.Size; i++)
            {
    
    
                // 实例化物品槽预制体
                var uiSlot = Instantiate(_inventorySlotPrefab, transform);
                var uiSlotScript = uiSlot.GetComponent<UI_InventorySlot>();

                // 将实例化生成的物品槽脚本添加到列表中
                _slots.Add(uiSlotScript);
            }
        }
    }
}

Modificar inventario

public int Size =>_size;

Efecto
Insertar descripción de la imagen aquí

Mostrar información del artículo

Modificar el script de ranura UI_InventorySlot

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

namespace InventorySystem.UI
{
    
    
    public class UI_InventorySlot : MonoBehaviour
    {
    
    
        // 物品栏
        [SerializeField]
        private Inventory _inventory;
        // 物品槽索引
        [SerializeField]
        private int _inventorySlotIndex;
        // 物品图标
        [SerializeField]
        private Image _itemIcon;
        // 激活指示器
        [SerializeField]
        private Image _activeIndicator;
        // 物品数量文本
        [SerializeField]
        private TMP_Text _numberOfItems;
        // 物品槽
        private InventorySlot _slot;

        private void Start()
        {
    
    
            AssignSlot(_inventorySlotIndex);
        }

        // 分配物品槽
        public void AssignSlot(int slotIndex)
        {
    
    
            // 取消之前的事件监听
            if (_slot != null) _slot.StateChanged -= OnStateChanged;

            // 更新物品槽索引
            _inventorySlotIndex = slotIndex;

            // 获取所属的物品栏
            if (_inventory == null) _inventory = GetComponentInParent<UI_Inventory>().Inventory;

            // 获取物品槽
            _slot = _inventory.Slots[_inventorySlotIndex];

            // 添加事件监听
            _slot.StateChanged += OnStateChanged;

            // 更新视图状态
            UpdateViewState(_slot.State, _slot.Active);
        }

        // 更新视图状态
        private void UpdateViewState(ItemStack state, bool active)
        {
    
    
            // 更新激活指示器
            _activeIndicator.enabled = active;

            var item = state?.Item;
            var hasItem = item != null;
            var isStackable = hasItem && item.IsStackable;

            // 更新物品图标显示
            _itemIcon.enabled = hasItem;

            // 更新物品数量文本显示
            _numberOfItems.enabled = isStackable;

            if (!hasItem) return;

            // 更新物品图标
            _itemIcon.sprite = item.UiSprite;

            if (isStackable)
            {
    
    
                // 更新物品数量文本
                _numberOfItems.SetText(state.NumberOfItems.ToString());
            }
        }

        // 物品槽状态改变事件处理方法
        // private void OnStateChanged(object sender, InventorySlotStateChangedArgs args)
        // {
    
    
        //     UpdateViewState(args.NewState, args.Active);
        // }
         private void OnStateChanged(object sender, (ItemStack, bool) e)
        {
    
    
            UpdateViewState(e.Item1,e.Item2);
        }
    }
}

UI_Inventario modificado

private void InitializeInventoryUi()
{
    
    
    //。。。
    for (var i = 0; i < _inventory.Size; i++)
    {
    
    
        // 。。。
        uiSlotScript.AssignSlot(i);
        // 。。。
    }
}

Modificar inventario

public List<InventorySlot> Slots => _slots;

Efecto
Insertar descripción de la imagen aquí

Indicador de interruptor

Se agregó el script InventoryInputHandler y lo montó en la tarea.

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

namespace InventorySystem
{
    
    
    public class InventoryInputHandler : MonoBehaviour
    {
    
    
        private Inventory _inventory;

        // 初始化_inventory变量
        private void Awake()
        {
    
    
            _inventory = GetComponent<Inventory>();
        }

        // 每帧检查是否响应按键
        void Update()
        {
    
    
            if (Input.GetKeyDown(KeyCode.Q))
            {
    
    
                OnPreviousItem(); // 选择上一项
            }
            if (Input.GetKeyDown(KeyCode.E))
            {
    
    
                OnNextItem(); // 选择上一项
            }
        }

        // 选择下一项物品
        private void OnNextItem()
        {
    
    
            _inventory.ActivateSlot(_inventory.ActiveSlotIndex + 1);
        }

        // 选择上一项物品
        private void OnPreviousItem()
        {
    
    
            _inventory.ActivateSlot(_inventory.ActiveSlotIndex - 1);
        }
    }
}

Modificar inventario

private void Awake()
{
    
    
    if (_size > 0)
    {
    
    
        _slots[0].Active = true;// 激活第一个物品槽
    }
}

public void ActivateSlot(int atIndex)
{
    
    
    ActiveSlotIndex = atIndex;
}

Efecto

Insertar descripción de la imagen aquí

descartar elementos

Modificar InventoryInputHandler

void Update()
{
    
    
    if (Input.GetKeyDown(KeyCode.G))
    {
    
     // 如果按下G键
        OnThrowItem(); // 丢弃物品
    }
}

// 丢弃当前物品
private void OnThrowItem()
{
    
    
    _inventory.RemoveItem(_inventory.ActiveSlotIndex, true);
}

Se agregó código GameItemSpawner para generar elementos y montarlos en personajes.

namespace InventorySystem
{
    
    
    public class GameItemSpawner : MonoBehaviour
    {
    
    
        [SerializeField]
        private GameObject _itemBasePrefab;

        // 生成物品
        public void SpawnItem(ItemStack itemStack)
        {
    
    
            // 如果物品基础预制体为空,直接返回
            if (_itemBasePrefab == null) return;

            Debug.Log(transform.position);

            // 实例化物品基础预制体,并将其设置为当前对象的子对象
            var item = Instantiate(_itemBasePrefab, transform.position, Quaternion.identity);
            item.transform.SetParent(null);

            // 获取物品的GameItem组件
            var gameItemScript = item.GetComponent<GameItem>();

            // 设置堆叠数量和物品定义到GameItem组件
            gameItemScript.SetStack(new ItemStack(itemStack.Item, itemStack.NumberOfItems));
        }
    }
}

Modificar inventario

private int _activeSlotIndex;

public int ActiveSlotIndex
{
    
    
   get => _activeSlotIndex;
    private set
    {
    
    
        _slots[_activeSlotIndex].Active = false;
        _activeSlotIndex = value < 0 ? _size - 1 : value % Size;// 如果索引小于0,循环到最后一个物品槽;否则取模获取合法索引
        _slots[_activeSlotIndex].Active = true;
    }
}

public bool HasItem(ItemStack itemStack, bool checkNumberOfItems = false)
{
    
    
    var itemSlot = FindSlot(itemStack.Item);
    if (itemSlot == null) return false;
    if (!checkNumberOfItems) return true;
    if (itemStack.Item.IsStackable)
    {
    
    
        return itemSlot.NumberOfItems >= itemStack.NumberOfItems; // 检查物品数量是否足够
    }
    return _slots.Count(slot => slot.Item == itemStack.Item) >= itemStack.NumberOfItems;// 检查物品槽数量是否足够
}
        
public ItemStack RemoveItem(int atIndex, bool spawn = false)
{
    
    
   if (!_slots[atIndex].HasItem)
        throw new InventoryException(InventoryOperation.Remove, "槽位为空");

    if (spawn && TryGetComponent<GameItemSpawner>(out var itemSpawner))
    {
    
    
        // 添加生成物品的逻辑
        itemSpawner.SpawnItem(_slots[atIndex].State);
    }
    ClearSlot(atIndex);
    return new ItemStack(null, 0);
}

public void ClearSlot(int atIndex)
{
    
    
    _slots[atIndex].Clear();
}

Modificar elemento de juego

// 设定物品堆栈
public void SetStack(ItemStack itemStack)
{
    
    
    _stack = itemStack;
}

Efecto
Insertar descripción de la imagen aquí

Agregar efecto emergente de descarte

Modificar GameItemSpawner

// 生成物品
public void SpawnItem(ItemStack itemStack)
{
    
    
    // 。。。

    // 抛掷物品(根据当前对象的缩放比例来确定抛掷方向)
    gameItemScript.Throw(transform.localScale.x);
}

Modificar elemento de juego

[Header("丢弃设置")]
[SerializeField]
private float _colliderEnabledAfter = 1f; // 多少秒后启用碰撞器
[SerializeField]
private float _throwGravity = 2f; // 扔出的物品受到的重力系数
[SerializeField]
private float _minThrowXForce = 3f; // 扔出物品时在X轴方向最小的力量值
[SerializeField]
private float _maxThrowXForce = 5f; // 扔出物品时在X轴方向最大的力量值
[SerializeField]
private float _throwYForce = 5f; // 扔出物品时在Y轴方向的力量值
private Collider2D _collider; // 碰撞器
private Rigidbody2D _rb; // 刚体

// 初始化
private void Awake()
{
    
    
    _collider = GetComponent<Collider2D>();
    _rb = GetComponent<Rigidbody2D>();
    _collider.enabled = false; // 防止在还没扔出时被玩家拾取
}

// 开始游戏时的初始化
private void Start()
{
    
    
    SetupGameObject(); // 设置游戏对象
    StartCoroutine(EnableCollider(_colliderEnabledAfter)); // 在设定的时间后启用碰撞器
}

// 等待一段时间后启用碰撞器
private IEnumerator EnableCollider(float afterTime)
{
    
    
    yield return new WaitForSeconds(afterTime);
    _collider.enabled = true;
}

// 扔出物品
public void Throw(float xDir)
{
    
    
    _rb.gravityScale = _throwGravity; // 设定重力值
    var throwXForce = Random.Range(_minThrowXForce, _maxThrowXForce); // 随机力量值
    _rb.velocity = new Vector2(Mathf.Sign(xDir) * throwXForce, _throwYForce); // 设定物体的速度向量
    StartCoroutine(DisableGravity(_throwYForce));
}

// 一定的高度后禁用重力,让物品飞行更自然
private IEnumerator DisableGravity(float atYVelocity)
{
    
    
    yield return new WaitUntil(() => _rb.velocity.y < -atYVelocity);
    _rb.velocity = Vector2.zero;
    _rb.gravityScale = 0;
}

Efecto
Insertar descripción de la imagen aquí

efecto final

Insertar descripción de la imagen aquí

Código fuente

Lo colgaré cuando lo haya solucionado.

fin

¡Regalos de rosas, regala una fragancia! Si el contenido de este artículo es útil para usted, no sea tacaño con sus 点赞评论和关注comentarios para que pueda recibirlos lo antes posible. Sus comentarios 支持son la mayor motivación para seguir creando. ¡Cuantos más Me gusta, más rápidas serán las actualizaciones! Por supuesto, si encuentras 存在错误algo en el artículo 更好的解决方法, ¡no dudes en comentar y enviarme un mensaje privado!

Está bien, lo soy 向宇, https://xiangyu.blog.csdn.net

Un desarrollador que ha estado trabajando silenciosamente en una pequeña empresa recientemente comenzó a estudiar Unity por su cuenta por interés. Si encuentra algún problema, también puede comentar y enviarme un mensaje privado. Aunque es posible que no sepa algunas de las preguntas, verificaré la información de todas las partes e intentaré dar las mejores sugerencias. Espero ayudar Más personas que quieran aprender programación. Gente, anímense unos a otros ~
Insertar descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/qq_36303853/article/details/133360862
Recomendado
Clasificación