
Организация объектов, или немного архитектуры.
Здравствуйте! В этой статье я бы хотел поделится с вами своими наработками в области создания базовой архитектуры для удобного создания и взаимодействия игровых объектов. Возможно, принцип который я опишу кто-то из вас уже применяет(так как он весьма логичен), возможно это окажется просто каким-нибудь паттерном(в которых я не очень осведомлен), но возможно для кого-то, особенно для новичков, эта информация и данный подход окажутся новыми и весьма полезными. Так же хотелось бы услышать критику со стороны бывалых программистов, ну и конечно же просто советы, так же поскольку эта статья рассчитана в основном именно на новичков, все будет расписано очень подробно, возможно даже излишне подробно.
И так давайте рассмотрим код класса BasicObject(особо не вникать, дальше подробнее!):
И так, как видите, в коде нет ничего сложного и уникального, но я все же объясню его. Первое что можно сказать это то, что присутствуют 4 основные функции, которые определяют жизненный цикл любого игрового объекта:
1) объект “рождается” — BasicObject();
2) объект инициализируется и начинает “жить” в игровом мире — init();
3) объект “живет”, данная функция вызывается каждый кадр — update();
4) объект “умирает” — destroy();
Как вы уже поняли экземпляры класса BasicObject напрямую никогда не создаются, он служит лишь “родителем” для всех остальных классов игровых объектов. Таким образом класс который наследуется от BasicObject просто переопределят все эти методы и в них уже добавляет свой уникальный и необходимый функционал.
Перед тем как перейти к напосредственному описанию кода в функциях, я расскажу о используемых переменных данного класса:
1) переменная isExist, как несложно догадаться определяет “жив ли” объект.
2) переменная container, это уже интереснее, данная переменная служит для того что бы хранить ссылку на нужный “слой” на котором будет отрисовываться данный объект. Дело в том что в большинстве случаев конкретный объект имеет конкретный “слой” на протяжении всей игры, так зачем же каждый раз, при создании объекта, вручную писать: слой.addChild(«объект»), проще сразу указать ссылку на “слой” при инициализации объекта.
3) переменная sprite — это графическое представление вашего объекта, в примере я использовал простой MovieClip, однако возможно вы используете какие-либо другие классы для вывода графики(например для растризации), тогда данная переменная имеет соответствующий тип.
4) переменная id — это переменная служит для идентификации объекта, используемый самописный класс Id я опишу позже, но вне зависимости от того, будете вы его применять или напишите свой, суть одна: в классе BasicObject должна присутствовать какая-либо система идентификации объектов, она необходима для всевозможных выборок, которые постоянно необходимо производить в ходе написания геймплея игры.
5) переменная _universe — это переменная представляет собой ссылку на класс игрового мира, в моем случае это Universe, его код я приводить не буду, однако скажу для чего он служит. Класс Universe это “ядро” игры, в нем хранятся и обрабатываются все игровые объекты, а так же, в доступном виде, хранятся все основные параметры для реализации игры. Как пример, в публичном виде хранятся “слои” на которых отрисовываются все игровые объекты, вообщем этот класс не имеет большого значения, вы можете использовать любой свой, так как класс такого типа присутствует я думаю у каждого.
Теперь я приведу код функций с комментариями, проблем в понимании возникнуть не должно:
Функция — конструктор:
Функция — init:
Функция — update:
Функция — destroy:
Как видите все просто и понятно, однако перед тем ка я покажу на примере как с использованием данного класса теперь легко реализуются любые игровые сущности, я все-таки упомяну про класс Id.
1. Каждый объект Id может иметь принадлежность к какой-то группе(обычно использую если надо разделить на “свои” “чужие”, GROUP_A, GROUP_B, GROUP_N(нейтральная)).
2. Каждый объект Id имеет набор меток, типов, которые служат для его характеризации и позволяют в последствии делать гибкие выборки, например: объект вида танк, так же он разрушаемый, а еще он особо опасен, то есть у него будет 3 метки(TYPE_TANK, TYPE_DESTROYABLE, TYPE_DENGER_HIGHT), для каждого геймплея это может быть свой набор меток, и самое главное что у всех объектов они могут различаться и нет необходимости характеризовать объект по всем параметрам которые присутствуют в других объектах.
3. Каждый объект Id имеет уникальный тег, обычно данный тег характеризует определенный вид объектов, например если у вас есть игровой объект Tank, то все экземпляры будут иметь тег — TAG_TANK.
Все эти группы, типы и теги являются простыми уникальными числовыми константами.
Таким образом при необходимости можно отбирать только те объекты которые нужны, например при разрыве гранаты: получаем все объекты в заданном радиусе и отбираем только те, которые имеют тип — TYPE_DESTROYABLE, ну и наносим урон.
Что бы каждый раз не проверять все вручную, я так же написал примитивный класс IdFilter, который как раз и служит для создания всевозможных выборок. Данный класс просто хранит 3 массива, массив для групп, для типов и для тегов. В классе же Id написано пару методов которые проверяют переданный им IdFilter и возвращают true или false в зависимости от того, подходят они под описание фильтра или нет. Таким образом что бы произвести выборку, достаточно создать фильтр с нужными параметрами и проверить id всех необходимых объектов на данный фильтр. К слову данные классы совершенно не доработаны и имеют лишь тот функционал который был необходим мне, но так как они очень просты их доработка не составит особого труда, а может быть вам хватит и этого, так как принципы по которым строится идентификация на мой взгляд очень удачны.
Id код:
IdFilter код:
Это простой пример сущности на основе BasicObject, для чего он нужен. Так как это учебный пример, то по задумке, при нашей “виртуальной компиляции” на экране должен появится объект, который имеет вид Droid_mc и начать двигаться вправо с заданной скоростью speed. Чтобы это произошло надо соответственно создать объект типа Droid и вызвать функцию init((), как-то так:
Но так мы не задали параметр speed нашего объекта, задаем его соответственно так:
Если один параметр то ладно, а что если их 5 или еще больше? Писать каждый раз 5-7 строк на создание одного объекта на мой взгляд не очень удобно и практично, хорошо бы все начальный параметры конкретного объекта инициализировать функцией, но функция init уже есть и мы не можем изменить или расширить ее под конкретный объект, она имеет строго три параметра, так как же быть? И тут приходит очень красивое и логичное решение, создать в классе Droid статическую функцию, например create и передавать в нее помимо 3 параметров функции init все остальные необходимые параметры для конкретного объекта, а функция будет возвращать нам объект типа Droid:
Почему нету угла поворота? Предположим что объект всегда появляется и летит строго слева-направо, так зачем же каждый раз прописывать лишний параметр 0? Как раз этим и хороша функция create, что помимо банальной перезаписи параметров, в ней еще можно рассчитывать эти самые параметры(в данном случае я просто опустил угол поворота, что было бы невозможно при прямом вызове метода init). Например нам нужно что бы объект Droid каждый раз появлялся за пределами экрана и летел в центр, эти вычисления можно переложить на плечи create, которая рассчитает все необходимые значения, а затем создаст объект Droid на основе этих данных, все красиво, аккуратно и функционально. Теперь что бы создать объект типа Droid необходимо в ЛЮБОМ месте нашего кода написать всего ОДНУ строчку:
Так же плюсом еще является то, что согласно принципам инкапсуляции наличие большого количества публичных полей не является хорошим тоном, а используя метод create мы можем инициализировать приватные значения данного класса, то есть в нашем случае переменную speed можно сделать private.
Полный код класса Droid:
P.S. Если вас заинтересует данный подход, и он окажется не совсем «крамольным», то я написал бы вторую статью, только уже с подробным применением данного подхода на практике, то есть какой нить мини проект от начала и до конца.
P.P.S. Текста получилось много, так что мог что-то пропустить или накосячить, если что поправьте.
И так о чем это я:
Когда я более менее разобрался с ООП, передомной встала следующая задача: создать архитектуру позволяющую удобно организовать взаимодействие всех игровых объектов, под словом удобно я понимаю такие вещи, как гибкая выборка объектов, единая система хранения и обработки объектов, унифицирование многих утилитных методов, и т.д, вообщем создать систему которая не зависела бы от того, что я хочу сделать и кочевала бы из проекта в проект.Пункт 1: Основа
Основой все системы является всего один класс: BasicObject, все игровые объекты начиная от основных и заканчивая самыми простыми и примитивными наследуются от него. Что нам это дает? Плюсов не мало, но так как мы пока не рассмотрели код, сложно описать основные, но вот пример: имея базовый класс к которому можно привести любой объект, легко разрабатываются различные модули(так как класс BasicObject является очень абстрактным), например камера которая следит за объектом, используя данный подход можно без труда привязать камеру к чему угодно начиная от летящей ракеты и заканчивая фикусом на заднем фоне, так как все объекты основаны на одном классе располагающим основной информацией для всех объектов, таким образом используя единый класс во всех своих проектах вы с легкостью будете дописывать всевозможные модули и складывать их в свою “копилочку”, из которой всегда можно достать и без труда применить в новом проекте, так как основа всегда одна.И так давайте рассмотрим код класса BasicObject(особо не вникать, дальше подробнее!):
BasicObject
package src.game.objects
{
import flash.display.MovieClip;
import flash.display.Sprite;
import src.framework.Id;
import src.game.core.Universe;
public class BasicObject extends Sprite
{
// PUBLIC
public var isExist:Boolean;
public var container:MovieClip;
public var sprite:MovieClip;
public var id:Id;
// PROTECTED
protected var _universe:Universe;
public function BasicObject()
{
_universe = Universe.getInstance()
id = new Id();
}
public function init(posX:Number, posY:Number, rot:Number):void
{
if (container == null ) trace(this + " dont have container!");
else container.addChild(this);
this.x = posX;
this.y = posY;
this.rotation = rot;
if(sprite !== null)
{
this.addChild(sprite);
}
_universe.objects.add(this);
isExist = true;
}
public function update():void
{
}
public function destroy():void
{
if (isExist)
{
if (this.parent !== null)
{
this.parent.removeChild(this);
}
if (sprite !== null)
{
this.removeChild(sprite);
}
container = null;
sprite = null;
_universe.objects.remove(this);
isExist = false;
}
}
}
}
И так, как видите, в коде нет ничего сложного и уникального, но я все же объясню его. Первое что можно сказать это то, что присутствуют 4 основные функции, которые определяют жизненный цикл любого игрового объекта:
1) объект “рождается” — BasicObject();
2) объект инициализируется и начинает “жить” в игровом мире — init();
3) объект “живет”, данная функция вызывается каждый кадр — update();
4) объект “умирает” — destroy();
Как вы уже поняли экземпляры класса BasicObject напрямую никогда не создаются, он служит лишь “родителем” для всех остальных классов игровых объектов. Таким образом класс который наследуется от BasicObject просто переопределят все эти методы и в них уже добавляет свой уникальный и необходимый функционал.
Перед тем как перейти к напосредственному описанию кода в функциях, я расскажу о используемых переменных данного класса:
1) переменная isExist, как несложно догадаться определяет “жив ли” объект.
2) переменная container, это уже интереснее, данная переменная служит для того что бы хранить ссылку на нужный “слой” на котором будет отрисовываться данный объект. Дело в том что в большинстве случаев конкретный объект имеет конкретный “слой” на протяжении всей игры, так зачем же каждый раз, при создании объекта, вручную писать: слой.addChild(«объект»), проще сразу указать ссылку на “слой” при инициализации объекта.
3) переменная sprite — это графическое представление вашего объекта, в примере я использовал простой MovieClip, однако возможно вы используете какие-либо другие классы для вывода графики(например для растризации), тогда данная переменная имеет соответствующий тип.
4) переменная id — это переменная служит для идентификации объекта, используемый самописный класс Id я опишу позже, но вне зависимости от того, будете вы его применять или напишите свой, суть одна: в классе BasicObject должна присутствовать какая-либо система идентификации объектов, она необходима для всевозможных выборок, которые постоянно необходимо производить в ходе написания геймплея игры.
5) переменная _universe — это переменная представляет собой ссылку на класс игрового мира, в моем случае это Universe, его код я приводить не буду, однако скажу для чего он служит. Класс Universe это “ядро” игры, в нем хранятся и обрабатываются все игровые объекты, а так же, в доступном виде, хранятся все основные параметры для реализации игры. Как пример, в публичном виде хранятся “слои” на которых отрисовываются все игровые объекты, вообщем этот класс не имеет большого значения, вы можете использовать любой свой, так как класс такого типа присутствует я думаю у каждого.
Теперь я приведу код функций с комментариями, проблем в понимании возникнуть не должно:
Функция — конструктор:
Функция - конструктор
// Функция конструктор BasicObject.
public function BasicObject()
{
_universe = Universe.getInstance() // Получаю ссылку на свой класс Universe.
id = new Id(); // Создаю новый идентификатор Id.
}
Функция — init:
Функция - init
// Функция init, принемает 3 основные параметра любого объекта: позицию и поворот.
public function init(posX:Number, posY:Number, rot:Number):void
{
// Если контейнера нет то выводим ошибку, если есть добавляем “себя” в этот контейнер.
if (container == null ) trace(this + " dont have container!");
else container.addChild(this);
// Инициализируем стартовые значения
this.x = posX;
this.y = posY;
this.rotation = rot;
// Если есть какое-либо графическое представление, то добавляем его в “свой” список отображения.
if(sprite !== null)
{
this.addChild(sprite);
}
// Добавляем “себя” в мир, в моем случае вызванная функция просто добавит данный объект
// в массив всех объектов находящийся в Universe.
_universe.objects.add(this);
// Устанавливаем флаг “жизни”
isExist = true;
}
Функция — update:
Функция - update
// Функция update, вызывается каждый кадр из моего Universe.
public function update():void
{
// В данном случае она пуста, однако при необходимости вы можете добавить сюда обработку
// каких либо данных необходимых всем игровым объектам, например можно сделать глобальную переменную,
// которая хранит угол поворота в радианах, и тут каждый кадр пересчитывать этот угол,
// однако надо понимать что все написанное здесь будет вызываться каждый кадр для ВСЕХ!!! объектов,
// а это значит что не стоит здесь писать чего-то лишнего, например перерасчет в радианы :D
}
Функция — destroy:
Функция - destroy
// Функция destroy, вызывается для уничтожения объекта.
public function destroy():void
{
if (isExist) // проверка на лишний вызов
{
// Если есть родитель, то удаляемся из него
if (this.parent !== null)
{
this.parent.removeChild(this);
}
// Если есть графика, то удаляем ее из списка отображения.
if (sprite !== null)
{
this.removeChild(sprite);
}
// Зануляем переменные контейнера и графики.
container = null;
sprite = null;
// Удаляемся из мира, в моем случае простое удаление из массива.
_universe.objects.remove(this);
// Снимаем флаг “жизни”
isExist = false;
}
}
Как видите все просто и понятно, однако перед тем ка я покажу на примере как с использованием данного класса теперь легко реализуются любые игровые сущности, я все-таки упомяну про класс Id.
Пункт 2: Идентификация объектов
Мой класс Id, достаточно прост и представляет собой следующую систему:1. Каждый объект Id может иметь принадлежность к какой-то группе(обычно использую если надо разделить на “свои” “чужие”, GROUP_A, GROUP_B, GROUP_N(нейтральная)).
2. Каждый объект Id имеет набор меток, типов, которые служат для его характеризации и позволяют в последствии делать гибкие выборки, например: объект вида танк, так же он разрушаемый, а еще он особо опасен, то есть у него будет 3 метки(TYPE_TANK, TYPE_DESTROYABLE, TYPE_DENGER_HIGHT), для каждого геймплея это может быть свой набор меток, и самое главное что у всех объектов они могут различаться и нет необходимости характеризовать объект по всем параметрам которые присутствуют в других объектах.
3. Каждый объект Id имеет уникальный тег, обычно данный тег характеризует определенный вид объектов, например если у вас есть игровой объект Tank, то все экземпляры будут иметь тег — TAG_TANK.
Все эти группы, типы и теги являются простыми уникальными числовыми константами.
Таким образом при необходимости можно отбирать только те объекты которые нужны, например при разрыве гранаты: получаем все объекты в заданном радиусе и отбираем только те, которые имеют тип — TYPE_DESTROYABLE, ну и наносим урон.
Что бы каждый раз не проверять все вручную, я так же написал примитивный класс IdFilter, который как раз и служит для создания всевозможных выборок. Данный класс просто хранит 3 массива, массив для групп, для типов и для тегов. В классе же Id написано пару методов которые проверяют переданный им IdFilter и возвращают true или false в зависимости от того, подходят они под описание фильтра или нет. Таким образом что бы произвести выборку, достаточно создать фильтр с нужными параметрами и проверить id всех необходимых объектов на данный фильтр. К слову данные классы совершенно не доработаны и имеют лишь тот функционал который был необходим мне, но так как они очень просты их доработка не составит особого труда, а может быть вам хватит и этого, так как принципы по которым строится идентификация на мой взгляд очень удачны.
Id код:
Id
package src.framework.id
{
import src.utils.Kind;
public class Id extends Object
{
public var group:int;
public var types:Array;
public var tag:int;
public function Id()
{
group = Kind.GROUP_NONE;
types = [];
tag = Kind.TAG_NONE;
}
//==========================TYPE=======================//
public function addType(type:int):void
{
if (types.indexOf(type) == -1)
{
types.push(type);
}
}
public function removeType(type:int):void
{
if (types.indexOf(type) !== -1)
{
types.splice(types.indexOf(type), 1);
}
}
public function checkOnTypes_OR(t:Array):Boolean
{
if (t.indexOf(Kind.TYPE_NONE) !== -1) return true;
var isCheck:Boolean = false;
var tLength:int = t.length;
if (types.length !== 0)
{
for (var i:int = 0; i < tLength; i++)
{
if (types.indexOf(t[i]) !== -1)
{
isCheck = true;
break;
}
}
}
return isCheck;
}
//=========================GROUP=======================//
public function checkOnGroups(g:Array):Boolean
{
var isCheck:Boolean = false;
if (g.indexOf(group) !== -1 || g.indexOf(Kind.GROUP_NONE) !== -1)
{
isCheck = true;
}
return isCheck;
}
public function checkOnGroup(g:int):Boolean
{
if (g == Kind.GROUP_NONE || g == group)
{
return true;
}
else
{
return false;
}
}
public function getHostileGroup():int
{
if (group == Kind.GROUP_A) return Kind.GROUP_B;
else if (group == Kind.GROUP_B) return Kind.GROUP_A;
else return Kind.GROUP_NONE;
}
//==========================TAG========================//
public function checkOnTags(t:Array):Boolean
{
var isCheck:Boolean = false;
if (t.indexOf(tag) !== -1 || t.indexOf(Kind.TAG_NONE) !== -1)
{
isCheck = true;
}
return isCheck;
}
public function checkOnTag(t:int):Boolean
{
if (t == Kind.TAG_NONE || t == tag)
{
return true;
}
else
{
return false;
}
}
public function checkOnFilter(filter:IdFilter):Boolean
{
return (checkOnTags(filter.tags) && checkOnGroups(filter.groups) && checkOnTypes_OR(filter.types));
}
}
}
IdFilter код:
IdFilter
package src.framework.id
{
public class IdFilter extends Object
{
public var groups:Array;
public var types:Array;
public var tags:Array;
public function IdFilter(g:Array, ty:Array, ta:Array)
{
groups = g;
types = ty;
tags = ta;
}
}
}
Пункт 3: Пример создания игровой сущности
Теперь давайте попробуем создать простую игровую сущность на основе класса BasicObject. Предположим это будет объект Droid который должен лететь справа-налево:
Droid
package src.game.objects.enemies.droids
{
import src.framework.id.IdFilter;
import src.game.objects.BasicObject;
import src.utils.Kind;
public class Droid extends BasicObject
{
public var speed = 0; // Переменная для скорости
public function Droid()
{
}
override public function init(posX:Number, posY:Number, rot:Number):void
{
container = _universe.centergroundContainer; // Контейнер
// Устанавливаем настройки Id
id.group = Kind.GROUP_B;
id.tag = Kind.TAG_DROID;
id.addType(Kind.TYPE_DESTROYABLE);
sprite = new Droid_mc(); // Задаем графическое представление
super.init(posX, posY, rot); // Дергаем родителя
}
override public function update():void
{
this.x += speed; // Двигаемся по x на заданную скорость
super.update(); // Вызываем родителя
}
override public function destroy():void
{
// Так как мы не создали локальных переменных класса ссылочного типа, то по сути нам пока
// нечего “зачищать”, это функцию в данном случае можно вообще не создавать.
super.destroy(); // Вызываем родителя
}
}
}
Это простой пример сущности на основе BasicObject, для чего он нужен. Так как это учебный пример, то по задумке, при нашей “виртуальной компиляции” на экране должен появится объект, который имеет вид Droid_mc и начать двигаться вправо с заданной скоростью speed. Чтобы это произошло надо соответственно создать объект типа Droid и вызвать функцию init((), как-то так:
var droid:Droid = new Droid();
droid.init(0, 200, 0);
Но так мы не задали параметр speed нашего объекта, задаем его соответственно так:
var droid:Droid = new Droid();
droid.speed = 10;
droid.init(0, 200, 0);
Если один параметр то ладно, а что если их 5 или еще больше? Писать каждый раз 5-7 строк на создание одного объекта на мой взгляд не очень удобно и практично, хорошо бы все начальный параметры конкретного объекта инициализировать функцией, но функция init уже есть и мы не можем изменить или расширить ее под конкретный объект, она имеет строго три параметра, так как же быть? И тут приходит очень красивое и логичное решение, создать в классе Droid статическую функцию, например create и передавать в нее помимо 3 параметров функции init все остальные необходимые параметры для конкретного объекта, а функция будет возвращать нам объект типа Droid:
public static function create(posX:Number, posY:Number, speed:int):Droid
{
var droid:Droid = new Droid();
droid.speed = speed;
droid.init(posX, posY, 0);
return droid;
}
Почему нету угла поворота? Предположим что объект всегда появляется и летит строго слева-направо, так зачем же каждый раз прописывать лишний параметр 0? Как раз этим и хороша функция create, что помимо банальной перезаписи параметров, в ней еще можно рассчитывать эти самые параметры(в данном случае я просто опустил угол поворота, что было бы невозможно при прямом вызове метода init). Например нам нужно что бы объект Droid каждый раз появлялся за пределами экрана и летел в центр, эти вычисления можно переложить на плечи create, которая рассчитает все необходимые значения, а затем создаст объект Droid на основе этих данных, все красиво, аккуратно и функционально. Теперь что бы создать объект типа Droid необходимо в ЛЮБОМ месте нашего кода написать всего ОДНУ строчку:
var object:Droid = Droid.create(0, 200, 10); // Если нужно сохранить ссылку
// Или просто
Droid.create(0, 200, 10); // Если нужно просто создать объект
Так же плюсом еще является то, что согласно принципам инкапсуляции наличие большого количества публичных полей не является хорошим тоном, а используя метод create мы можем инициализировать приватные значения данного класса, то есть в нашем случае переменную speed можно сделать private.
Полный код класса Droid:
Droid
package src.game.objects.enemies.droids
{
import src.framework.id.IdFilter;
import src.game.objects.BasicObject;
import src.utils.Kind;
public class Droid extends BasicObject
{
public var speed = 0; // Переменная для скорости
public static function create(posX:Number, posY:Number, speed:int):Droid
{
var droid:Droid = new Droid();
droid.speed = speed;
droid.init(posX, posY, 0);
return droid;
}
public function Droid()
{
}
override public function init(posX:Number, posY:Number, rot:Number):void
{
container = _universe.centergroundContainer; // Контейнер
// Устанавливаем настройки Id
id.group = Kind.GROUP_B;
id.tag = Kind.TAG_DROID;
id.addType(Kind.TYPE_DESTROYABLE);
sprite = new Droid_mc(); // Задаем графическое представление
super.init(posX, posY, rot); // Дергаем родителя
}
override public function update():void
{
this.x += speed; // Двигаемся по x на заданную скорость
super.update(); // Вызываем родителя
}
override public function destroy():void
{
// Так как мы не создали локальных переменных класса ссылочного типа, то по сути
// нам пока нечего “зачищать”, это функцию в данном случае можно вообще не создавать.
super.destroy(); // Вызываем родителя
}
}
}
Пункт 4: Вывод
Как видите с применением класса BasicObject мы свели к самому минимуму рутинные работы, связанные с добавлением в список отображения, идентификацией, добавлением в массив и т.д.(кстати эта рутина может расти в зависимости от потребностей конкретной игры), однако теперь для создания какой-либо сущности нужно всего лишь создать класс, унаследовать его от BasicObject, переопределить необходимые методы и работать чисто над логикой объекта минуя всевозможную рутину, а сам объект и подавно создается всего одной строкой.P.S. Если вас заинтересует данный подход, и он окажется не совсем «крамольным», то я написал бы вторую статью, только уже с подробным применением данного подхода на практике, то есть какой нить мини проект от начала и до конца.
P.P.S. Текста получилось много, так что мог что-то пропустить или накосячить, если что поправьте.
- +5
- condor
Комментарии (24)
1. Вместо проверки (container !== null && container.contains(this)) лучше использовать if( parent ) parent.removeChild( this )
2. Если isExist будет использоваться для пулинга, то пулинг во флеше надо применять очень осторожно — он (флеш) для этого плохо приспособлен.
3. странным показалось использование как картинки (sprite — это графическое представление вашего объекта) MovieClip:
-расширен класс Sprite, зачем еще лишняя ссылка на child?
-почему поле sprite не DisplayObject? — это более универсально: картинкой может быть Bitmap,Sprite,MovieClip не ограничиваясь только тяжеловесным MovieClip
-если уж наследуется Sprite, то в него можно просто добавить любой child (Bitmap,Sprite,MovieClip) — опять же нет ограничения на тип + доступ по getChildAt(0)
-MovieClip достаточно тежеловесен и в плане памяти (а то тут ориентация на мобилы же) и в плане затрат на обработку (такты процессора).
Ну и с id я бы работал не так. Но это скорее дело вкуса, хотя мне это кажется еще и достаточно избыточным. Например описанной для танков я бы делал не через id, а через ООП:
Наследуемся от базового объекта, в наследнике делаем поле класса — список или массив, в который все танки и будут заноститься — можем получить к ним доступ в любое время. А добавлять в константы еще и префиксы — имо путано. Тем более флеш не поддерживает няшных enum-ов по которым можно однозначно определять константы, т.к. обычные целые константы могут быть легко перепутанны по значению.
У тебя списки игровых объектов вынесено в отдельный класс IdFilter с массивами массивов types:Array — это медленнее чем прямой массив в поле класса. Ну может это чем-то там оправдано, но не могу представить чем.
Ну и я вообще не представляю зачем все эти списки с фильтрами? Ни разу не использовал такого подхода.
А также где проверка столкновений? Метод update есть, а проверки столкновений (что-то вроде check_collisions) — нет? Это же первое правило котлеты-отдельно, мухи — … то-есть: апдейты — отдельно, проверки столкновений — отдельно.
Ну и насчет универсальности в разрезе флеша советую еще повьезжать в события (вот сразу можно оптимизировать твой двиг, не подписывая на update те объекты, кому это не надо, чтоб не дергать все подряд).
По 2 пункту не совсем понял что такое пулинг, но в моем случае это просто переменная которая отслеживает, «жив» ли объект, то есть функционален ли он в игровом мире.
По 3 пункту, MovieClip описан лишь как пример, например я использую тип AntActor, кто-то другой что то еще, суть не в конкретном типе, а в том что BasicObject имеет переменную под графику(тоже самое и про Id), так как большинство объектов графические и обходятся одним клипом, однако при необходимости объект можно собрать и из нескольких создав эти переменные уже в конкретном классе.
Система Id я делал с таким расчетом чтобы сделать код наиболее гибким и друг-от-друга независимым. То что описали вы, хоть я и не до конца понял, но понятно что используются и изменяются конкретные классы чего я пытался избежать, а у меня просто из проекта в проект качуют 2 класса. Константы у меня находятся в отдельном классе в статическом виде, так как нам не важны значения констант, а важно что бы они были уникальны, то метод в пару строк вернет гарантированно уникальное число.
Проверка столкновений у меня находится в классе Universe, но опять таки столкновения это лишь один из вариантов применения Id и IdFilter(у меня функция принимает объект, фильтр на проверку и функцию которую надо вызвать при столкновении), суть в том что все надстройки строятся как раз над классом Basicobject и его переменными в том числе Id.
Во-первых, contains() как минимум работает дольше — это все же рекурсивный обход вверх (я надеюсь, что вверх, от индусов всего можно ожидать).
Во-вторых, это все же слишком самоуверенно (чисто в программистском, необидном смысле) использовать код, который нельзя считать корректным в общем случае — мало ли когда этот общий случай настанет ;)
P.S.
А еще я последнее время полюбил такие закорлючки :)
Но это уже так, предпочтения, в отличие от сказанного выше.
За то новичок сразу учится делать правильно. Не велосипедя и набивая шишки на ровном месте.
Ведь по сути, там просто более универсальная и правильная декомпозиция ваших наработок.
1) Методология Component-Entity Systems — очень удачная модель для построения многих игр. Отчасти именно ей Unity обязана своему успеху (а в Unity все работает именно так, с небольшими поправками на терминологию).
2) Статья — весьма добротная. Я сам с ее помощью осваивал основы Component-Entity Systems. Но из-за подхода «давайте вместе пройдем весь путь эволюции и придумаем Component-Entity Systems», ее придется прочитать целиком.
3) Все свои игры я строю сейчас именно так, правда с некоторыми важными поправками на специфику и ограничения HTML5.
Опасения, что от такого подхода будет оверхед в медленных GC типа AS3 — верные. Нужно все это использовать с головой и очень аккуратно.
1. Я не потив системы энтити/компонент или какие там ее названия для этого принципа придумали еще (каждый называет как хочет), — я ее сам использую в лично написанном фв.
2. Статья — глупая, как и очевидно, код (его части в статье и приводятся). Код автора содержит много лишнего (если не говорить грубее).
3. Я на своем фв строю игры тоже уже несколько последних лет. Я написал не один игровой движок с 0 и сделал не одну итерацию фв, — чтобы было понятно, что я все-же понимаю, о чем говорю.
4. Заслуга Юнити сомнительна — там С# просто способствует эвент-драйвен/компонентному: и делегаты отдельно для простых событий и event-ы для сложных. А первенство и продвижение в массы — тоже: Делфи был задолго до Юнити, там вполне себе компонентый подход, даже так и называется самими разработчиками.
Развернуто:
Та принцип норм, я не про него. Я его использовать начал (пришел к нему) в эволиции своего движкописания году в 2006-7, еще когда про него особо не писали. По крайней мере лично не читал и не встречал литературы по сабжу в то время, да и вообще по движкописанию было достаточно пусто. Однако из личного общения уже позже узнал, что одни из разработчиков использовали подобный подход (по описанию, сорцов я не видел) в одной из игровых студий Харькова еще задолго до меня. Это просто логичное развитие в сторону универсальности унификации и рано или поздно все к этому подходу приходят. Я называл это тогда «плагинной архитектурой», потому что компонентнтой не хватало наглости называть, т.к. эталоном компонентной был Делфи, — мне до него было далеко, ибо чистый код без визуальных редакторов и настроек.
Про двиг Dungeon Siege я читал еще дааавно — там свою систему называли вообщета Data-Driven — это помню, в статье это и указано, кстати. И насколько я тогда в нее въехал (вобщем не въехал я в нее тогда) — это было немного про другое. (зы: а вот они это сейчас уже компонентной системой обзывают, увидел… терминоголия устаканивается)
Ну и с терминами реальная путаница. Одни энтити-системой называют объекто-ориентиованный подход, другие уже разбивку на действительно отдельные сущности, связываемые событиями. Последнее считаю универсальнее и лучше (сложности с яп которые не поддерживают делегацию/ссылки на методы/). Еще называют компонентной системой. А вот разрабы DS оказывается вообще Data-Driven Game Object System назвали.
То, что все подход реализовывают по разному — это совсем другое. И то, что автор статьи, перевод которой привели на хабре сделал замороченую и далекую от реальности реализацию — отдельный момент. Я ж от доброты и человеколюбия советую, чтоб не забивать голову чепухой (хотя вот опять разжевать стоило х*у тучу времени и боюсь опять быть не понятым)…
Да просто замечательный. Я даже в этом нисколько не сомневаюсь. Только вы используете чужие фреймвоки, а я свои пишу — разница лично мне очевидна. И я использовал свои не в паре игр.
Вот вы можете мне объяснить зачем например в IProcess метод start(), который в приведенных реализациях всегда возващает true (гениально замечательный ход)? Зачем там end()? Мне не надо рассказывать, что думает про них автор — я прочитал, для чего он их задумал. Но где это реально используется в реальном коде игр? Специально придумать ненужные методы, а потом специально писать такой кривой код чтобы их использовать? Мы, что не знаем надо нам подписываться или нет? Или не знаем когда отписываемся и что после этого делать?
Вопрос зачем обычное событие (по сути) звучно называть процессом оставлю риторическим, — каждый волен называть сущности своего фв как ему хочется. Но вот зачем выносить его в интерфейс, ограничивая одним обработчиком update() на реализацию/класс?
Для меня это очевидные глупости и даже хуже — ограничения.
Открою великую тайну — даже приоритеты там не нужны (даже для систем ГУИ — проверено) достаточно других, более простых действий и организации архитектуры.
Я про игровое программирование если чо, а то в системном приоритеты могут решать (хотяяя...).
Для понимания подхода рекомендую cowboyprogramming.com/2007/01/05/evolve-your-heirachy/ — чисто теория без реализации, для понимания самих принципов. Хотя можно вообще просто помедитировать на картинку оттуда cowboyprogramming.com/images/eyh/Fig-2.gif — имо для понимания принципа достаточно.
И вообще для компонентного подхода важна одна вещь: нормальная система событий, та что в флеше встроена — не подходит. Если яп не поддерживает динамического программирования, то еще может помочь система «словарей» (список ключ-значение), но и это так-себе и спорно, просто более гибко может получится, а можно и полями все решать или теми-же событиями.
Вот ты спрашиваешь про IProcess. А ты отследил, что IProcess удаляется из фреймворка в процессе рассуждения и заменяется на ISystem? И что делаем MoveSystem, в которой в методе update мы идем по всем объектам с данной компонентой и двигаем их. И это что, можно назвать «событием»? :)
Entity-component подход достаточно стабилизировался в последнее время:
— «Entity» — коллекции компонент, без методом и полей. В юнити есть классы Entity («префабы»).
— «Components» — объекты-компоненты, коллекции полей, чистые данные (иногда разрешаются чисто собственные методы, работающие только с полями данной компоненты), без игровой логики, не имеющие вообще никаких зависимостей друг от друга.
— «Процессы» (или «Системы», или «Скрипты») — функции, которые исполняются в игровом цикле (реже по событиям игрока, в некоторых реализациях игровая логика в событиях не допускается для упрощения управления зависимостями) над объектами с компонентами только своего типа («Узлы»).
Вот, ИМХО, именно недостатки и преимущества такого подхода и нужно обсуждать. Навязывать свою терминологию и подменять предмет обсуждения — не очень корректно. Пусть даже они и «украли» чужую терминологию.
А вообще я так испугался бреда в IProcess и просто не стал читать дальше, как я уже и писал. Ну не надо начинать с бреда — я то тут при чем. Начал с бреда — получил, — я честно сказал, что после начального бреда дальше не читал. Мнение не меняю, если даже начать с бреда и прийти к чему-то нормальному, лучше сразу начинать нормально и развивать мысль, а не забивать мозги и только позже менять на что-то лучше. Это ж как в игре — облажался сразу, не показал качества, — все игрок ушел. Обратный возврат с низкой вероятностью.
Да. Паттерн обсервер, представляющий события. Это представляется в виде событие Move + его обработчики / слушатели / подписчики. Называть можно как угодно, суть одна — в событийном представлении уже описал. Опять же зачем городить отдельные классы, когда можно сделать через одно событие «move»? Опять же строгая привязка к методу update — ограничение / недостаток (лично у меня все гибче и проверено и оправдано практикой).
Вот опять ты привел кучу ненужных сущностей реализации подхода (Entity, Components, Процессы), которые ну типа как паскаль — хорошо для обучения, там чтобы понять, от чего отталкиваться, но не нужны по сути в реализации. Основу я указал — события. Все остальное — фигня, детали реализации. Хотят люди плодить ненужные сущности — на здоровье, я — не хочу и по доброте и другим не советую.
Я не навязываю свою терминологию — я стараюсь оперировать непосредственно явлениями. Для этого просто использую альтернативные термины, чтобы поближе подобраться к сути. И уж я ничего не подменял. Если не понятно о чем я — мы говорим на разных языках, я пытался разжевать как могу.
зы: ну можно считать, что я использую тогда не entity system framework, — тогда значит, я критикую entity system framework(s).
Это — нужные сущности. Они принципиально отличают архитектуру entity-component (Ash и, например, Unity от других подходов). Уберешь эти понятия (например, разрешишь инкапсуляцию, интерфейсы и зависимости между объектами) — будут другие, не менее полезные архитектуры и подходы получаться.
> Это представляется в виде событие Move
Событие тут enterFrame. MoveSystem — это скрипт, процесс или система, которые реагируют на событие enterFrame. Называть его событием Move — неверно.
> которые ну типа как паскаль — хорошо для обучения,
Ну вот строят огромную кучу AAA игр так, доклады авторитетные чуваки на GDC делают, обсуждают как такой подход с производительностью помогает и расширяемостью. Не только, вестимо, для обучения подходит?
> зы: ну можно считать, что я использую тогда не entity system framework, — тогда значит, я критикую entity system framework(s).
Чтобы критиковать entity system framework, нужно внимательно ознакомиться с его предположениями, сильными и слабыми сторонами. Понять мотивацию подхода (почему в entity нет полей и методов, почему компоненты не могут иметь зависимостей друг от друга и обычно не имеют методов, почему вообще происходит осознанный отказ от принципа инкапсуляции).
Вот тогда можно и осознанно покритиковать. Обсудить зависимости по данным против зависимостей по управлению. Покритиковать проблему появления нелогичных флаговых полей в компонентах.
Критиковать без глубокого понимания темы — не следует, не продуктивно это.
Ты разрабатывал свои фреймвоки? сколько? что они позволяют?
Я разрабатывал. Пришел к последнему, итераций и рефакторингов там было уже дофига и еще есть, что допиливать. Его мощность, гибкость, скорость: физдвижок, ГУИ от низкоуровнегового системного АПИ (а не на флеше), система дисплейного рендеринга идентичная флешевой но на растре без вектора (даже базовые классы называются похоже — исключительно ради удобности портирования), система твинов, игры, ИИ (боты), системы частиц. На флеше в производительность не упирался, но и всякие тяжести в виде частиц туда не пихал в неумеренных количествах.
Ты можешь представить что-то аналогичное для сравнения понимания, а не игры на чужих фреймвоках?
Насчет «Событие тут enterFrame», а «MoveSystem — это скрипт» — лично мое мнение — ты запутался в терминах. Не вижу смысла тебя разубеждать, продолжай придумывать прослойки и названия для них. Суть: enterFrame привело к обработчику MoveSystem, то что ты называешь «скрипт, процесс или система, которые реагируют на событие enterFrame» — если тебе не понятно, что это паттерн обсервер, хоть как ты его не называй, хоть сколько прослоек между источником и обработчиком ни втуляй, то о чем тут можно говорить вообще.
Если заострять на терминологии, можно сказать, что я значит, критикую entity system фреймворки и не просто теоретизирую, а сделал фв проще и гибче.
А вообще фреймворки можно пилить и наворачивать до бесконечности, но оценить его можно только делая конечные продукты!
Конечно опенсорсность — это плюс — можно поправить под себя.
Я ж написал — я его писал на основе своего фв. Порт бокса2д по скорости не то, что нервно курит, а просто не попадает. Чтобы объективно — по возможностям, мой конечно не дотягивает, хотя это просто вопрос времени — надо допиливать, а я его забросил после 1й игры, ибо обстоятельства — не вижу смысла: физиграми зафлудили всё, интереса к ним нет ни на флеше ни на мобилах уже, только что-то совсем невероятное делать. А это как и возможный куш так и приличный риск. То, что есть: прямоугольники (вращающиеся вроде не делал — были не нужны), окружности, объекты подвижные и неподвижные (в терминах бокса — статичные динамичные) с касаниями, соединения «точка», соотв веревки на этих соединениях с расчетом касаний (собственно где бокс и начинает захлебываться). Как-бы уже посложнее арканоида, но полигоны и прочие соединения не сделал.
Вот я бегло глянул на гитхабе Аш, впечатления:
— заточено чисто на игры, т.е. на конечную реализацию игр. Т.е. то, что я пишу на своем помимо: ГУИ, свой рендерер, физику и т.п. там сделать будет сложновато.
— значительная часть ядра (а то там есть пулинг, сериализация и прочее, что в ядро не попадает) у меня всего в двух классах, а не в куче, как там. И все ок. Они конечно не на пару строк, а достаточно емкие, но справляются.
— стойкое ощущение теории не покидает. Оно конечно стройно и красиво выглядит, но не жизненно. Чисто со своей колокольни, — понятно же надеюсь. По тому с чем мне приходилось работать.
(вот вы же написали, что допиливали его — вот и факт, хотя тут и ньюанс: или дополнять фичами или править код /пилить /рефакторить — что именно вы делали)
Та да. Если не делать клон одной игры, то с новых игр обязательно разные «нужности» дописываются, хотя это как-бы не ядро.
Но вот и допиливать по результатм новых проектов — тоже приходится. Вобще у меня изначально более-менее по структуре на Аш походило (ну с натяжкой так, меньше хлама все-же было), просто ненужное выпилилось, о чем я и толкую, что там много лишнего и автор больше теоретик, чем практик-разработчик игр. Вот есть этому опровержение? В исходниках там только либы (хоть флинт, конечно и навороченая).
Просто я встречался с таким распространенным явлением: есть программисты в компаниях, которые чисто пишут либы низкоуровневые — это адъ. Они конечно шарят в работе с железом и т.д., но надо же хотя-бы задумываться, как их либами пользоваться. Они про это не думают. Они думают «фичами», а как этим зоопарком пользоваться — типа не их проблема. Примеры:
— виндовс АПИ (вот для сравнения могу противопоставить Палм АПИ — было такое устройство на заре смартфонов (ничего лишнего — телефона там нет, а так все на своем месте было));
— бокс2д (ужасный апи, вот нейп значительно лучше по этому показателю);
— пушбаттон энжин (был такой на флеше);
— ну и другие штуки, которые вряд-ли известны сообществу.
Вобщем-то рис можно и палочками есть, можно и вилкой, а можно и ложкой — как кому привычнее.
Фреймворков я за свою жизнь построил немало (и в геймдеве, и за его пределами). Большой доблести в этом не вижу, мерятся их крутизной смысла особого не вижу. Если бы была возможность откатить время назад, я бы не писал ни одного, а пользовался только готовыми (особенно сейчас, когда почти на халяву можно очень серьезными наработками пользоваться). Но приходится иногда — для новых HTML5 игр написал на коленке подмножество Ash.
Но если тебе не хочется разбираться в деталях, почему ISystem реализует паттерн observer, но имеет отдельное имя; какими такими свойствами она обладает, что названа вот так; то я же не могу тебя заставить. Твое право!
Ну а если ты сделал фреймворк круче и лучше — ну так просто отлично! Удачи с его дальнейшей разработкой. Может все на него посмотрят и поймут, что подход Unity — плох. Делом всегда лучше доказывать, чем спорами в удаленных уголках блога. :)
Ну и уж точно не стоит продолжать, когда получается так:
а затем
не буду комментитровать, просто отмечу.
Давай не будем путать теплое с мягким: у меня чистый код — у юнити среда разработки (с визуальными редакторами, плагинами и прочим). А если насчет именно их фреймвока, то я что-то не видел чтобы их фреймвок был выложен отдельно от среды и люди наперебой мчались им пользоваться. Так что «за что купил...»
Рендереры, физдвижки у них на своем фреймвоке? — это внешние выкупленные технологии, к фреймвогу никакого онтошения не имеющие.
Ну и насчет отдаленных уголков, неужели ты думаешь, что я просто треплюсь — я в любой момент, по требованию могу предоставить воплощения, просто не пытаюсь, как ты сказал проявлять «большую доблесть». Ну и конечно моя скромность, поэтому и не на презентациях, а в бложиках. И не топиками, а скромно — комментами.
Да и кому сейчас это нужно, когда Просто мой личный интерес к теме и мой личный опыт, которым я по доброте делюсь.
Ну опять-же если речь о том, что Эш похож на юнити и чтобы понять юнити надо почитать про Эш — это отдельная тема.
Я про то, что все можно делать проще и гибче. Если это уже называется не entity system framework — ок, но это отдельная тема. Я, с учетом этого, даже признаю, что статья крутая и хорошая и Эш замечательный фв, но на принципах entity system, а у меня просто не entity system framework.
И это отличный результат дискуссии! Было бы странно, если бы все в мире было устроено одинаково.
Более того, в этом контексте можно даже продолжить и подвести некоторые итоги:
1) Я вполне соглашусь, что строить другим способом «гибче»! Поскольку entity system frameworks намеренно навязывают целый набор ограничений, а, скажем, чистый С++ вообще никаких ограничений не имеет. Конечно же он гибче!
2) Проще ли? Зависит от задачи. Есть определенный класс задач, в которых так становится гораздо проще. Я для себя их определяю: «Игровой логики в 10 раз больше, чем базовой механики». Если наоборот (как в примерах в статье в игре про астероиды), то большой выигрышности у подхода не будет. Если интересно могу пояснить мысль более подробно и поделиться опытом, почему я вообще начал использовать этот подход.
3) Про события я имел в виду исключительно то, что не очень логично Move system называть move event (как ты предложил). И сразу сказал, что она является обработчиком события enter frame (но называть его enter frame event handler тоже не очень логично, так как не передается его функциональность). Кроме того, я не сказал, что systems имеют целый ряд других особенностей (не имеют состояния, не могу вызывать друг друга), поэтому требуют какого-то термина (т.к. имеют свойства, которые просто у observer вовсе не обязательны). Тут наше расхождение исключительно по формулировкам.
Т.е. базовое ядро совсем несложное: мач3 в одной игре, click group to clear в другой. Но на него накладывается куча слабо связанных друг с другом фич, сильно превышающих по суммарной сложности механику:
— Босс. Рядом с ним можно делать матчи, его убивать.
— Штуки на поле, которые можно матчем собирать. Штуки, которые блокируют ходы.
— Игровые режимы на время, на количество ходов.
— Ну и еще куча всего такого похожего.
Подход аля Ash «системы по очереди исполняются, апдейтят состояние доски, каждая система может менять что хочет» мне показался очень удобным тем, что системы реально друг от друга развязаны и можно строить все поступательно. Специально конкретизирую, что в общем-то это совсем не полная реализация entity-component-process, там нет общего правила «сущности строятся чисто как совокупности компонент».
Например (порядок исполнения важен):
— PlayerMoveSystem: если пользователь сделал правильный свайп, заносим его в сущность «ход пользователя»
— BlockSystem: если ход пользователя затрагивает заблокированную фишку, перевести состояние хода в rejected
— Match3MechanicsSystem: нашла все матчи, добавила их в список заматченных
— BossSystem: идем по списку заматченных объектов, если есть рядом, наносим боссу урон
— BossDisplaySystem: визуализируем удар по боссу
Это совсем не единственный способ. Например, до этого в прошлых играх я строил некоторый класс GameWorld, который реализовывал основную механику. Он имел кучу стратегий, которые определяли его поведение (или просто нотифицировал о событиях, дергая функции интерфейсов), сами шейпы были активными сущностями (и у них были полиморфные поверапы, например). Недостатки тут такие:
1. Нужно постоянно рефакторить GameWorld и прочие классы на предмет добавления точек расширения. А если порядок исполнения важен, то это все тоже требует аккуратного обдумывания.
2. Постоянно возникают творческие задачи, в которых нужно думать головой об области видимости (какие параметры несут в себе события, что передается в стратегии). Классическое ООП направлено на скрывание ненужных деталей, но на практике для именно геймплейной логике принцип «вижу все» гораздо проще.
3. Когда чего-то упускаешь или торопишься, код быстро закакивается.
При реализации прямолинейным способом а-ля Аш все получается очень просто:
— Нужно заблокировать ход? Вставляй новую систему между вводом хода и исполнением механики для блокировки.
— Нужно заделать босса? Вставляй новую систему после матчинга, до визуализации.
Заодно легко вынеслись из главного когда штуки типа подсчета очков (которые тоже по суммарному объему со всякими комбо оказались нетривиальными).
Т.е. в общем-то получился чисто процедурный game loop — функции работают на состоянием — но:
— С возможностью легко добавить-убрать систему в зависимости от уровня (например, тьюториалы не везде есть)
— С возможностью прописать зависимости систем от сущностей-компонент (кому что нужно), и иметь некоторое
внутреннее приватное состояние системы, если это полезно.
Но есть цена такой простоты:
— КУЧА «ифов» и «форов» по одним и тем же спискам, сущностям, стейтам. Я считаю (но трудно это проверить), что JIT компилятор Javascript достаточно эффективен, чтобы этим можно было пренебречь. Систем же максимум 30, некоторые из них первым делом тупо проверяют, что состояние верное.
— Размен строго типизированных структурированных зависимостей на рантаймовые зависимости («система для регистрации хода не может стоять после системы механики, все скомпилируется и запустится, но не будет работать»).
— Появляется некоторая кучка чисто флаговых компонентов, обычное такое «семафоренье» в процедурном стиле. Которые хочется сделать полиморфно, а нельзя (у компонентов нет методов).
— Чудес не бывает. Некоторые штуки тупо приходится поднимать на уровень механики (например, нетривиальные поверапы и вредные шейпы).
В итоге по моей оценке код получился чище, чем классическая ООПшная реализация с игровой логикой размазанной по сущностям, а не по срезам функциональности. Добавлять фичи «вширь» так очень легко.