Глобальное хранилище

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

?гра — это достаточно сложная структура классов и запутанное их взаимодействие. Хороший программист, как правило, считает важным все устроить и упорядочить так, чтобы всегда можно было быстро разобраться в своем старом проекте, или без стыда отдать исходники на изучение кому-либо. Почти любой программист, имеющий хоть какой-то опыт разработки, понимает, что конструкции типа game.world.objects.getObject("myObject").position.x — не очень хорошо, и это определенно следует как-то оптимизировать.

В одном из прошлых уроков я рассказывал про маленькую хитрость, которая, как оказалось, является паттерном Singleton — штука действительно удобная, но имеет свои недостатки:

  1. ?спользуя данный паттерн, мы можем создать всего лишь один уникальный экземпляр объекта.
  2. Если в нашем проекте разные объекты используют синглтон класс то нам в каждом из этих объектов необходимо получать указатель на синглтон. Это, конечно, не проблема, когда мы в игре используем всего лишь один синглтон SoundManager для воспроизведения звуков, а если проект сложный и объектов синглтонов несколько, то уже далеко не всегда удобно писать громоздкие конструкции чтобы, например, воспроизвести один звук:
    import com.mysuperproject.SoundManager;
    //...
    var sound:SoundManager = SoundManager.getInstance();
    sound.play("soundName");
    
  3. ? третий недостаток — это не столько недостаток, сколько представление каждого из нас о целостности и логичности структуры приложения. Есть разработчики, которые считают, что синглтон может нарушать целостность структуры, естественно, с точки зрения читаемости кода и восприятия проекта в целом с последующим переносом его отдельных частей в другие проекты.

Недостатки, конечно не страшные, но осадок от них имеет место быть.

Каждый раз, заканчивая очередную игру, мне порой кажется, что если я сейчас закрою редактор, то завтра, открыв его вновь — я уже и не разберусь сам, что тут к чему, так как все кажется таким запутанным за счет перекрестных ссылок между разными классами. ?, начиная новый проект, я все внимательнее стараюсь относится к таким вещам и придумываю всякие классы управлялки, которые бы следили и упрощали доступ к конкретным объектам, но управлялки тоже ведь нужно контролировать. ? даже однажды я поймал себя на мысли придумывания класса менеджера менеджеров :) Хотя, при разработке игры, основной движок игры — это, как правило, и должен быть менеджер менеджеров. Но все же не все менеджеры могут иметь непосредственно отношение к самой игре и должны существовать где-то отдельно, дабы не нарушать целостность структуры. В общем, это я так немного отвлекся :)

Чудо-сундучок

Однажды, копаясь в ?нтернет, я случайно наткнулся на интересное решение глобального хранилища данных, которое может помочь организовать доступ к данным внутри игры и позволит избавиться от синглтонов и в разы упростить доступ к важным методам. Правда, при этом само глобальное хранилище является синглтоном :) В общем, лучше один раз увидеть, чем много читать, привожу примеры использования.

?нициализация хранилища и добавление данных:

// Получение указателя на глобальное хранилище
var g:Global = Global.getInstance();

// ?нициализация всех управлялок для нашей игры,
// например во время создании ядра игры.
g.sounds = new SoundManager();
g.effects = new EffectManager();
g.objects = new ObjectController();
g.key = new KeyController();

// Так же в хранилище можно хранить указатели на методы
g.sound = onSoundPlay;
g.effect = onMakeEffect;

// Метод, проигрывающий звук
function onSoundPlay(name:String):void
{
  // Проигрываем звук по имени
}

// Метод, создающий эффект
function onMakeEffect(name:String):void
{
  // Создаем эффект, например эффект взрыва
}

?спользование данных из хранилища:

// Создание эффекта
var effect:MyEffect = g.effects.get();
effect.init(...);

// ?ли используем общий метод создания эффекта
g.effect(...);

// Работа со звуком
if (g.sounds.isPlayed("soundName"))
{
  // Если звук проигрывается, что-нибудь тут делаем
}

// Отключение всех звуков
g.sounds.mute(true);

// ?ли используем общий метод воспроизведения звука
g.sound("mySound");

В общем, как видно из примеров, все эти многоэтажные ссылки типа game.soundManager.playSound() можно сократить до простых и понятных: g.sound().

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

Пример базового класса игрового юнита:

class BaseUnit extends Object
{
  protected var g:Global = Global.getInstance();
  protected var _kind:int = 0;
  protected var _variety:int = 0;
  // и т.п.
}

А это класс потомок реализующий конкретную разновидность юнита:

class SoldierUnit extends BaseUnit
{
  function SoldierUnit():void
  {
    _kind = Kind.SOLDIER;
    // Доступ к хранилищу
    g.sound("hi");
  }
}

В целом все выглядит очень вкусно, и я надеюсь, что вам уже захотелось попробовать этот «чудо-сундучок» в деле. А мне в свою очередь очень интересно узнать ваше мнение — действительно ли это удобно и оптимально или может быть кто-то из читателей знает более оптимальные/удобные способы? :)

?сходники с примером — CS4, *.zip, 19кб.

P.S.: Несмотря на все видимые удобства глобального хранилища, его необходимо использовать с умом и не следует помещать туда все подряд. Будьте внимательны к структуре вашей игры/программы! :)

 

а действительно) Наследование рулит!
Я всё время передавал указатель на хранилище прям в класс. там ему давал имя через это имя к нему и обращался.

Виктор Солодилов
27 Февраля 2011
— 12:27
#

Спасибо за рецепт) Я новичек, поэтому мне было очень полезно узнать даже про Singleton из прошлых уроков. Его пока и хватает. Как я понял ваш метод сундучка подходит уже для серьезных проектов.

Алексей
27 Февраля 2011
— 13:39
#

Не совсем понятно, зачем использовать этот сундучёк. Можно же делать так:

SoundsController.instance.playSound();

или, если вы хотите через гет/сет

SoundsController.getInstance().playSound();

Записывается в одну строку и мне, например, глаза не мозолит в таком виде. А вот заморачиваться и в каждом классе наследование выставлять - по-моему - не очень гуд.

1g0rrr
27 Февраля 2011
— 14:31
#

На данный момент, моя структура состоит из множества менеджеров, таких как screenManager и soundManager и т.д. ?х инициализация происходит в основном приложении App и чтобы было удобно к ним обращаться я сохраняю их в статической переменной.

Получается такая картина:

public static var sounds:SoundManager = SoundManager.getInstance();

и далее по аналогии........

чтобы запустить звук где-нибудь в игре я делаю следующее.

App.sounds.play("имя звука");

Таким образом нет необходимости передавать ссылка на основной класс App через конструктор.

Данный метод я увидел здесь: http://active.tutsplus.com/tutorials/workflow/handling-screen-architecture-the-painless-way/ и попробовав его для управления экранами, решил сделать по-аналогии и с другими менеджерами.

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

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

Тут бы пример привести, т.к. в исходниках только нет примера, какие именно классы можно наследовать. Я так понял это, классы App, Game, Map и т.д.?

Вадим М.
27 Февраля 2011
— 14:34
#

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

александр
27 Февраля 2011
— 14:54
#

Антон, все же повторюсь, что такой метод имеет очень серьезные системные недостатки.

game.world.objects.getObject("myObject").position.x - это примерно как температура 39.3 при ангине. Тут явно всплыло нарушение "Закона Деметера" (http://en.wikipedia.org/wiki/Law_of_Demeter), индикатор беды в организме. А ты вместо правильного антибиотика (т.е. рефакторинга классов так, чтобы классу не нужно было лезть внутрь другого класса, убирания глупых лукапов объекта по имени), советуешь пить парацетамол (т.е. маскировать проблему).

Фактически, все объектное программирование направлено на то, чтобы сделать зависимости между объектами явными. Объект должен знать о небольшом кол-ве своих соседей (7+-2 объекта) и с ними работать. Тогда легко повторно использовать код, легко понимать код, легко менять код. Например, физический и графический движок моей Gluey никогда не должен иметь права обращаться к Sound Manager. Звуки проигрывать имеет право внутри игрового процесса только класс GlueyGame и потомки GlueyPowerup. Если изменится завтра система звуко-извлечения, я знаю, что придется менять только два класса.

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

Особенно вредный совет - использовать наследование. ;( Открытое наследование можно использовать ТОЛЬКО для единственной цели - реализация отношения "IS-A" ("является"). Как только ты применил его для чего-то другого, ты получишь проблему "хочу унаследоваться от двух классов" (от global data и от Enemy). А множественное открытое наследование - запрещено. В результате твои самые базовые классу будут унаследованы от глобальных данных.

Так как ты написал можно писать в Руби (где есть миксины) и в С++ (где есть ЗАКРЫТОЕ наследование).

А решение - всех этих проблем понятное, но сложное. Про него не напишешь одну статью, а нужно читать книги типа "Code Complete".

Нужно при проектировании программы заранее спланировать, каким классам и для чего нужно видеть какие-то другие классы. Если выявляются "классы-всезнайки", которые должны знать про всех, то такие классы нужно делить на несколько классов.

Часто развязать зависимости помогает паттерны.

"Обработчик событий" часто решает проблему "нижележащий класс стал знать слишком много про верхние классы". Так, например, мой игровой класс не имеет права делать выключение звука, но он может бросить событие "уровень пройден" и контроллер игры выключит звук, чтобы сиграла "interlevel ad".

Паттерн "Стратегия" решает проблему "мой игровой объект знает все и про рисование и про поведение".

Жду следующего Флэш-гамма, чтобы с тобой лично побеседовать про архитектуры! Очень интересно пишешь. ;)

Sergey Batishchev
27 Февраля 2011
— 14:57
#

? в дополнение пара практических мыслей.

Такой сундучок хочется обычно сделать для глобальных сервисов (SoundManager, ResourceManager, ScreenFloatyMessages).

?деальное решение - inversion of control и применение настоящего dependency injection framework (в Java - Google Juice, в AS3 - тоже есть разные, например, Parsley).

Но в простом приложении (<20000 строк) их нормально сделать синглетонами. Только в этом случае желательно на листочке четко очертить границы, куда этим синглетонам можно, а куда нельзя заходить.

Например, игровым классам имеет смысл разрешить использовать SoundPlayer (для проигрывания звуков), но ни при каких обстоятельствах не разрешить вызывать методы SoundManager (mute/unmute).

Sergey Batishchev
27 Февраля 2011
— 15:09
#

@1g0rrr, в тексте записи я привел пример со строчкой на подобие вашего примера:

> SoundsController.getInstance().playSound();

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

Ant.Karlov
27 Февраля 2011
— 16:36
#

@Вадим М., глобальное хранилище получается чуть более универсальное чем использование, например класса App.as для хранения общедоступных переменных. Дело в том что хранилище не привязывается к конкретному проекту и существует какбы само по себе, таким образом к нему могут так же иметь доступ любые другие классы за приделами игрового проекта, например классы утилиты или какой-нибудь фреймворк. А так же можно отлавливать события на изменения каких-либо данных в хранилище.

Пример про базовый родительский класс и его последователей я привел в исходниках — это классы SuperParent.as и FirstChildren.as. Только каюсь, информацию по поводу этого я подал не правильно. Подробности в другом комментарии.

Ant.Karlov
27 Февраля 2011
— 16:44
#

@Sergey Batishchev,

> game.world.objects.getObject("myObject").position.x - это примерно как температура 39.3 при ангине.

Вот тут я сразу понял что абзац «А стоит ли это использовать?» я зря выкинул из записи перед публикацией чтобы не смущать читателей :)

> Например, физический и графический
> движок моей Gluey никогда не должен иметь
> права обращаться к Sound Manager. Звуки
> проигрывать имеет право внутри игрового
> процесса только класс GlueyGame и потомки
> GlueyPowerup.


То есть взаимодействие классов в твоем случае получается через события? Какие-то объекты выполняют действия, посылают свои сообщения, а движок слушает всех и шевелит менеджеров?

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

Мне кажется вполне удобным использование публичных методов в глобальном хранилище: например когда все игровые объекты вызывают универсальный метод из хранилища g.sound() там где нужно воспроизводить звук, а в самом методе уже выполняется доступ к менеджеру звуков. ? перенос классов юнитов в другие проекты в целом не должен вызывать трудностей если другие проекты используют подобный метод :D

Конечно где-нибудь на конференции об этом было бы намного удобнее и продуктивнее пообщаться. Но я еще тот самоучка... :P

Ant.Karlov
27 Февраля 2011
— 17:03
#

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

Это кстати хороший компромиссный подход, почти dependency injection, но "вручную". Я именно так в текущем проекте и делаю - не решился брать риски интеграции Parsley AS3 или robotleg.

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

Sergey Batishchev
27 Февраля 2011
— 17:05
#

@Sergey Batishchev, а как на счет взаимодействия игровых объектов с игровым движком, объектам следует самостоятельно добавляться в движок или движок при создании объектов должен сам их удалять и добавлять?

Я, например, стараюсь делать все игровые объекты самостоятельными — они сами добавляются и удаляются, а игровой движок выступает только в роли процессора который процессит все объекты. Но когда например нужно очистить уровень перед созданием нового, то движок всем объектам как бы говорит «валите отсюда» и они разбегаются по своим кэш хранилищам самостоятельно до тех пор пока снова не будут востребованы. Таким образом игровой движок является общедоступным классом который состоит из менеджеров и контроллеров обслуживающих объекты разного типа. Ну во всяком случае я сейчас пробую такую схему в текущем проекте, а в предыдущих играх все намного страшнее :D

Ant.Karlov
27 Февраля 2011
— 17:18
#

Антон, спасибо за очередную интересную статью. Натолкнул на мысли.

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

Сама игра - это несколько класов (основной клас, клас главного героя, клас статистики, класы страниц меню).

Задача: получить доступ из меню к управлению звуками (включить/отключить музыку и звуковые эффекты полностью в игре (то-есть во всех ее класах)).

Проблемма: сами звуки будут привязаны к разным класам (главному класу, класу героя, класу статистики). Как кликом из одного класа повлиять на звуки во всех класах?

?нтересно узнать, как опытные флеш-разработчики реализовывают подобную систему?

Я пока-что очень мало пользуюсь наследованием. В основном классы у меня взаимодействуют по принципу parent-child, соответственно, у детей нет другого подходящего метода обратиться к родителю, кроме пользовательских событий.

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

jarofed
27 Февраля 2011
— 17:43
#

To jarofed:

Рекомендую посмотреть здесь: http://evolve.reintroducing.com/2011/01/06/as3/as3-soundmanager-v1-4/

А также на форуме http://flashgamedev.ru/ было обсуждение данного класса управления звуками и его изменённая форма. Только не помню ссылку, задай в поиске SoundManager.

Вадим М.
27 Февраля 2011
— 17:48
#

> Мне кажется вполне удобным использование публичных методов в глобальном хранилище: например когда все игровые объекты вызывают универсальный метод из хранилища g.sound() там где нужно воспроизводить звук, а в самом методе уже выполняется доступ к менеджеру звуков

Да - так вполне можно сделать.

Какая в этом подходе может возникнуть проблема в некоторых случаях? Я попробую описать на примере.

Вот смотри - у тебя есть в Global переменная key (это менеджер кнопочек, правда?). А она на самом деле нужна всем игровым классам? Врагам можно проверять текущую клавишу на клавиатуре?

Обычно хорошая идея обрабатывать взаимодействие с контроллером в главном игровом классе. Тогда, при изготовлении версии для планшета с поддержкой Флэш, например, тебе нужно будет исправить только один класс, а не 100 мест по всему коду. Ведь на планшете клавиатуры нет.

Если ты согласен, что g.key вызывать плохая идея по всей игре, то у тебя есть два варианта:
- Просто где-то в комментариях написать "g.key использовать только в главном игровом классе, хотя он и всем доступен, но кто будет вызывать его в потомках GamePlayer и GameEnemy, руки оторву ;)".
- Не передавать g в параметры Enemy. У Enemy будет только SoundPlayer и Effects по отдельности.

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

Но если ты просто в базовом классе Enemy будешь иметь только указатель на SoundManager и на Effects (а не указатель на Globals), то вышеописанная идея будет четко описана в коде. ? твой неопытный напарник, или ты сам в запарке боя, просто не сможешь допустить расползания g.key по игре.

Ты можешь сказать - "ну я выкину все такие ненужные ссылки из Globals". Вот тогда твой код станет просто отличным! Только класс тогда лучше назвать как-то получше. Например, GameActorServices и сказать, что это сервисы доступные для игровых акторов.

В конце немного практических мыслей:
- В своих играх я стараюсь очень четко держать отдельным и независимым слой "мета игры" (карта уровня, unlockable levels, агрегированная статистика). Я реализовал один раз Microsoft GDK API. Я не хочу это делать второй раз в Gluey 2. ;) В эти классы я не пускаю ни одного элемента, специфичного для игры. ? наоборот игровые классы только бросают события и ничего не знают про этот слой. ?наче очень трудно из Gluey сделать Gluey Tournament.
- Управление игрой. Я пишу программу из соображений, что в последний день может потребоваться переключить управление с мыши на клавиатуру и наоборот. Ни один игровой класс не имеет права читать с клавиатуры, видеть мышь и т.п.
- Движки и компоненты. Например, компонента "летающий текст" или "нахождение пути". Они не должны зависеть ни от чего (ни от звука, ни от эффектов, ни от игровых классов). Чтобы в любой игре я их мог поюзать.
- Сами игровые классы. А вот этим ребятам я разрешаю больше вольности - они могут дергать что угодно. В конце концов редко можно по настоящему реюзнуть врага в другой игре.

Sergey Batishchev
27 Февраля 2011
— 18:05
#

> @Sergey Batishchev, а как на счет взаимодействия игровых объектов с игровым движком, объектам следует самостоятельно добавляться в движок или движок при создании объектов должен сам их удалять и добавлять?

Антон - я уверен, что тут нет одного правильного ответа, иначе бы такая инструкция поставлялась в комплекте с Adobe CS5. ;)

Я сделал так в Gluey 2. Есть интерфейс IGameWorld. Он дает такие методы:
- createBlob(blobDef:Object) : GlueyBlob - создает блоб из описания блоба и добавляет его в мир.
- getBlob(index:int) : GlueyBlob
- get blobCount
- removeBlob(blob:GlueyBlob)
- removeBlobGroup(blob:GlueyBlob)

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

Однако Gluey - вполне конкретная игра с конкретными требованиями. В другой архитектуре смысл могут иметь другие решения.

Sergey Batishchev
27 Февраля 2011
— 18:31
#

@jarofed, спасибо за идею! По поводу звуков я хотел рассмотреть простой пример работы со звуками на примере уроков по TowerDefence. Так что рано или поздно материал по этой теме точно будет :)

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

Кстати, как уже верно было замечено в одном из комментариев, игровые классы не должны влиять на всю звуковую систему игры в целом, они могут только давать команду звуковому менеджеру о каких-то произошедших событиях чтобы тот воспроизвел соответствующий звук. Команды можно подавать через отправку событий или напрямую через вызов специального метода звукового менеджера: SoundManager.playSound(...); Ну и отключать и включать звуки в игре соответственно должна лишь одна специальная кнопка или потеря фокуса флешки ;)

Ant.Karlov
27 Февраля 2011
— 20:38
#

@Sergey Batishchev,

> Вот смотри - у тебя есть в Global переменная key (это менеджер кнопочек, правда?). А она на самом деле нужна всем игровым классам?

Да, это просто пример того что туда можно запихнуть управлялку кнопками. В действительности эта управлялка конечно никому не нужна, но при разработке я себя поймал на том, что порой очень удобно вешать разнообразные дебажные методы (юнит тесты) на горячие клавиши :) А вообще у меня в таком хранилище в основном как раз находятся всякие дебажные инструменты, например консоль — нужна она там чтобы любой класс мог зарегистрировать в ней свою команду с callback методом ну и записать в нее какую-нибудь запись если что-то отломалось.

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

Спасибо за развернутые комментарии! Они очень хорошо дополняют запись тем, что я не решился написать из-за своей неопределенности в целесообразности использования.

Ant.Karlov
27 Февраля 2011
— 20:59
#

@Sergey Batishchev,

> Антон - я уверен, что тут нет одного правильного ответа, иначе бы такая инструкция поставлялась в комплекте с Adobe CS5.

Понятно, я так и думал :) Просто когда ты начал говорить о независимости объектов (классов) я наверное слишком все обобщил и начал переживать, что возможно чего-то не понимаю во взаимодействии классов. Так как мои игровые объекты дают команды звуковому менеджеру пошуметь, сами добавляются в мир при создании и удалятся из него самостоятельно когда «умирают», так же сами разбегаются по своим кэшам когда не нужны — это же намного удобнее когда каждый класс знает из какого кэша он вылез, куда добавляться и откуда удалятся, чем, например парсить все объекты из основного класса игры и распихивать их по местам :)

Ant.Karlov
27 Февраля 2011
— 21:15
#

?нтересный подход. Может быть, к нему приду, когда начну запутываться :)

Но сейчас у меня функции, к примеру, проигрывания звука, реализованы как static класса SoundManager, и это работает. Такой подход имеет право на существование, или есть какие-то подводные камни, с которыми не пришлось пока столкнуться?

General
27 Февраля 2011
— 22:22
#

По хорошему надо использовать Singleton именно как глобальное хранилище данных. Для статических глобальных функций используются утилитные классы типа SoundUtil, TimeUtil, StringUtil. Все остальное (ссылки на объекты) должны реализовываться средствами ООП, иными словами хорошей архитектурой проекта. Посоветовал бы на досуге рассмотреть pureMVC - для игр она не сгодиться, но многие вещи станут понятными и вы сможете использовать эту концепцию в своих проектах.

shaman4d
28 Февраля 2011
— 11:36
#

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

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

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

Вобщем, все должно быть в меру и сообразно целям.

BuxomBerry
1 Марта 2011
— 20:51
#

Я у себя сделал разделение на основные игровые классы и вспомогательные (утилитные). Все основные классы связаны в логичную структуру, никаких синглтонов не требуется. Утилитные классы (менеджер событий, менеджер звуков и музыки) реализуются через статичные функции в соответствующих классах. Весьма удобно, не нужно ничего нигде собирать - всё и так доступно.

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

elmortem
2 Марта 2011
— 01:13
#

P.S. Антон, ты не мог бы немного растянуть комментарии - эти столбики очень неудобно читать. ):

elmortem
2 Марта 2011
— 01:14
#

@elmortem:
?нтересно во что превратится код, когда он весь будет наполнен вот такими конструкциями:

instanceManager.GetBulletsInRadius(center,radius);
instanceManager.GetFireballsInRectangle(left,top,width,height);
instanceManager.GetNearestSoldier();
instanceManager.GetFarthestSoldier();

и как апофеоз:
instanceManager.GetNearestObjectInRadiusAtLevelWithGun(center,radius,current_level,"shovel");

? даже если поменять подход, то все равно это будет выглядеть вот так:

var collection_id = g.Collect(test_function);
g.Sort(collection_id,sort_function);
if (g.Length(collection_id) > 0) this.Reaction();
g.Apply(collection_id,apply_function);

? где тут улучшение читаемости программы? Шило на мыло получается.

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

BuxomBerry
2 Марта 2011
— 09:49
#

@elmortem, да, комментарии надо полностью переделывать для более удобной работы с ними. Как-то не рассчитывал я ранее, что в большинстве своем комментарии будут получаться в формате интересных дискуссий :)

Ant.Karlov
3 Марта 2011
— 20:30
#

@BuxomBerry,

> instanceManager.GetBulletsInRadius(center,radius);
instanceManager.GetFireballsInRectangle(left,top,width,height);
instanceManager.GetNearestSoldier();
instanceManager.GetFarthestSoldier();


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

В моих проектах, как я уже писал ранее, основной класс, например Engine — выступает как основной менеджер, то есть движок игры к которому имеют доступ все другие игровые объекты. Поэтому у всех игровых объектов в базовом классе есть инициализация указателя на движок:

_engine = Engine.getInstance();

Далее все потомки уже по умолчанию имеют доступ к игровому движку. Само же ядро, например делится на физику, графику, эффекты и звук. Далее к каждому из этих менеджеров можно получить доступ например так:

_engine.graphics.add(this);
_engine.physics.CreateBody(bodyDef); // box2d
var eff:MyEffect = _engine.effects.get("explosion");
eff.init(dx, dy, angle, scale);
_engine.sound("explosion");

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

Ant.Karlov
3 Марта 2011
— 20:44
#

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

Неужели в ваших проектах ни один игровой объект никогда не обращается к другим игровым объектам подобным образом:
other = system.GetAnyOther(); //получаем указатель
other.pos.x //проверяем или изменяем
other.speed.x //аналогично
other = null //освобождаем указатель
?
Уверен, что еще как обращаются!

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

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

Но, наверно не убедил, да? :)

BuxomBerry
4 Марта 2011
— 11:28
#

@BuxomBerry, объекты просто обязаны иметь возможность передавать друг-другу какую-либо информацию о себе или о своих действиях в игровом мире — без этого не обойтись. Но совсем другое дело как осуществлять между собой общение этих самых объектов.

Берем опять же пулю — тут все как в жизни. Кто-то выпускает пулю и другие объекты ничего о ней не подозревают пока она не столкнется с кем-нибудь, а когда возникает коллизия пули с объектом, она ему как бы говорит: «привет, я пуля! Урон мой такой-то, скорость такая, прилетела оттуда...» и только тогда объект должен отреагировать на неё — это намного оптимальнее чем каждый игровой объект будет искать вокруг себя пули и проверять не столкнулась ли они с ним.

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

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

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

Не знаю как у вас, а у меня типичный платформер — это фактически крупный программный комплекс уже :) ? если не следовать правилам ООП то с каждым новым игровым объектом, возможность добавить новые игровые элементы становится все большим геморроем.

Ant.Karlov
4 Марта 2011
— 13:57
#

В жизни пуля просто летит и чтобы ее остановить как раз таки самой мишени нужно приложить усилия, а пуле все равно куда лететь и что пробивать, она будет двигаться пока не расплющится :)

Тем более ей все равно в какую поверхность она попала и что при этом вылетает. Сам факт, что пуля определяет поверхность с которой столкнулась и знает при этом какой спецэффект ей создать говорит о том, что для каждой новой поверхности пулю придется доучивать. Уже не так оптимально, не так ли?

?дем дальше... Третий абзац... "после сообщения цели о том что она в него попала"

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

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

А насчет статичных объектов. Ну вот скажите какая разница (в плане оптимальности выполнения кода), что 1000 статичных объектов проверит себя на коллизию с 3мя пулями, что 3 пули проверят себя на коллизию с 1000 статичных объектов? Без дополнительных оптимизаций игрового пространства проделанная работа будет одинакова (при условии, что ударившиеся пули будут исключаться из проверок).

Но, опять же соглашусь, лучше когда внутри кода пули в одном месте просматриваются сразу все мишени (если при этом мишени не делают того же). Всегда лучше, когда все собрано в одном месте, так легче работать с кодом. Да и статичные объекты порой кешируются и вовсе не могут самостоятельно выполнять какой-то код.

Вот я не могу представить игровой объект, который влияет на игровой мир больше чем ему нужно. Спрайтовое яблоко на статичном дереве крутить небо, землю и звезды вокруг себя? Ну вот не знаю даже что тут сказать :) Соглашусь разве что с этой очевидной истиной.

Все таки крупный программный комплекс это что-то другое :) Вот АСУТП системы, это крупный программный комплекс.
Один человек программируя небольшую игрушку не должен испытывать больших трудностей при структурном подходе. Даже наоборот пользу от этого определенную получать должен.

Пожалуй, Антон, я утомил вас своими спорами :) ? от темы поста уже отдалились. Вобщем, я во многом с вами согласен, может быть даже кто-то почерпнет из нашего спора что-то полезное :)

BuxomBerry
4 Марта 2011
— 15:24
#

Соглашусь с shaman4d, стоит посмотреть на PureMVC, хотя бы на её pdf, есть на русском языке. Как раз о том, что вы тут пишете. В PureMVC чётко показано разделение, где одна часть взаимодействует с другой только событиями или командами. Очень советую тем, кто ещё размышляет "кто о ком и сколько должен знать".

CJay
28 Сентября 2011
— 18:05
#

package
{
import ru.exteer.service.StaticLib;
public const $LIB:StaticLib = new StaticLib();
}

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

DG
2 Декабря 2011
— 18:44
#

Object.prototype.a = 1
trace(new MovieClip().a)//1

=)

хотя в строгом режиме будут ошибки в некоторых местах(почему - долго описывать)

Евгеша
24 Декабря 2011
— 22:09
#

+ в строгом режиме можно ошибки избежать, если писать this.a, this["a"]

Евгеша
24 Декабря 2011
— 22:11
#

УУфф..Хорошо быть чайником..а потом такое откинуть...что все гуру..на уши стануть..
Хотите? Вот , к прмеру вытащите "сундучёк "
с Аркады " Снукер". и пакажите управление консоли ..Стойки на ушах гарантирую.
!1,.Штука.один В заначе...Всеми геймами мировой контороль....Ага..хитрющие..

Виктор,viktor.m46@list.ru
28 Июня 2014
— 19:38
#

Спасибо за информацию. Там можно хранить и игровые объекты и игровые эффекты?

игрок
16 Октября 2015
— 16:36
#

@игрок,

> Там можно хранить и игровые объекты и игровые эффекты?

Хранилище подобного плана подразумевает хранение указателей на существующие игровые объекты или переменные игрового мира. Для хранения визуальных ресурсов игры существуют хранилища данных иного типа.

Ant.Karlov
17 Октября 2015
— 14:19
#