Pull to refresh
1
0
Михаил Клочков @WeslomPo

Инженер программист

Send message

Все хорошо, пока в анимации не упадет ошибка и не свалится в варнинг, который обычно выключен т.к. Unity туда кучу бесполезного мусора накидывает.

Строго рекомендуете использовать в качестве Event-ов struct-ы, а при вызове GEventPool.SendMessage - упаковываете её в объект. Ну и в чем тогда смысл?

Далее, вы утверждаете что вот это DI:

private readonly PlayerHealthbarEmitter _healthbarEmitter = GDataPool.Get<PlayerHealthbarEmitter>();

По факту это сервис локатор, что не является DI. Вот [SerializeField] - это DI. Вы не знаете как, кто (и при определенной сноровке что) просунет вам в переменную. В вашем же случае объект сам просит конкретную реализацию объекта из конкретного места.

Чтобы сделать инъекцию зависимости без контейнера зависимостей, просто передавайте зависимости в конструктор, либо в поле, либо в метод.

Например:

public class PlayerHealthbarSystem : IStartSystem {

private int _health;

private readonly IHealthEmitter _healthEmitter;

public PlayerHealthbarSystem(IHealthEmmiter healthEmmiter)

_healthEmmiter = healthEmmiter;

}

...

}

Вот теперь мы не знаем кто, как и "что конкретно" передаст нам в качестве зависимости. Ну и инъекция при помощи контейнера для бедных:

_system.Add(new PlayerHealthbarSystem(playerHealthbarEmitter);

Чем дольше смотрю на все это, тем страшнее становится за тех кто будет с этим работать. Как то от чего следует отталкиваться вам лично в вашем развитии - неплохой проект, через пару месяцев вы от него либо откажетесь, либо перепишете полностью.

Как то что рекомендовать кому-то к реализации в своих проектах - плохая идея. Во-первых продукт сырой, во-вторых решает всего пару проблем (организация очереди вычислений, передача сообщений, единая организация кода), при этом недостаточно правильным образом.

Несмотря на то что утверждаете что используете DI - используете ServiceLocator. Это чревато тем, что когда захотите дёрнуть какую-нибудь систему из одного проекта в другой, вам придется, помимо явных зависимостей, тащить за собой весь ваш фреймворк, либо организовывать заглушки для SL.

В шину событий рекомендуете посылать структуры, но сразу же их упаковываете в объект. Сама по себе шина событий достаточно неприятная штука. Я бы не рекомендовал пользоваться ей на широкую ногу, закапаетесь в лапшу из вызовов событий. Шаг в сторону типизированной шины - правильный, но сама по себе шина источник кодовой запутанности.

Несколько явных Singletone-ов - отказаться от такой архитектуры в дальнейшем будет очень затруднительно, придется клещами потом выдирать эти куски кода из проекта. Синглтоны - плохо, уже столько статей про это написано, а вы в своей архитектуре их сразу же наплодили кучку.

Попробуйте Zenject - в нем уже все эти вопросы решены. Даже шина событий есть, только, я её не рекомендую использовать. И даже синглтон есть, но это если поискать :). Это хороший DI контейнер, поможет вам понять что это и как с этим жить, научит архитектуре получше. Есть множество других, но Zenject наиболее взрослый.

Попробуйте Entitas - он стал бесплатным, а лучше попробуйте EntitasRedux - у него все проще с настройкой проекта и правильное именование компонентов сущности. ECS - это отличное архитектурное решение, признанное мировым сообществом разработки игр, Entitas - один из самых простых для осваивания фреймворков. Есть еще для Unity - Leo ECS, Svelto ECS и многие другие. Сам использую Entitas, и вообще приверженец кодогенерации.

В заключение, как я уже говорил, ваш фреймворк неплохое начало для осознания того, какой должна быть архитектура вашего проекта. Вы уже начали осознавать что двигаться путем который предлагали разработчики Unity в разработке игр - гиблое дело, но не нашли то как можно делать по другому. Собственно попробуйте сделать проект с минимальным количеством MonoBehaviour, используйте конструкторы (и методы, где нет возможности их использовать) для инъекции зависимостей, используйте больше интерфейсов, попробуйте TDD подход для решения какой либо задачи (без использования MonoBehaviour). Разберитесь что такое struct и чем его едят. Попробуйте Data Driven подход к разработке, попробуйте какой-нибудь фреймворк ECS помимо юнитивского (ибо он архисложный, ещё по этому же не рекомендую Svelto).

Не столько во внешней карте, сколько в том что они принудительно включают турбобуст и тут же пытаются его охладить преждевременно. Есть умельцы которые настраивают профили вентиляторов чтобы держали 70°C, но имхо это глупо. Если вырубить TB, то проблема с шумом вентиляторов при подключении монитора уйдёт, только вот работать система станет чуть медленнее (где-то заметно, где-то не очень). Собственно я пошёл по этому пути.
Это похоже на слухи. Apple говорит о том что будет выбрано устройство с большим током (на 2017 это было так и на 16" 2019 так же). У меня моник 50W а зарядка 96W одновременно подключены.
Имхо похоже
Нет, с чего бы? Вы путаете кислое с пресным. Одно дело оригинальное озвучивание, другое дело — локализация. И опять же, у меня было бы меньше претензий, если бы озвучивали больше людей, а то на одну игру с 20 персонажами возьмут 4 человека. Как это вообще сравнивать можно?
Меня больше раздражает три с половиной голоса озвучки. Играешь такой, а с тобой разговаривает напарник из мафии 2. Или злодей говорит голосом твоего напарника. Это очень неприятно. Никакая локализация не передаст голоса людей которые озвучивали эту игру в нормальной обстановке. Я против озвучки, только субтитры.
За год не сталкивался с такими изменениями. Поживем увидим :). Главное чтобы данные не пропали, а как их достать — всегда можно придумать.
Реализации да, поддержка — нет. Один раз настроил и работает. У каждой таблицы добавляется еще одна вкладка Schema — которая описывает формат JSON который нужно сформировать для Unity.
Но там без связей. И вот связи приходится на клиенте восстанавливать. Инспектор кастомный делать не нужно, кнопку скачивания можно вынести в меню.
То что надо на лету подсасывать, это уже должно быть из нормальной базы данных с нормальным бекэндом. Api гугла медленное, чтобы не злоупотребляли. Для ГД еще можно сделать, для игроков — нет. Поэтому скрипты для работы я в Editor засунул.
А мы подняли сервер на гуглокоде, который через api docs по схожему принципу вытаскивает данные и просто отдает JSON в Unity, где его из промежуточного формата (если требуется) превращают в ScriptableObject.
Отличная статья! Вспоминаю боль, когда впервые столкнулся с конфликтом библиотек, во время сборки проекта под андроид.
Стоит отметить что GameObject можно спрятать в иерархии, чтобы он не мешался:
gameObject.hideFlags = HideInHierarchy;

Вместо сцены, на мой взгляд, лучше использовать Zenject с инсталлером на ProjectContext, но для библиотек — это не очень подходит.
Можно еще использовать атрибут
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize(){ Debug.Log("[RuntimeInitializeOnLoadMethod]");}

И, хотя, время исполнения его не гарантированно, это, в теории, позволит избежать создания GameObject. То есть, можно в этом методе создать класс не наследованный от MonoBehaviour и сохранить его в статическую переменную. Сигналы же, получать через колбэки от библиотеки.
0. Этому классу совсем неважно как его создадут и куда поместят, это не его проблемы.
1. Да более подходящее имя.
2. Не важно как :) как удобно. Например из пула объектов.
3. Обычно контроллеры не занимаются такой ерундой (просто получают список всех нужных объектов, либо пул), но можно и так.
4. Это одна из причин почему он этим не занимается.
5. Все что нужно MonoBehaviour — это вид и его проблемы, там же и решаем.
6. Под каждый кусок проекта завожу папку, внутри иерархию из папок Controllers, Views, Enums, Interfaces и т.д., папки исключаю из генерации namespace. Пока так. Но не уверен что это лучшее решение.
7. Распространенное заблуждение :). В крупных проектах долгожителях на первый план выходит поддержка, а не написание. Создавать модули — чуточку сложнее становится, за то поддерживать в десяток раз проще. Плюс разделение логики сильно сокращает время написания. Да я, лично, не люблю прокидывать классы через контейнер (так много всего нужно учесть), но это делается один раз, а потом готовые классы просто дополняются нужным функционалом. Кстати, больше всего ошибок допускается и времени уходит на виды, а контроллеры и модели писать — одно удовольствие, чистая логика.
В целом LeonThundeR прав, если проект мелкий и жить долго не собирается — лучше не морочить голову архитектурой особо — весь запал выжрет — это применимо только если нужно долго поддерживать проект.
Не использовать.

Когда изучаешь Unity, автомагически приобретаешь болезнь MonoBehaviour головного мозга xD. Вот ты захотел решить задачу со списком кнопок — и сразу создал «class BuildingButtonsList: MonoBehaviour» — а зачем? Можно же простой класс использовать, а в нужный момент вызвать метод Initialize либо другой аналог Start, из класса контроллера (вопрос целесообразности существования самого такого класса оставим за скобками).

Когда перестаешь при решении задач думать о том как бы это все в недокомпонентную модель вписать — сразу же приобретаешь свободу в решении задач, становится проще применять разные паттерны проектирования на практике.

Может возникнуть опасение, что этот подход не вяжется с Unity — однако это не так, движок достаточно гибкий. Попробуй Zenject изучить, довольно неплохо помогает прокачаться.
Стараться не использовать MonoBehaviour :).

Вместо Object.Instantiate — нужно использовать фабрику, и тогда-то зависимости будут прокинуты.

По примеру, больно много уже классов, чтобы быстро пояснить… я только один изменю — BuildingButtonPrefab.
public class BuildingButton : MonoBehaviour {
	[SerializeField] private Button _button;
	[SerializeField] private Text _title;
	[SerializeField] private Particles _particles;

	public void Listen(Action onClick) { _button.onClick.AddListener(onClick.Invoke); }

	public void Unlisten() { _button.onClick.RemoveAllListeners(); }

	public void Initialize(string title) { _title.text = title; }

	public void Burst() { _particles.Launch(); }

	public void SetState(ButtonState state) {
		switch (state) {
			case ButtonState.Disabled:
				_button.interactable = false;
				// _button.image = _gray;
				break;
			case ButtonState.Inactive:
				_button.interactable = false;
				// _button.image = _blue;
				break;
			case ButtonState.Active:
				_button.interactable = true;
				// _button.image = _green;
				break;
			default:
				throw new ArgumentOutOfRangeException("state", state, null);
		}
	}
}

Как видно, тут нет синглтонов, как и нет никакой логики изменения состояния, нет самих состояний. Вообще, я руководствуюсь правилом — если это MonoBehaviour то это Вид и он максимально тупой. Все данные которые нужны, кнопка получает через методы (или поля), сама кнопка так же ничего и не спрашивает ни у кого, у ней есть все нужные ей данные для работы. Ее внутренние состояние можно поменять, в теории, только через публичные методы. Можно выделить интерфейс — и таким образом заблокировать доступ к gameObject
Логика изменения поведения кнопки находится в другом классе, он не-MonoBehaviour и контролирует ее состояние обращаясь к моделям или другим контроллерам.
Используйте Zenject или любой другой контейнер.
Я бы порекомендовал прочесть Сергея Теплякова «Паттерны проектирования на платформе .NET» — современная, написана на русском языке — нет проблем с переводом.
Я стараюсь использовать в Unity как можно меньше Unity.

По статье как-то незаметно, компоненты поверх компонентов, компонентами погоняют :). ScriptableObject'ы легко меняются на обычный класс. Сравни с моей реализацией — там один MB — как точка входа алгоритма, а записывать можно любые данные практически.

1 — Есть еще readonly, но с MonoBehaviour такое не провернешь. А можно геттером спрятать.
3 и 4 — MyClass.Something уже плохо.
Правильно приготовленный синглтон — это синглтон который не был написан.

Обращаясь из одного класса к другому через MyClass.Something — ты создаешь зависимость, которую довольно сложно отследить, а потом, при рефакторинге заменить. Привязываешь класс А к классу Б стальными тросами. Тут даже нет речи об интерфейсах и т.д. Сплошное несчастье.

Это сложно объяснить, но когда ты столкнешься с этим на практике, то поймешь насколько синглтоны — зло.

Синглтоны: Повышение связанности кода, невозможностью заменить\удалить и почти всегда применение синглтона говорит о том что с архитектурой что-то не то — участок кода попахивает.
http://rsdn.org/forum/design/2615563.flat#2615563

Статичные переменные — родственники глобальных переменных. Основная проблема — потеря контроля над значениями. Их может изменить кто угодно и откуда угодно. Я, похоже запутался, и сказал про сложный тип, но имел в виду константы (строки, числа). Т.е. константные статические переменные еще куда не шло, а вот изменяемые значения — зло стопроцентное.

Хотя в Unity — использовать константы не круто, потому что сам по себе движок помогает с сериализацией, и в 99% случаев лучше использовать ScriptableObject, а значения менять прямо из редактора.

Вообще почитайте про инверсию управления через Dependency Injection. Отличный фреймворк для этих целей под Unity — Zenject. В пару проектов втыкаешь и забываешь про синглтоны.
Больше синглтонов богу синглтонов? Синглтоны и статичные переменные сложных типов — зло. Ищите решение, которое их не использует. Например пусть какой-нибудь менеджер находит все компоненты определенного типа и сохраняет их. Не забываем добавить уникальный идентификатор для каждого такого компонента.
Для сериализации есть прекрасная утилита, которая заслуживает упоминания в этой статье:
JsonUtility.ToJson();
JsonUtility.FromJson<Foo>();

которая может сериализовать практически любой класс с приемлемой скоростью.
Вот накидал за полчаса примерчик:
Примерная реализация без синглтонов
Пример менеджера сохраняющего нужные нам данные:
public class TransformSaver : MonoBehaviour
{
	[SerializeField]
	private Transform[] _transforms;
	private readonly SaveManager _saveManager = new SaveManager();

	private void Start() {
		for (var index = 0; index < _transforms.Length; index++)
			_saveManager.Register(new TransformSave(index.ToString(), _transforms[index]));
	}

	[ContextMenu("Save")]
	public void Save() {
		_saveManager.Save();
	}

	[ContextMenu("Load")]
	public void Load() {
		_saveManager.Load();
	}

}

Остальные классы, нужные для работы сего безобразия:
public class SaveManager
{
	private readonly List<ISave> _saves = new List<ISave>();

	public void Register(ISave element) { _saves.Add(element); }

	public void Unregister(ISave element) { _saves.Remove(element); }

	public void Save() {
		var saves = new Saves();
		foreach (var save in _saves)
			saves.Add(save.Uid, save.Serialize());
		PlayerPrefs.SetString("Save", JsonUtility.ToJson(saves));
		PlayerPrefs.Save(); // Force save player prefs
	}
	public void Load() {
		var json = PlayerPrefs.GetString("Save", "");
		if (string.IsNullOrEmpty(json))
			return;
		var saves = JsonUtility.FromJson<Saves>(json);
		for (var index = 0; index < saves.Uids.Count; index++) {
			var element = _saves.Single(x => x.Uid == saves.Uids[index]);
			element.Deserialize(saves.List[index]);
		}
	}

	[Serializable]
	private class Saves
	{
		public List<string> Uids = new List<string>();
		public List<string> List = new List<string>();

		public void Add(string uid, string value) {
			if (Uids.Contains(uid))
				throw new ArgumentException("Uids has already have \"" + uid + "\"");
			Uids.Add(uid);
			List.Add(value);
		}
	}

}
	
public interface ISave
{
	string Uid { get; }
	string Serialize();
	void Deserialize(string json);
}

public class TransformSave : ISave
{

	private readonly Transform _transform;
	public string Uid { get; private set; }

	public TransformSave(string uid, Transform transform) {
		Uid = uid;
		_transform = transform;
	}

	public string Serialize() {
		return JsonUtility.ToJson(
			new TransformData {
				                  Position = _transform.position,
				                  Rotation = _transform.rotation
			                  }
		);
	}

	public void Deserialize(string json) {
		var deserialized = JsonUtility.FromJson<TransformData>(json);
		_transform.SetPositionAndRotation(deserialized.Position, deserialized.Rotation);
		D.Log("Json", json);
	}

	[Serializable]
	private class TransformData
	{
		public Vector3 Position;
		public Quaternion Rotation;
	}
}



Больше бесит, что для пароля существуют условия, которые ты можешь узнать только во время смены пароля или регистрации. Я как бы не против таких требований, я против того чтобы их прятать на экране авторизации, особенно когда испытываешь проблемы с авторизацией, можно было бы их напомнить на всякий случай.

Information

Rating
Does not participate
Location
Тольятти, Самарская обл., Россия
Date of birth
Registered
Activity