Skip to content

(RU) Framework changes

Dimitry edited this page Apr 10, 2019 · 10 revisions

Здесь описаны изменения коснувшиеся фреймворка. Полезно в первую очередь для миграции существующих проектов.

Новый namespace Pixeye.Framework

Весь фреймворк теперь находится в области имен Pixeye.Framework. Почему? Для специализации. В своих проектах я пользуюсь областью имен Pixeye :)

Изменения коснувшиеся редактора

Добавлены drag листы для массивов и листов в инспекторе. Улучшен foldout group атрибут. Сразу хочу предупредить, что драг листы написаны не мной, а взяты отсюда. Посчитал полезной фишкой для редактора. Если вы пользуетесь ODIN инспектором, то инспектор фреймворка будет отключен так что никаких проблем с совместимостью возникнуть не должно.

Изменения коснувшиеся Starter скриптов

Starter gif

Теперь ассеты сцен можно добавлять в билд лист прямо из стартера, так же сцену можно подгрузить в редакторе нажав на плюсик.

Шаблон компонентов

Чтобы избежать ручного добавления бойлерплейт кода при создании компонентов используйте шаблон компонента.

  • (ПКМ project->Actors Framework-> Generate Component)

Component Generator

Изменение наименований

  • Вместо ProcessingBase просто Processor
public class ProcessorCollisions : Processor, ITick
	{
  • Все классы фреймворка которые были со словом Processing теперь используют Processor
ProcessorSignals.Send(); // ProcessingSignals

Сущности

Вместо int для обозначения индекса сущности теперь используется структура ent.

Почему? Использование int в качестве ключа было быстро и удобно, но так как во фреймворке очень много extension методов для удобной работы с сущностями ( вроде обращений к компонентам ) то выходило так, что например можно было попытаться обратиться к компоненту у длинны массива просто потому что он возвращает int.

ent весит 5 байт ( против 4 байт int ) и состоит из int id отвечающий за сам ключ и byte age отвечающий за поколение сущности.

Зачем нужны поколения? Пример. Есть пушка с автонаведением и у нее была цель с индексом 10. Это корабль инопланетян имеющий серьезные намерения совершать непотребства с игроком и его союзниками. Он был уничтожен и индекс 10 освободился и ушел в пул свободных сущностей. Родилась робоальпака, верный союзник игрока. Она взяла индекс 10... и теперь пушка расстреливает робоальпаку. Просто потому что ее цель была с индексом 10 и пушка не знает, что десятка теперь друг.

Поколения являются дополнительной проверкой на соответствие. Так, корабль инопланетян был индексом 10 с поколением 0. При рождении альпаки бог из машины увидел, что когда-то подобная сущность существовала и присвоил ей поколение 1.

Так как ent является структурой, value-значением то у пушки осталась информация о старой десятке. Пушка смотрит что индексы совпадают, а поколения - нет, а значит предыдущая цель однозначно была уничтожена и это уже что-то другое.

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

Создание сущностей

Сущности конвертируемы в int значения.

ent entity = 10; // так можно. Но за фасадом это выглядит как ent entity = new ent(10,0) где id=10, age=0

Однако это не приведет к регистрации сущности в фреймворке. Ниже примеры как надо.

// создаем пустую сущность
ent entity = ent.Create();
// создаем сущность и соединяем ее с префабом из Resources
var entity = ent.CreateFor("obj player");
// создаем сущность и соединяем ее с префабом из Resources
var prefab = Resources.Load("Prefabs/obj player") as GameObject;
// тоже самое только передаем префаб
var entity = ent.CreateFor(prefab);

AddMonoReference

Этот метод позволяет добавить MonoEntity скрипт на созданный объект. Это monobehavior скрипт, "мостик" между фреймворком и объектом юнити. Зачем это нужно? Например если считаем столкновения и хотим узнать сущность объекта нам нужно чтобы объект юнити мог вернуть какой-то скрипт хранящий индексы этой сущности.

ЕСЛИ таким образом вы создаете объект на котором присутствует Actor скрипт, то использовать AddMonoReference не нужно.

var entity = ent.CreateFor(prefab);
entity.AddMonoReference();

Transform

Если вы создавали сущность с привязкой объекта юнити то можно легко получить трансформ этого объекта.

var entity = ent.CreateFor("obj player");
var tr = entity.transform;

Изменения при добавлении компонентов сущности.

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

var entity = ent.CreateFor("obj player");
var cPlayer = entity.Add<ComponentPlayer>();
cPlayer.name = "Sonic";

Add, AddLater

Раньше компоненты добавлялись только через метод Add. Что такое AddLater? AddLater полезен при создании акторов, blueprints или других способов композиции сущности. Он добавляет настроенный компонент в хранилище, но не забрасывает его в системы. Таким образом можно полностью настроить все компоненты сущности и решить какие из компонентов заработают сразу, а какие будут добавлены позже.

var entity = ent.CreateFor("obj player");
// Component Player добавится сразу.
var cPlayer = entity.Add<ComponentPlayer>();
cPlayer.name = "Sonic";
var cHealth = entity.Add<ComponentHealth>();
cHealth.Hp = 10;
// Component Death будет создан, размещен, но не добавлен в системы.
var cDeath = entity.AddLater<ComponentDeath>();
cDeath.animationType = "anim_player_death";


// где-то далеко дальше в коде c той самой созданной сущностью.
 
if (entityPlayer.ComponentHealth().Hp==0)
entityPlayer.Add<ComponentDeath>(); // добавит в системы раннее созданный Component Death с настройкой "anim_player_death"

EntityComposer

Зачем нужен entitycomposer. Выше было сказано, что изменения в компоненты будут внесены только на следующий кадр. Допустим мы создаем 100 сущностей за раз и у каждой по 5 компонентов. Это значит что мы сделаем 500 операций добавления. Я не говорю, что это страшно ( на синтетических тестах создвал десятки тысяч объектов за раз ), однако кол-во операций можно сократить. В среднем это на 30% быстрее ленивой работы с сущностями и такой подход стоит использовать если вы за раз хотите добавить сущности больше 1-2 компонентов + тэги, да еще и нужно инициализировать скопом сразу несколько таких сущностей.

Композитор вызывается на сущности через метод Modify(); и обязательно должен закончиться методом Deploy();

/// вместо 4 операций будет две. 1 на добавление компонентов, другая на добавление тэга.
var composer = ent.CreateFrom("obj player").Modify();

var cPlayer = composer.Add<ComponentPlayer>();
var cTurret = composer.Add<ComponentTurret>();
var cWeapon = composer.Add<ComponentWeapon>();

composer.Deploy(Tag.StateIdle);
var entity = ent.CreateFrom("obj player");
var composer = entity.Modify();

var cPlayer = composer.Add<ComponentPlayer>();
var cTurret = composer.Add<ComponentTurret>();
var cWeapon = composer.Add<ComponentWeapon>();

composer.Deploy(Tag.StateIdle);

Component Object

Component Object являлся компонентом пустышкой для объекта. Сейчас сущности создаются без него. Если он нужен то добавляется отдельно как и все остальные компоненты. Единственный пример на ум приходит если нужен компонент который бы больше работал с тэгами и ему просто необходим компонент в качестве "тела" группы. К сожалению в фреймворке пока нет групп на одних тэгах.

Изменения в тэгах.

Тэги являются компонентами пустышками с счетчиком в рамках ECS. ( А так они могут служить и как индетификаторы ) На примере разрабатываемой мною игры:

Dungelot

Монстр "блокирует" все неоткрытые соседние рядом с ним клеточки. С точки зрения системы ECS есть фильтр в виде группы клеточек с тэгом Blocked - пока будет хотя бы один такой тэг маркер блокировки не спадет с клеточки и герой не сможет туда зайти. Это удобно ведь одну и ту же клетку могут заблокировать несколько монстров, а значит и тэг blocked на клетку повесится несколько раз. Событие же произойдет только когда все тэги Blocked пропадут с клетки.

Визуально работа с тэгами никак не поменялась, однако сильно изменилась их внутренняя структура. Раньше для работы тэгов использовался словарь ключей, но я нашел словарь тяжелым. Вместо этого теперь применяется структура BufferTag - тэги хранятся как ushort поля, ushort позволяет хранить диапазон от 0 до 65 535. Так что не используйте id для тэгов выше этих значений ( не встречал на практике ни разу ). Кол-во хранимых повторений на тэг равно 1 байту ( 256 раз можно один и тот же тэг повесить на сущность )

Размер кол-ва тэгов на сущность предустановлен жестко. По умолчанию во фреймворке можно переключиться между вариантами в 6, 12, 24 тэга на одну сущность. От этого влияет кол-во памяти выделяемой на тэги для каждой сущности. Если понадобится больше нужно редактировать BufferTag, это несложно.

Изменений в событиях подписки групп

onAdd, onRemove вместо Add,Remove. Вместо int entity в качестве аргумента передается in ent entity

group_brains.onAdd += (in ent entity) =>
	{
	var cState = entity.ComponentState();
	cState.current = Tag.StateIdle;
	};

Компоненты

Ниже представлена схема шаблона компонента. Компоненты удобнее всего создавать через шаблоны своей IDE или вышеуказанным методом в юнити. IComponent обладает двумя методами. Dispose для чистки компонента при необходимости. Copy нужен для работы с Blueprints. В кратце он нужен для ручного копирования полей из компонента-образца в компоненты-копии.

	[System.Serializable]
	public class ComponentGoblin : IComponent
	{

		public void Copy(int entityID)
		{
			var component = Storage<ComponentGoblin>.Instance.GetFromStorage(entityID);
		}
		public void Dispose()
		{
	 
		}

	}

	public static partial class HelperComponents
	{

		[RuntimeInitializeOnLoadMethod]
		static void ComponentGoblinInit()
		{
			Storage<ComponentGoblin>.Instance.Creator = () => { return new ComponentGoblin(); };
		}

		public static ComponentGoblin ComponentGoblin(in this ent entity)
		{
			return Storage<ComponentGoblin>.Instance.components[entity.id];
		}

	}

В HelperComponents два метода. Первый делает настройки во фреймворке. Раньше хранилища компонентов создавали компоненты как generic T(); Это сильно дороже и дольше чем создать нужный класс по его типу. Метод init() передает функцию которая бы позволяла создавать компонент оптимальнее.

Второй метод просто позволяет обращаться к компоненту через сущность:

entity.ComponentGoblin();

Actor и Monocached

Практически не претерпели изменений оба класса, однако Актор больше не наследуется от Monocached. Если нужно получить ссылку на сущность с объекта unity. Ищите интерфейс IEntity. И актор и монокешд обладают этим интерфейсом.

Так же Actor держит ссылку на ассет blueprint. О работе с ним распишу отдельно. Классы наследуемые от Actor так же можно использовать в качестве "View" классов ( прослойке для работы на стороне юнити с ее компонентами )

Blueprint

Распишу в ближайшем будущем. Blueprint - это ассет с базовыми настройками компонентов сущности. Он во многом похож на актора, но если актор это компонент висящий на префабе, то blueprint это SO ассет создающий сущность с компонентами. Blueprints очень легко визуально настраиваются если есть инспектор ODIN.

Например так бы выглядело в коде создание сущности героя.

var entity = ent.CreateFor("Obj Player");
entity.Add<ComponentMotion>();
entity.Add<ComponentWeapon>();
entity.Add<ComponentJump>();
entity.Add<ComponentHealth>();
entity.Add<ComponentStealth>();
entity.Add<ComponentMagicAbility>();

Если бы у нас был блюпринт героя то это выглядело бы так:

var entity = ent.CreateFor(Blueprints.Player);
Clone this wiki locally