-
Notifications
You must be signed in to change notification settings - Fork 76
(RU) Components
Актуально (06.05.2020)
Компонент - это кирпич игры описывающий некий набор переменных. Наборы компонентов формируют контекст сущности и хранят ее состояние. Во фреймворке компоненты часто пишутся с атрибутом [Serializable] для того чтобы редактировать поля в инспекторе Unity.
Помимо самого класса компонента данных обязательно нужно объявить класс хранилища компонентов (зона хелперов в примере).
Начинайте имя компонента с
Component
, в противном случае ломается debug-режим фреймворка (если для вас это не подходит, то переключить в release можно с помощью пункта меню Actors в Unity)
using System;
using Pixeye.Actors;
namespace Pixeye.Source
{
[Serializable]
public class ComponentHealth
{
public int val;
}
#region HELPERS
public static partial class Component
{
public const string Health = "Pixeye.Source.ComponentHealth";
internal static ref ComponentHealth ComponentHealth(in this ent entity)
=> ref Storage<ComponentHealth>.components[entity.id];
}
sealed class StorageComponentHealth : Storage<ComponentHealth>
{
public override ComponentHealth Create() => new ComponentHealth();
public override void Dispose(indexes disposed)
{
foreach (int id in disposed)
{
ref var component = ref components[id];
component.val = 0;
}
}
}
#endregion
}
Где {FullName} - имя компонента. Самым удобным способом будет создать шаблон внутри вашего IDE для быстрого создания компонентов.
using Pixeye.Actors;
namespace Pixeye.Source
{
public class ${FullName}
{
}
#region HELPERS
public static partial class Component
{
public const string $end = "Pixeye.Source.$FullName";
internal static ref ${FullName} ${FullName}(in this ent entity)
=> ref Storage<${FullName}>.components[entity.id];
}
sealed class Storage${FullName} : Storage<${FullName}>
{
public override ${FullName} Create() => new ${FullName}();
public override void Dispose(indexes disposed)
{
foreach (var id in disposed)
{
ref var component = ref components[id];
}
}
}
#endregion
}
Visual Studio
Shortcut для вызова сниппета - comp
. После двойного нажатия TAB вам нужно ввести имя компонента (без префикса Component, он уже прописан), после чего вы можете нажать снова TAB и ввести namespace, в котором находится класс компонента данных (или отредактируйте сниппет, исключив это).
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Класс компонента данных</Title>
<Shortcut>comp</Shortcut>
<Description>Шаблон компонента данных</Description>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Imports>
<Import>
<Namespace>Pixeye.Actors</Namespace>
</Import>
</Imports>
<Declarations>
<Literal>
<ID>shortname</ID>
<ToolTip>Имя компонента без суффиксов</ToolTip>
<Default>Name</Default>
</Literal>
<Literal>
<ID>namespace</ID>
<ToolTip>Неймспэйс в котором находится класс компонента данных</ToolTip>
<Default>Namespace</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[
public class Component$shortname$
{
$end$
}
#region HELPERS
public static partial class Component
{
public const string $shortname$ = "$namespace$.Component$shortname$";
internal static ref Component$shortname$ Component$shortname$(in this ent entity)
=> ref Storage<Component$shortname$>.components[entity.id];
}
sealed class Storage$shortname$ : Storage<Component$shortname$>
{
public override Component$shortname$ Create() => new Component$shortname$();
public override void Dispose(indexes disposed)
{
foreach (var id in disposed)
{
ref var component = ref components[id];
//dispose (reset) logic
}
}
}
#endregion
]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
Чтобы использовать сниппет, разместите файл с расширением .snippet по расположению %USERPROFILE%\Documents\Visual Studio 2019\Code Snippets\Visual C#\My Code Snippets.
Руководство по сниппетам. (доп.)
Live Template in Rider
Перейдите в Settings -> Editor -> Live template
. Выберите C#, нажмите New template
(справа ввверху). Заполните поле кода сниппета:
public class Component$shortname$
{
$end$
}
#region HELPERS
public static partial class Component
{
public const string $shortname$ = "$namespace$.Component$shortname$";
internal static ref Component$shortname$ Component$shortname$(in this Pixeye.Actors.ent entity)
=> ref Storage<Component$shortname$>.components[entity.id];
}
sealed class Storage$shortname$ : Storage<Component$shortname$>
{
public override Component$shortname$ Create() => new Component$shortname$();
public override void Dispose(Pixeye.Actors.indexes disposed)
{
foreach (var id in disposed)
{
ref var component = ref components[id];
//dispose (reset) logic
}
}
}
#endregion
Настройте сниппет:
Расположите переменные в удобном для вас порядке их ввода (как на картинке). Также нажмите change macro
и выберите containing type name для переменной shortname
:
Настройте change macro
для namespace
:
Руководство по сниппетам.
VS Code
Shortcut для вызова сниппета - comp
. После чего нажатием TAB или мышкой выбираем его, вводим имя компонента (без префикса Component, он уже прописан) - готово.
Настройка сниппета: File>Preferences>User Snippets
выбираем csharp
, затем вставляем этот "код"(разметку json) в файл. Форматирование уже заботливо выполнено мной, но если вам не нравится перенос строки, то: SHIFT+ALT+F.
P.S. это глобальный файл для всех C# сниппетов
"Component": {
"prefix": "сomp",
"body": [
"using System;",
"using Pixeye.Actors;",
"namespace Pixeye.Source",
"{",
"\t[Serializable]",
"\tpublic class Component${1:FullName}",
"\t{",
"\t\t//Data",
"\t}",
"#region HELPERS",
"\tpublic static partial class Component",
"\t{",
"\t\tpublic const string ${1:FullName} = \"Pixeye.Source.Component${1:FullName}\";",
"\t\tinternal static ref Component${1:FullName} Component${1:FullName}(in this ent entity) \n\t\t\t\t=> ref Storage<Component${1:FullName}>.components[entity.id];",
"\t}",
"\tsealed class StorageComponent${1:FullName} : Storage<Component${1:FullName}>",
"\t{",
"\t\tpublic override Component${1:FullName} Create() => new Component${1:FullName}();",
"\t\tpublic override void Dispose(indexes disposed)",
"\t\t{",
"\t\t\tforeach (var id in disposed)",
"\t\t\t{",
"\t\t\t\tref var component = ref components[id];",
"\t\t\t}",
"\t\t}",
"\t}",
"#endregion",
"}"
],
"description": "New Actors Component with Helper"
}
Rider
Воспользуйтесь этим меню:
Если этого пункта нет, то перейдите в Settings/Preferences -> Editor | File Templates | Other Languages
. Нажмите +
, укажите расширение .cs
в соответствующем поле.
Укажите текст шаблона:
#set($FullName = ${NAME})
#set($end = $FullName.substring(9))
#set($component = $FullName.substring(1))
using Pixeye.Actors;
namespace Pixeye.Source
{
public class ${FullName}
{
}
#region HELPERS
public static partial class Component
{
public const string $end = "Pixeye.Source.$FullName";
internal static ref ${FullName} c$component(in this ent entity)
=> ref Storage<${FullName}>.components[entity.id];
}
sealed class Storage${FullName} : Storage<${FullName}>
{
public override ${FullName} Create() => new ${FullName}();
public override void Dispose(indexes disposed)
{
foreach (var index in disposed)
{
ref var component = ref components[index];
}
}
}
#endregion
}
Руководство по File Templates.
Create->Actors->Add->Component
Проведу аналогию с Unity: раньше мы делали что-то вроде gameobject.GetComponent<{НАЗВАНИЕ}>()
- gameobject
выступал в роли сущности. Теперь gameobject
сам является лишь частью сущности.
Через сущность мы можем попытаться обратиться к любому компоненту:
var entity = 1; // берем первую сущность на сцене
//этот метод-расширение определен в хелпере
var cObject = entity.ComponentObject(); // напрямую берем ComponentObject
Проверить наличие компонента у сущности:
var entity = 1; // берем первую сущность на сцене
if (entity.Has<ComponentObject>()) {} // проверяем наличие
Одновременно достать компонент и проверить его наличие.
var entity = 1; // берем первую сущность на сцене
ComponentObject cObject;
if (entity.Get<ComponentObject>(out cObject)) {
// do stuff with cObject
}
Одновременно достать несколько компонентов и проверить их наличие.
ComponentAnimation cAnimation;
ComponentMotion cMotion;
if (entity.Get(out cAnimation, out cMotion))
{
}
Advanced
Если мы не хотим использовать хелперы (из-за проблем с переименованием или по религиозным соображениям), то можем воспользоваться атрибутом [ActorsComponent]
:
using Pixeye.Actors;
using UnityEngine;
namespace Components
{
[ActorsComponent]
public class ComponentClick
{
public string Id;
public Vector2 Point;
}
}
Быстрый доступ из групп к компоненту через метод-расширение сломается, так как до он был определен в коде хелпера, но теперь:
var cClick = entity.ComponentClick()// вызовет ошибку компилляции
Этот метод ничего не мешает написать снова, но тогда остается проблема с переименованием(нужно изменить имя метода-расширения вместе с именем компонента) и порция дополнительного кода.
Поэтому для доступа к компоненту из групп используйте (самый быстрый способ):
var cClick = Storage<ComponentClick>.components[entity.id];
Или напишите универсальные методы для доступа:
public static class Component
{
/*Возвращает компонент сущности, не проверяя его наличия.
* (группа гарантирует наличие компонента)
*/
public static ref T FromGroup<T>(in this ent entity)
=> ref Storage<T>.components[entity.id];
/*Возвращает компонент сущности в component-переменной.
* Ограничение where T: class существует, чтобы предостеречь
* от возврата компонента по значению, если он явл. структурой.
* Используйте для компонентов-структур предыдущий метод.
*/
public static void FromGroup<T>(in this ent entity, out T component) where T: class
=> component = Storage<T>.components[entity.id];
}
Тогда обращение к компоненту будет выглядеть так:
entity.FromGroup(out ComponentInput inputData);
Создание нового компонента через entity.Set<T>()
станет дороже, т.к. будет использоваться реализация Storage<T>.Create
по умолчанию - Activator.CreateInstance<T>
. Это медленнее, чем прямой вызов конструктора. В местах первых массовых инициализаций сущностей можно использовать entity.Set<T>(T component)
:
//создаем компонент вручную, минуя дефолтный Activator.CreateInstance<T>()
var cPlayer = new ComponentPlayer();
entity.Set(cPlayer);
Dispose остается не реализованным, поэтому за сброс значений компонентов сущности после entity.Release()
отвечаете вы. В entity.Set<T>()
вам может попасться старый компонент от релизнутой сущности (со старыми данными). Благодаря этому entity.Set<T>()
не всегда будет использовать Activator.CreateInstance<T>
, а брать компоненты из пула, что очень дешево. Поэтому применение entity.Set<T>(T component)
целесообразно, только когда пул пуст (например, в начале игры) и нужно инициализировать большое кол-во сущностей.