Всем привет. Меня зовут Гриша Дядиченко, и я технический продюсер. Почему так сложно писать про хороший код? Меня периодически спрашивают, почему я так мало пишу про архитектуру. В то же время я даже среди заказчиков встречаю мнение что “в Unity пишется только плохой код”. Чтож, давайте один раз попробуем, а точнее я попробую показать, почему это очень сложно. Разработаем вместе такую “простую вещь” как инвентарь. Итак, но начнём с неких общих слов. Есть такой замечательный эффект, как эффект Даннинга — Крюгера. Его суть заключается в том, что люди с низкой квалификацией всегда уверены, что они правы. А люди с высокой квалификацией наоборот же считают, что они в чём-то не до конца разбираются, могут придумать миллиард нюансов и где то, что они говорят не совсем так. И в этом плане архитектурные вопросы – это бездна. Так что давайте попробуем разработать наш идеальный инвентарь. Давайте рассуждать пока абстрактно, но помня примеры инвентарей. По сути это хранилище предметов. Предметы бывают разные, так что давайте сразу заведём абстрактный класс BaseItem, который будет выглядеть как-то так. Максимально абстрактный предмет в инвентаре по сути обладает идентификатором (для оптимизации возьмём не string, а long), путём до его иконки в инвентаре и именем. Да?Конечно же нет. Переписываем класс, так как имени и иконки тут не должно быть. Почему? Потому что имя как сущность в игре вообще не существует, а существует у нас Alias локализации, да и иконка не относится к предмету, если мы хотим уметь играть в игру в консоли. Поэтому оставляем максимально абстрактный BaseItem. Хорошо. Предмет у нас есть. Теперь давайте создадим инвентарь. Представим его как просто набором предметов. И вот мы создали инвентарь, теперь надо бы научиться добавлять в него предметы и вытаскивать их из него. Так как это у нас поведение, то давайте сразу без лишних прелюдий заведём интерфейс IInventoryActions. Для получения всех предметов используем IEnumerable так как мало ли мы захотим в инвентаре наш массив заменить на словарь или на что-то ещё. Наша реализация теперь. Add и Remove сразу выдают bool для методов валидации "а добавили ли мы предмет я в ячейку". Поверьте, это пригодится потом. И это не проверяется через критерий занятости ячейки. Совсем параноики конечно могут определить условие IsCellHasItem внутри этих методов. Но эти булы будут говорить не об этом. Итак, у нас есть что-то похожее на инвентарь. Наверное, чтобы удобнее его тестировать нам нужен к нему какой-то графический интерфейс. Пока забудем о том, что вы написали свой "самый удобный фреймворк для GUI внутри Unity" и напишем всё довольно просто и конкретно. И ячейка выглядит как: Так сказать первые монобехи. До этого играть можно было в консоли. Для теста мы создали инвентарь прям в окне, но вообще для него нужно какое-то хранилище. Например пользователь. Так что давайте заведём нашего пользователя. Он нам ещё пригодится. В рпг и сингл плеерных играх пользователя удобно делать синглтоном. Потому что все остальные юзеры, кроме текущего, на мой взгляд это Actors или вроде того. А пользователь у нас всегда один. Если у нас игра без hotseat и т.п. Но я последнее время предпочитаю не писать часть с приватным конструктором, а просто иметь статик доступ через Current, чтобы в случае необходимости прокидывать мок юзера. Вообще "как написать грамотно юзера" — этого на ещё одну статью хватит. Ну и окно инвентаря теперь выглядит вот так. Едем дальше. По хорошему предметам нужны иконки, которые мы будем отображать в ячейках. Заведём данные нескольких игровых предметов и сторажд с ними. Начнём с поведения: По сути мы только получаем данные из стораджа, при том отдельными объектами. Дальше напишем данные предмета. Сразу оговорюсь, что пусть и хранилище мы ща пишем ридонли по всем канонам, я считаю паранойей делать такую защиту от дурака. Но тем не менее это по сути статическое хранилище данных, которые в рантайме мы менять не должны, так что пусть будет иммутабельно в пределах базовых типов. А теперь в само хранилище: Его уже не хочется делать статик контейнером в самом себе, поэтому сделаем так: Ну и для загрузки данных для удобства пока используем SO без плясок с кастомной отрисовкой словарей в редакторе и их сериализацией, то есть без защиты от дурака. Хотя там тоже есть что написать. Теперь наш SO с предметами выглядит как-то так: Добавив пока для теста в конструктор пользователя несколько предметов, мы увидим, что всё работает: Небольшая ремарка. Почему инвентарь != storage. Ведь по сути инвентарь можно было бы воспринимать, как некую форму Storage. Я не очень люблю совсем общие обобщения всего подряд. Так как Storage в моём понимании рид-онли в рантайме. И так в разы проще отслеживать баги, да и править, когда сущности разделены. Теперь предметы хотелось бы допустим перекладывать и удалять. Для иллюстрации концепта, удалять мы будем через клик, а перемещать через драг. И оставим пока наши тест предметы. Для начала сделаем удаление. Чуть изменим наши классы графического интерфейса. И удаление работает. С драгом же чуть посложнее. Давайте заведём DragContext. Зачем нужен контекст? В идеале с Drag&Drop удобно когда есть некий контекст драг энд дропа, чтобы нельзя было, да и не надо было обрабатывать, что если у нас есть две панели с драг энд дропом и ошибки, так как мы кидаем из одной в другую. Потому что они не взаимодействуют между друг другом благодаря разным контекстам. Теперь же напишем контейнер для нашего контекста: Создавать кнопку по внешнем контейнере с огромным Z-index пока мне показалось из всех способов реализации драга удобнее всего. Тут же мы заводим нужный нам контекст, который будет реализовывать наш переброс предметов внутри инвентаря. И осталось сделать реализацию в самой ячейке инвентаря. И вот спустя некоторое время Drag & Drop работает в нашем инвентаре.  Чтож инвентарь есть, мы можем двигать в нём предметы. Теперь сохраним состояние инвентаря между запусками, как финальный штрих. Для этого создадим динамический Storage и начнём как всегда с интерфейса: Теперь реализуем наш конкретный Storage: И прокинем его в пользователя. Плюс создадим событие на изменение инвентаря, чтобы сохранятся скажем при каждом изменении: Теперь наш инвентарь умеет сохранять состояние предметов пользователя, запоминая что и куда мы положили. Мок инициализацию я пока оставил чисто для демонстрации, чтобы не делать полноценный мок. Ну вроде базовый инвентарь готов, его можно расширять и делать что-то. А теперь наконец-то поговорим про код. Весь репозиторий можно посмотреть тут. Ну я считаю довольно неплохой, но тут есть множество вопросов. Всё классно, а что если у нас асинхронный сторадж (скажем сохранения по сети?) и почему не написать всё через Task сразу? А что есть у нас WebGL и Task там адекватно не работает и нужно юзать Uni Task? А что если у нас инвентарь имеет форму, а предметы размер как в некоторых RPG. Тогда для правильной синхронизации графический интерфейс должен отражать эту форму, а модель данных должна валидировать это всё (и придётся очень многое переписывать). Так же тут заложен потенциальный риск бага, который возможно не все сразу заметили, что при подобном подходе к отрисовке и изменению состояния ячеек, есть риск того, что состояние на экране не будет соответствовать состоянию модели данных. Так как интерфейс перерисовывается по действиям, а не реактивно, как я больше люблю. То есть когда мы изменили "стейт" — перерисуй всё. Сторадж для визуальных данных предмета. Зачем его делать отдельно? Ведь можно все данные о предмете собрать в один SO и там удобно будет отредактировать. А можно сделать один SO и распихивать данные по куче стораджей из него, при этом гуй не поменяется, но зато провайдеры данных будут разделены. И скажем иконки могут весить много и уехать на CDN, а может нам нужна кор механика, а поменять только названия и иконки. И это только я сам к себе так могу придраться. Поэтому очень тяжело писать про хороший код, так как у каждого своё понятие хорошего кода, исходя из опыта. Есть как бы норм код, и не норм. А остальное вкусы и вопрос контекста задачи. Я считаю, что не бывает универсальных решений, если это не супер мелкая задача. Спасибо за внимание, надеюсь эта статья была вам полезна и вы узнали из неё что-то новое. Полный проект с инвентарём можно найти тут.  Master of Unity3D