Entity Component System на основе движка


9

Примечание: я программирую это в Javascript, но в большинстве случаев это не зависит от языка.

Я думаю о преобразовании моего двигателя в ECS.

Я понял основную идею ( примечание: это неправильно, см. Мой ответ ):

Сущности - это игровые объекты.
Компоненты - это биты функций ( reactToInput()) или состояний ( position), которые могут быть «приклеены» к объектам.
У систем есть список объектов, которыми они управляют и которые обновляют.

Но я не совсем уверен, что я получу реализацию и некоторые детали ...

Вопрос: может ли система работать на разных типах объектов? Я обычно привожу пример класса, называемого Sceneв моем движке, и он будет служить этой цели и сейчас. Сцена - это контейнер всех объектов, которые можно рендерить, обновлять, влиять на рендеринг (источники света) и, возможно, в будущем даже 2DSoundEmitterобъекты. Он имеет высокоуровневый интерфейс, поэтому пользователю не нужно беспокоиться о типе объекта, который он использует scene.add(), и о подобных вещах.

Я понимаю, что это Sceneможет быть система. Он принимает сущности, сохраняет их, а затем может вызывать их методы обновления и, возможно, даже вносить некоторые изменения состояния. Но есть проблема: как я описал выше, Sceneможно кормить разные типы объектов! Что я должен делать, скажем, в ситуации, когда в сцене есть как визуализируемые объекты («нарисованные объекты»), так и источники света? Должен ли я сделать проверку типа объектов перед взаимодействием? Или я должен решить его на еще более низком уровне: сделать LightSourceкомпонент , который может быть добавлен к любому объекту, и свет будет просто лицо с LightSourceи Positionкомпонентами. Это приемлемо?

Кроме того, стоит ли по-прежнему использовать обычное наследование и традиционные классы? Например, я просто не могу понять, кем бы я Rendererбыл! Это не система, поскольку ее единственная функция - снимать камеру и сцену, визуализировать все и применять эффекты (например, тени). Он также управляет контекстом, шириной и высотой игры, делает переводы ... Но это еще не система!

Изменить: не могли бы вы связать какие-либо ресурсы, которые вы нашли в ECS? У меня проблемы с поиском хороших.


2
Вместо того, чтобы публиковать ответ на этой странице, я просто дам ссылку: gamedev.stackexchange.com/questions/23533/… Сущность не должна быть получена, любые различия между сущностями должны быть достигнуты с помощью компонентов. Как правило, вам потребуется интерфейс для каждой основной системы (рендеринг, физика, работа в сети, ввод, аудио и т. Д.). Способ, которым я настроил мой рендерер, состоит в том, чтобы запросить сцену для рендерируемых сущностей, и менеджер сцены затем запрашивает информацию о рендеринге у каждой сущности, у которой есть компонент рендеринга.
Ник Фостер

1
Дизайн компонентов в блоге T = Machine (так как вы попросили хороший)
Джон Макдональд

Код и обсуждение структуры сущностей: gamadu.com/artemis
Патрик Хьюз

@JohnMcDonald, я написал комментарий к этой статье, хотя он ожидает модерации. Вы можете увидеть это здесь: t-machine.org/index.php/2007/12/22/… . Я "Яннбейн".
Jcora

Кроме того, @NicFoster, статья, на которую Джон ссылался на T = Machine, описывает что-то отличное от вашего ответа ... Для этого Дэйва у сущностей нет списка компонентов, они просто имя. Как "flsjn304" - это сущность. Хранится «где-то». И я должен перечитать эту вещь, чтобы понять, хранит ли он компоненты внутри систем , что мне кажется очень странным!
Jcora

Ответы:


6

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

Прежде всего, о факторе «не используйте ООП», помните, что объекты JavaScript похожи на playdough по сравнению с другими языками, и вам действительно нужно изо всех сил создать кошмар каскадной схемы наследования, поскольку JS не класс на основе и композитинг приходит гораздо более естественным образом. Если вы внедряете в JS какую-то глупую классную или прототипную систему ручной работы, подумайте об отказе от нее. В JS мы используем замыкания, прототипы и передаем функции, как конфеты. Это отвратительно, грязно и неправильно, но также мощно, кратко, и так нам нравится.

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

Я не знаю, почему так много программистов любят это делать (особенно Java-парни, пишущие JavaScript по какой-то причине), но это ужасно, неразборчиво и совершенно не поддерживается, когда используется в избытке. Наследование хорошо здесь и там, но не обязательно в JS. В языках, где это более заманчивый ярлык, он действительно должен быть зарезервирован для более абстрактных архитектурных задач, а не для более буквальных схем моделирования, таких как frankensteining реализация зомби через цепочку наследования, которая включала BunnyRabbit, потому что это работало. Это не очень хорошее повторное использование кода. Это кошмар обслуживания.

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

  • Сущность - конкретная вещь, которую вы разрабатываете. Мы говорим больше в направлении имен собственных (но не на самом деле, конечно). Не «сцена», а «IntroAreaLevelOne». IntroAreaLevelOne может находиться в каком-либо блоке sceneEntity, но мы концентрируемся на чем-то конкретном, что отличается от других связанных вещей. В коде сущность на самом деле является просто именем (или идентификатором), связанным с кучей вещей, которые необходимо внедрить или установить (компоненты), чтобы быть полезными.

  • Компоненты - типы вещей, в которых нуждается организация. Это общие существительные. Нравится WalkingAnimation. В рамках WalkingAnimation мы можем получить более конкретную информацию, например, «Shabing» (хороший выбор для зомби и растительных монстров) или «ChickenWalker» (отлично подходит для роботов с обратной связью ed-209ish). Примечание: Не уверен, как это могло бы отделить от рендеринга 3D-модели, подобной этой, так что, может быть, это дерьмовый пример, но я скорее профессионал JS, чем опытный разработчик игр. В JS я бы поместил механизм сопоставления в одну коробку с компонентами. Компоненты сами по себе, вероятно, будут лёгкими в логике и большей части дорожной карты, сообщающей вашим системам, что следует реализовывать, если системы даже необходимы (в моей попытке ECS некоторые компоненты являются просто наборами наборов свойств). Как только компонент установлен, он '

  • Системы - Настоящая программа мяса здесь. Системы ИИ построены и связаны, рендеринг достигнут, последовательности анимаций установлены и т. Д. Я собираюсь выделить их, оставляя в основном воображению, но в примере System.AI принимает множество свойств и выплевывает функцию, которая используется для добавления обработчиков событий к объекту, который в конечном итоге используется в реализации. Ключевым моментом в System.AI является то, что он охватывает несколько типов компонентов. Вы могли бы разобрать все вещи ИИ с одним компонентом, но сделать это - неправильно понять смысл детализации.

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

Так что в JS может быть как то так. Разработчики игр, пожалуйста, скажите мне, если я неправильно понял:

//I'm going with simple objects of flags over arrays of component names
//easier to read and can provide an opt-out default
//Assume a genre-bending stealth assassin game

//new (function etc... is a lazy way to define a constructor and auto-instantiate
var npcEntities = new (function NpcEntities(){

    //note: {} in JS is an object literal, a simple obj namespace (a dictionary)
    //plain ol' internal var in JS is akin to a private member
    var default={ //most NPCs are humanoids and critters - why repeat things?
        speedAttributes:true,
        maneuverAttributes:true,
        combatAttributes:true,
        walkingAnimation:true,
        runningAnimation:true,
        combatAnimation:true,
        aiOblivious:true,
        aiAggro:true,
        aiWary:true, //"I heard something!"
        aiFearful:true
    };

    //this. exposes as public

    this.zombie={ //zombies are slow, but keep on coming so don't need these
        runningAnimation:false,
        aiFearful:false
    };

    this.laserTurret={ //most defaults are pointless so ignore 'em
        ignoreDefault:true,
        combatAttributes:true,
        maneuverAttrubtes:true, //turning speed only
    };
    //also this.nerd, this.lawyer and on and on...

    //loop runs on instantiation which we're forcing on the spot

    //note: it would be silly to repeat this loop in other entity collections
    //but I'm spelling it out to keep things straight-forward.
    //Probably a good example of a place where one-level inheritance from
    //a more general entity class might make sense with hurting the pattern.
    //In JS, of course, that would be completely unnecessary. I'd just build a
    //constructor factory with a looping function new objects could access via
    //closure.

    for(var x in npcEntities){

        var thisEntity = npcEntities[x];

        if(!thisEntity.ignoreDefaults){

            thisEntity = someObjectXCopyFunction(defaults,thisEntity);
            //copies entity properties over defaults

        }
        else {
            //remove nonComponent property since we loop again later
            delete thisEntity.ignoreDefaults;
        }
    }
})() //end of entity instantiation

var npcComponents = {
    //all components should have public entityMap properties

    //No systems in use here. Just bundles of related attributes
    speedAttributes: new (function SpeedAttributes(){
        var shamblingBiped = {
            walkingAcceleration:1,
            topWalking:3
        },
        averageMan = {
            walkingAcceleration:3,
            runningAcceleration:4,
            topWalking: 4,
            topRunning: 6
        },
        programmer = {
            walkingAcceleration:1,
            runningAcceleration:100,
            topWalking:2
            topRunning:2000
        }; //end local/private vars

        //left is entity names | right is the component subcategory
        this.entityMap={
            zombie:shamblingBiped,
            lawyer:averageMan,
            nerd:programmer,
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(), //end speedAttributes

    //Now an example of an AI component - maps to function used to set eventHandlers
    //functions which, because JS is awesome we can pass around like candy
    //I'll just use some imaginary systems on this one

    aiFearful: new (function AiFearful(){
        var averageMan = Systems.AI({ //builds and returns eventSetting function
            fearThreshold:70, //%hitpoints remaining
            fleeFrom:'lastAttacker',
            tactic:'avoidIntercept',
            hazardAwareness:'distracted'
        }),
        programmer = Systems.AI({
            fearThreshold:95,
            fleeFrom:'anythingMoving',
            tactic:'beeline',
            hazardAwareness:'pantsCrappingPanic'
        });//end local vars/private members


         this.entityMap={
            lawyer:averageMan,
            nerd:averageMan, //nerds can run like programmers but are less cowardly
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(),//and more components...

    //Systems.AI is general and would get called for all the AI components.
    //It basically spits out functions used to set events on NPC objects that
    //determine their behavior. You could do it all in one shot but
    //the idea is to keep it granular enough for designers to actually tweak stuff
    //easily without tugging on developer pantlegs constantly.
    //e.g. SuperZombies, zombies, but slightly tougher, faster, smarter
}//end npcComponents

function createNPCConstructor(npcType){

    var components = npcEntities[npcType],

    //objConstructor is returned but components is still accessible via closure.

    objConstructor = function(){
        for(var x in components){
            //object iteration <property> in <object>

            var thisComponent = components[x];

            if(typeof thisComponent === 'function'){
                thisComponent.apply(this);
                //fires function as if it were a property of instance
                //would allow the function to add additional properties and set
                //event handlers via the 'this' keyword
            }
            else {
                objConstructor.prototype[x] = thisComponent;
                //public property accessed via reference to constructor prototype
                //good for low memory footprint among other things
            }
        }
    }
    return objConstructor;
}

var npcBuilders= {}; //empty object literal
for (var x in npcEntities){
    npcConstructors[x] = createNPCConstructor(x);
}

Теперь в любое время, когда вам нужен NPC, вы можете строить с npcBuilders.<npcName>();

Графический интерфейс может подключаться к объектам npcEntities и компонентов и позволяет дизайнерам настраивать старые объекты или создавать новые, просто смешивая и сопоставляя компоненты (хотя там нет механизма для компонентов не по умолчанию, но специальные компоненты могут быть добавлены на лету в код, пока существует определенный компонент для него.


Глядя на это шесть лет спустя, я не уверен, что понимаю свой ответ. Можно ли это улучшить?
Эрик Реппен

1

Я читал об Entity Systems в статьях, которые любезно предоставили люди в комментариях, но у меня все еще были некоторые сомнения, поэтому я задал еще один вопрос .

Во-первых, мои определения были неверны. Сущности и компоненты - просто тупые держатели данных, в то время как системы предоставляют все функциональные возможности.

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

SceneКласс я говорил не должна быть система. Тем не менее, он должен быть центральным менеджером, который может хранить все объекты, облегчать сообщения и, возможно, даже управлять системами. Он также может функционировать как своего рода фабрика для сущностей, и я решил использовать его таким образом. Он может принимать любой тип объекта, но затем он должен передавать этот объект в соответствующую систему (которая по соображениям производительности не должна выполнять никаких проверок типов, если только они не являются побитовыми).

Я не должен использовать ООП при реализации ES, предлагает Адам, но я не вижу причин, чтобы не иметь объектов с методами как для сущностей, так и для компонентов, а не только для глупых держателей данных.

RendererМожет просто быть реализован в виде системы. Он будет вести список draw()доступных для рисования объектов и каждые 16 мс будет вызывать метод их компонента рендеринга.


1
«Сущности и компоненты - просто тупые держатели данных, в то время как системы предоставляют все функциональные возможности», «вызывайте метод draw () их компонента рендеринга», вы все еще в замешательстве, кроме того, метод «draw» в целом побеждает цель системы рендеринга. Также я не понимаю, почему ваш граф Scene не может быть частью Renderer, это просто удобный инструмент, вы всегда можете реализовать свой «рисуемый» компонент как узел. Заставлять граф сцены отвечать больше, чем за сцену, просто не нужно, и я уверен, что это будет беспорядок для отладки.
dreta

@dreta, рендерер в настоящее время (реализация движка не-ES) выполняет преобразования, изменения камеры, альфа-компоненты, и в будущем он будет рисовать различные эффекты, графический интерфейс и тени. Просто казалось естественным группировать такие вещи. Разве сцена не должна нести ответственность за создание объектов хранения? Или что-то еще должно хранить их? Часть создания, вероятно, представляет собой всего лишь несколько строк, объединяющих предоставленные пользователем компоненты, на самом деле она вообще ничего не «создает», а просто создает экземпляры.
Jcora

не каждый объект является визуализируемым, не каждый объект может сталкиваться или издавать звук, с вашим объектом сцены вы делаете экстремальную связь, почему? это будет просто боль писать и отлаживать. Объекты используются для идентификации объекта, компоненты содержат данные, а системы работают с данными. Зачем вам смешивать все это вместе, вместо того, чтобы иметь надлежащие системы, такие как RenderingSystem и SoundSystem, и беспокоить эти системы, только если у объекта есть все необходимые компоненты.
dreta

1
отбрасывание теней обычно привязано к источникам света, хотя вы можете просто создать компонент "CastsShadow" и искать его при рендеринге динамических объектов. если вы делаете 2D, то это просто основной вопрос заказа, простой алгоритм художника решит эту проблему для вас. ТБХ, ты беспокоишься слишком рано. Вы поймете это, когда придет время сделать это, и у вас на уме будет только это, сейчас вы просто путаете себя. Вы не можете надеяться сделать все правильно с первого раза, просто этого не произойдет. ты перейдешь этот мост, когда доберешься до него.
dreta

1
«Объекты и компоненты - просто тупые держатели данных, в то время как системы предоставляют все функциональные возможности». Не обязательно. Они в подходах некоторых людей. Но не другие. Посмотрите на движок Unity - все поведение в компонентах.
Kylotan

-2

Введение в управление зависимостями 101.

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

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

Это идет по фразе: « newэто клей».

Я буду демонстрировать это в C #.

public interface IEntity
{
    int[] Position { get; }
    int[] Size { get; }
    bool Update();
    void Render();
}

public interface IRenderSystem
{
    void Draw(IEntity entity);
}

public interface IMovementSystem
{
    bool CanMoveLeft();
    void MoveLeft();
    bool CanMoveRight();
    void MoveRight();
    bool CanMoveUp();
    void MoveUp();
    bool CanMoveDown();
    void MoveDown();
    bool Moved();
    int[] Position { get; set; }
}

public interface IInputSystem
{
    string Direction { get; set; }
}

public class Player : IEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public bool Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();

        return _movementSystem.Moved();
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

Это базовая система ввода, перемещения и рендеринга. В Playerэтом случае класс является сущностью, а компоненты - интерфейсами. PlayerКласс ничего не знает о внутреннем , как конкретная IRenderSystem, IMovementSystemили IInputSystemработе. Однако интерфейсы предоставляют способ Playerотправки сигналов (например, вызов Draw в IRenderSystem), независимо от того, как достигнут конечный результат.

Например, возьмите мою реализацию IMovementSystem:

public interface IGameMap
{
    string LeftOf(int[] currentPosition);
    string RightOf(int[] currentPosition);
    string UpOf(int[] currentPosition);
    string DownOf(int[] currentPosition);
}

public class MovementSystem : IMovementSystem
{
    private readonly IGameMap _gameMap;
    private int[] _previousPosition;
    private readonly int[] _currentPosition;
    public MovementSystem(IGameMap gameMap, int[] initialPosition)
    {
        _gameMap = gameMap;
        _currentPosition = initialPosition;
        _previousPosition = initialPosition;
    }

    public bool CanMoveLeft()
    {
        return _gameMap.LeftOf(_currentPosition) == "Unoccupied";
    }

    public void MoveLeft()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]--;
    }

    public bool CanMoveRight()
    {
        return _gameMap.RightOf(_currentPosition) == "Unoccupied";
    }

    public void MoveRight()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]++;
    }

    public bool CanMoveUp()
    {
        return _gameMap.UpOf(_currentPosition) == "Unoccupied";
    }

    public void MoveUp()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]--;
    }

    public bool CanMoveDown()
    {
        return _gameMap.DownOf(_currentPosition) == "Unoccupied";
    }

    public void MoveDown()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]++;
    }

    public bool Moved()
    {
        return _previousPosition == _currentPosition;
    }

    public int[] Position
    {
        get { return _currentPosition; }
    }
}

MovementSystemможет иметь свои зависимости и Playerдаже не заботиться. С помощью интерфейсов можно создать игровой автомат:

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IEntity>();
        foreach (var entity in _entities)
        {
            if(entity.Update())
                _renderQueue.Add(entity);
        }
        // Linq version for those interested
        //_renderQueue.AddRange(_entities.Where(e => e.Update()));
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

И это начало прекрасной игры (которая также может быть проверена модулем).

И с несколькими дополнениями и некоторым полиморфизмом:

public interface IEntity
{
}

public interface IRenderableEntity : IEntity
{
    void Render();        
}

public interface IUpdateableEntity : IEntity
{
    void Update();
    bool Updated { get; }
}

public interface IRenderSystem
{
    void Draw(IRenderableEntity entity);
}

// new player class
public class Player : IRenderableEntity, IUpdateableEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public void Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();
    }

    public bool Updated
    {
        get { return _movementSystem.Moved(); }
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IRenderableEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IRenderableEntity>();
        foreach (var entity in _entities)
        {
            if (entity is IUpdateableEntity)
            {
                var updateEntity = entity as IUpdateableEntity;
                updateEntity.Update();
            }

            if (entity is IRenderableEntity)
            {
                var renderEntity = entity as IRenderableEntity;
                _renderQueue.Add(renderEntity);
            }
        }
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

Теперь у нас есть примитивная система сущностей / компонентов, основанная на агрегирующих интерфейсах и свободном наследовании.


1
Это так против дизайна компонентов :) Что бы вы сделали, если бы вы хотели, чтобы один игрок издавал звуки, а другой - нет?
Кикаймару

@Kikaimaru Пройдите в ISoundSystem, которая не воспроизводит звук. т.е. ничего не делает.
Дастин Кинген

3
-1, не потому, что это плохой код, а потому, что он вообще не имеет отношения к архитектуре на основе компонентов - на самом деле это стремление избежать таких распространенных интерфейсов, как этот.
Kylotan

@ Килотан Полагаю, мое понимание неверно.
Дастин Кинген
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.