Как настроить соответствие между ScriptableObject и Enum

У меня есть ScriptableObject для предметов, которые игрок может собирать. Это может быть монета, палка, камень, кокос и так далее. Такой объект имеет название, спрайт и тип:

public enum FancyItemType
{
    Coin, Stick, Stone, Coconut
}

[CreateAssetMenu(fileName = "New Fancy Item", menuName = "New item", order = 51)]
public class FancyItem : ScriptableObject
{
    public FancyItemType Type;
    public Sprite Sprite;
    public string Name;
}

Я создал и настроил такие объекты в юнити. Мне хотелось бы получать такой FancyItem по перечислению Enum. Например, я хочу выдать игроку 10 палок:

void ReceiveBonus (FancyItemType type, int amount) {
    // Эта функция принимает ТИП предмета, который игроку надо выдать
    // Функция должна каким-то образом узнать спрайт и название предмета по типу
}
...
ReceiveBonus(FancyItemType.Stick, 10); // Передаем тип предмета -- палка, 10 штук

Как я могу получить FancyItem со всеми данными: спрайт и имя, по перечислению Enum?

У меня есть такая идея: сделать класс, в который я в инспекторе брошу ссылки на ScriptableObject, а он будет по Enum выдавать необходимый:

public class FancyItemHelper : MonoBehaviour {
    [SerializeField] private FancyItem Coin, Stick, Stone, Coconut;

    public FancyItem GetItem (FancyItemType type) {
        switch (type) {
            case FancyItemType.Coin:
            return Coin;
            case FancyItemType.Stick:
            return Stick;
            case FancyItemType.Stone:
            return Stone;
            case FancyItemType.Coconut:
            return Coconut;
        }
    }
}

Однако мне такой подход слишком сложным. Как мне следует это реализовать?


Ответы (2 шт):

Автор решения: Alemkhan Utepkaliev

Есть вариант с созданием обьекта на сцене возможно с использованием синглтона для доступности из любой точки (на ваше усмотрение), со скриптом который будет хранить в себе List с нужным вами элементами, заполняемые и изменяемые в испекторе


public class GameItems : MonoBehaviour 
{
    [SerializeField] private List<FancyItem> _fancyItems;
    // Сюда также можно добавить таким же способом любой другой список предметов

    public static Instance { get; private set;}

    private void Awake()
    {
       if (Instance == null)
       {
           Instance = this;
           DontDestroyOnLoad(this.gameObject);

           return;
       }

       Destroy(this.gameObject);
    }

    // Вернёт null если не смог найти по типу, можно обработать как угодно
    public FancyItem GetFancyItemByType(FancyItemType fancyItemType)
    {
       foreach (FancyItem fancyItem in _fancyItems)
       {
          if (fancyItem.Type == fancyItemType)
          {
              return fancyItem;
          }
       }

       return null;
    }
}

Возможно это решение которое Вам нужно!

→ Ссылка
Автор решения: KOTlK

Есть два стула.

  1. Использовать словарь
  2. Не использовать словарь и енамы с ним заодно.

Первый самый легкий - делать то же самое, что в свиче, только со словарем:

using System.Collections.Generic;
using UnityEngine;

namespace ScriptableHell
{
    public interface IConverter<in TIn, out TOut>
    {
        TOut Convert(TIn from);
    }
    
    public class EnumToItem : MonoBehaviour, IConverter<FancyItemType, FancyItem>
    {
        [SerializeField] private FancyItem _coin, _stick, _kamenbYaNeDam, _coconut;

        private Dictionary<FancyItemType, FancyItem> _items;

        private void Awake()
        {
            _items = new Dictionary<FancyItemType, FancyItem>()
            {
                {FancyItemType.Coin, _coin},
                {FancyItemType.Coconut, _coconut},
                {FancyItemType.Stone, _kamenbYaNeDam},
                {FancyItemType.Stick, _stick}
            };
        }


        public FancyItem Convert(FancyItemType from) => _items[from];
    }
}

Второй малость посложнее. Отрефакторить код и удалить енамы.

Жахнуть интерфейс:

using UnityEngine;

namespace ScriptableHell
{
    public interface IItem
    {
        Sprite Sprite { get; }
        string Name { get; }
    }
}

Реализовать его в наследнике SO:

using UnityEngine;

namespace ScriptableHell
{
    [CreateAssetMenu(menuName = "New/Item", fileName = "NewItem")]
    public class Item : ScriptableObject, IItem
    {
        [field: SerializeField] public Sprite Sprite { get; private set; }
        [field: SerializeField] public string Name { get; private set; }
    }
}

Дальше просто через декораторы эти айтемы расширять. Как пример - айтем, который можно поднимать и кидать. Снова интерфейсы:

using UnityEngine;

namespace ScriptableHell
{
    public interface IPickable
    {
        IItem Pick();
    }
    
    public interface IThrowable
    {
        void Throw(Vector3 direction, float distance);
    }
}

И их реализация:

using System.Collections;
using UnityEngine;

namespace ScriptableHell
{
    [RequireComponent(typeof(SpriteRenderer), typeof(Collider2D))]
    public class PickableItem : MonoBehaviour, IItem, IPickable, IThrowable
    {
        [SerializeField] private Item _item;
        
        private IItem _origin;

        private const float ThrowingTime = 3f;

        public Sprite Sprite => _origin.Sprite;
        public string Name => _origin.Name;
        
        public void Init(IItem origin)
        {
            if (_origin != null) return;
            
            _origin = origin;
            GetComponent<SpriteRenderer>().sprite = _origin.Sprite;
        }
        
        public IItem Pick()
        {
            Destroy(gameObject);
            return _origin;
        }
        
        public void Throw(Vector3 direction, float distance)
        {
            StartCoroutine(Throwing(direction, distance));
        }

        private void OnValidate()
        {
            if (_item == null)
            {
                GetComponent<SpriteRenderer>().sprite = null;
                return;
            }

            GetComponent<SpriteRenderer>().sprite = _item.Sprite;
        }

        private void Awake()
        {
            if (_item == null) return;
            _origin = _item;
        }


        private IEnumerator Throwing(Vector3 direction, float distance)
        {
            var startPosition = transform.position;
            var targetPosition = startPosition + direction * distance;
            var elapsedTime = 0f;

            while (elapsedTime < ThrowingTime)
            {
                var progress = elapsedTime / ThrowingTime;
                elapsedTime += Time.deltaTime;

                transform.position = Vector3.Lerp(startPosition, targetPosition, progress);
                yield return null;
            }

        }
        
    }
}

PickableItem - декоратор для бедных, т.к. у MonoBehaviour нет нормальных конструкторов и приходится делать это. _item нужен лишь для того, чтобы можно было распихать этот поднимабельный объект по сцене и в инспекторе ему закинуть нужный айтем. Также сундук, который содержит несколько однотипных айтемов: контейнер:

using System;
using UnityEngine;

namespace ScriptableHell
{
    [Serializable]
    public class ItemContainer : IItem
    {
        [field: SerializeField] public Item Item { get; private set; }
        [field: SerializeField] public int Amount { get; private set; }
        
        
        public Sprite Sprite => Item.Sprite;
        public string Name => Item.Name;
    }
}

сундук:

using UnityEngine;

namespace ScriptableHell
{
    public class BonusChest : MonoBehaviour, IPickable
    {
        [SerializeField] private ItemContainer _itemContainer;
        
        public IItem Pick()
        {
            Destroy(gameObject);
            return _itemContainer;
        }
    }
}

Компонент, который будет все поднимать висит на плеере:

using UnityEngine;

namespace ScriptableHell
{
    [RequireComponent(typeof(Collider2D))]
    public class PickArea : MonoBehaviour
    {
        [SerializeField] private Player _player;

        private void OnTriggerEnter2D(Collider2D other)
        {
            if (other.TryGetComponent(out IPickable pickable))
            {
                Pick(pickable.Pick());
            }
        }

        private void Pick(ItemContainer container)
        {
            for (var i = 0; i < container.Amount; i++)
            {
                _player.GiveItem(container.Item);
            }
        }
        
        private void Pick(IItem item)
        {
            if (item is ItemContainer container)
            {
                Pick(container);
                return;
            }
            
            _player.GiveItem(item);
        }
    }
}

Сам игрок:

using System.Collections.Generic;
using UnityEngine;

namespace ScriptableHell
{
    public class Player : MonoBehaviour
    {
        [SerializeField] private Transform _parent;
        [SerializeField] private ItemUIPresentation _prefab;
        [SerializeField] private PickableItem _throwablePrefab;
        [SerializeField] private float _speed = 5f;
        
        private readonly List<IItem> _items = new List<IItem>();

        private readonly List<ItemUIPresentation> _spawnedItems = new List<ItemUIPresentation>();
        
        public void GiveItem(IItem item)
        {
            _items.Add(item);
            Draw(_items);
        }

        private void Update()
        {
            var x = Input.GetAxis("Horizontal");
            var y = Input.GetAxis("Vertical");

            transform.position += new Vector3(x, y) * Time.deltaTime * _speed;
            
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Throw();
            }
        }

        private void Throw()
        {
            if (_items.Count == 0) return;

            var item = _items[0];
            var pickable = Instantiate(_throwablePrefab, transform.position + transform.up * 2f, Quaternion.identity);
            pickable.Init(item);
            pickable.Throw(transform.up, 5f);

            _items.Remove(item);

            Draw(_items);
        }

        private void Draw(List<IItem> items)
        {
            Clear();

            if (items.Count == 0) return;
            
            foreach (var item in items)
            {
                var spawned = Instantiate(_prefab, _parent);
                spawned.Init(item);
                _spawnedItems.Add(spawned);
            }
        }

        private void Clear()
        {
            if (_spawnedItems.Count == 0) return;
            
            foreach (var item in _spawnedItems)
            {
                Destroy(item.gameObject);
            }
            _spawnedItems.Clear();
        }
    }
}

Игрок у меня еще и инвентарь сам отрисовывает, поэтому ему нужен префаб ячейки инвентаря, которая тоже является декоратором для бедных:

using UnityEngine;
using UnityEngine.UI;

namespace ScriptableHell
{
    [RequireComponent(typeof(RectTransform), typeof(Image))]
    public class ItemUIPresentation : MonoBehaviour, IItem
    {
        private IItem _origin;

        public Sprite Sprite => _origin.Sprite;
        public string Name => _origin.Name;

        public void Init(IItem item)
        {
            _origin = item;
            GetComponent<Image>().sprite = _origin.Sprite;
        }
    }
}

Не нужно никаких синглтонов/словарей/возвращений null и т.д.

Не знаю зачем, но вот так это все в итоге выглядит

→ Ссылка