Подскажите по загрузке данных в Unity (архитектурное решение)
Рад Вас приветствовать
Введение: создаётся карточная игра с разными типами карт (например карты "Враг" и карты "Предмет"). Каждый тип имеет свои данные. Так как этих карт будет большое количество, было принято решение загружать данные о картах из json файла.
Пример загрузки карт типа "Враг":
{
"AllEnemies": [
{
"Id": 0,
"CardName": "Big fly",
"Stats": [ 0.5, 0, 5, 0.25, 70 ],
"Sprites": [ "someSprite", "someSprite"]
},
{
"Id": 1,
"CardName": "Bug",
"Stats": [ 0.5, 0.25, 30, 1.4, 15 ],
"Sprites": [ "someSprite", "someSprite"]
}
}
Соответственно у других типов карт сериализуемые поля будут отличаться.
Вопрос: как архитектурно и в какой момент времени лучше сделать загрузку этих данных?
Рассказываю свою реализацию, которую я считаю громоздкой и неверной, но ничего лучше придумать пока что не могу
У меня есть абстрактный класс
public abstract class LoadCardsFromJson : MonoBehaviour
{
[SerializeField]
private Texture2D _spriteSheet;
protected Sprite[] allSprites;
private void Awake()
{
allSprites = Resources.LoadAll<Sprite>("CardSprites/" + _spriteSheet.name);
}
public abstract List<Card> LoadCardsFromFile(string fileName);
}
Который будет наследоваться классами, отвечающих за загрузку каждого типа карты, например загрузка карт врагов:
public class LoadEnemies : LoadCardsFromJson
{
public override List<Card> LoadCardsFromFile(string fileName)
{
List<Card> returnEnemies = new List<Card>();
//логика загрузки карт врагов
return returnEnemies;
}
}
Также есть абстрактный класс CardController,который будет наследоваться остальными контроллерами.
public abstract class CardController : MonoBehaviour
{
private CardView _cardView;
private List<Card> _cards;
public abstract void LoadCards();
}
Каждый класс, который унаследует CardController, должен будет реализовать метод LoadCards() и будет содержать в себе нужный Load класс
(для EnemyController - LoadEnemies, для ItemController - LoadItem, и т.д.)
Например, EnemyController будет выглядеть так:
public class EnemyController : CardController
{
[SerializeField]
private LoadEnemies _loadEnemies;
private List<Card> _allEnemies = new List<Card>();
private void Awake()
{
LoadCards();
}
public override void LoadCards()
{
_allEnemies = _loadEnemies.LoadCardsFromFile("Enemies");
}
На Unity сцене будет пустой GameObject, содержащий в себе все Controller'ы (EnemyController, ItemController и т.д.) и при запуске сцены в каждом будет запускаться метод Awake() и, соответственно, подгружаться все данные из json файлов
Но эта реализация мне очень не нравится по следующим причинам:
- Дублирование кода для каждого типа
- Для каждого нового типа данных мне нужно будет создавать большое количество кода, который уже написан, но используется чуть иначе в других типах карт
В связи с этим я ищу больше архитектурное решение моей проблемы, если это вообще проблема. Если можно этот код сделать более абстрактам и удободополняемым, я буду рад прочесть любые комментарии на этот счёт
P.S. Я думал о том, что это всё можно привязать через события, но так и не придумал как именно
Ответы (1 шт):
LoadCardsFromJson, судя по названию он из некого источника, и из json выдавать модели данных в виде соответствующих типу структур, но вместо этого вижу текстуры и спрайты... что? Подбором ресурсов по данным это другая ответственность. В Unity3D для этих дел есть JsonUtility.
LoadEnemies, зачем он нужен не понимаю, при сериализации твоего одного большого джейсена выйдет что-то типа:
[Serializable]
public struct AllCardData
{
public ItemCardData[] AllItems;
public EnemyCardData[] AllEnemies;
}
Названия ужасные, почему нельзя просто items и enemys?
CardController... если не знаешь чем занимается класс и в чем его ответственность, назови его controller, древняя традиция. Есть визуализация, лист кард и еще грузит карты... что это? зачем это?
Дублирование кода для каждого типа
Напиши метода в базовом классе и не придется дублировать его в наследниках, а просто вызывать метод.
Для каждого нового типа данных мне нужно будет создавать большое количество кода, который уже написан, но используется чуть иначе в других типах карт
Что бы этого избежать, есть наследование, абстракция и вообще можно выделять поведения в самостоятельные классы.
Вообще это не имеет никакого смысла хранить и грузить все данные из json. Все карты в игре, их картинки, звуки, эффекты и т.д. изначально в проекте, а не на сервере, если добавляются новые, выпускается апдейт. Храня данные в тексте потом геморой связывать их с упомянутыми ресурсами, и вообще грузить... зачем это нужно?
Все можно хранить и настроить в ScriptableObject
public class CardData : ScriptableObject
{
[SerializeField] private string _name;
[SerializeField] private Sprite _image;
[SerializeField] private AudioClip _useClip;
...
public string Name => _name;
public Sprite Image => _image;
public AudioClip UseClip => _useClip;
...
}
[CreateAssetMenu(fileName = "UnitCard", menuName = "Card/Data/Unit")]
public class UnitCardData : CardData
{
...
}
[CreateAssetMenu(fileName = "ItemCard", menuName = "Card/Data/Item")]
public class ItemCardData : CardData
{
...
}
[CreateAssetMenu(fileName = "CardsDataCollection", menuName = "Card/DataCollection")]
public class CardsDataHolder : ScriptableObject
{
[SerializeField] private CardData[] _collection;
public IEnumerable<CardData> Collection => _collection;
public IEnumerable<UnitCardData> UnitCollection =>
_collection.Where(c => c is UnitCardData).Cast<UnitCardData>();
public IEnumerable<ItemCardData> ItemCollection =>
_collection.Where(c => c is ItemCardData).Cast<ItemCardData>();
}
public class Card
{
protected readonly CardData data;
protected Card (CardData data)
{
this.data = data;
}
public virtual void Use ();
}
public class UnitCard : Card
{
public UnitCard (EnemyCardData data) : base(data)
{
}
public UnitCardData UnitData => data as UnitCardData;
public override void Use ()
{
...
}
}
И какой нибудь генератор карт, который кастит типы данных и создает соответствующие карты
public static class CardDataExtension
{
public static Card GetCard (this CardData data)
{
if (data is UnitCardData uData)
return new UnitCard(uData);
else if (data is ItemCardData iData)
return new ItemCard(iData);
else
throw new Exception("unrecognized card data type");
}
}
public class Foo : MonoBehaviour
{
[SerializeField] private CardsDataHolder _cards;
public void DoSometing ()
{
var data = _cards.UnitCollection.First();
UnitCard card = data.GetCard() as UnitCard;
}
}