Ещё забыл упомянуть: при возбуждении исключения в одном из обработчиков стандартных событий C#, последующие обработчики не будут выполнены. Ну нелепо же.
как эти проблемы решены в упомянутом Rx.
Они не то чтобы решены, они просто изначально не заложены в дизайн.
А что кривого в событиях C#? Опишите чем конкретно недовольно прогрессивное человечество
События в .NET являются синтаксическим сахаром для паттерна Observer. Причём сахаром довольно никчемным, потому что упрощения от них чуть, а ограничения весьма существенные.
Как правило среди наблюдателей (observers) и наблюдаемого (observable) большим сроком жизни (областью видимости) обладает последний. Это значит, что в их взаимодействии критически важной становится отписка от событий. В противном случае мы сталкиваемся с утечками памяти — подвисанием короткоживущих объектов, которые после своего использования недоступны сборщику мусора, даже если отсутствуют прямые ссылки на них в пользовательском коде (поскольку продолжают оставаться неявные ссылки из источника событий).
В нормальных реализациях паттерна Observer для отписки используются токены подписки (subscriptions), желательно совместимые с идиомой RAII (отписка через деструкторы в C++ или через IDisposable в C#). Именно так это сделано в Boost.Signals2 в C++ или в Rx в C#. Правильный синтаксический сахар в C# должен был выглядеть так (псевдокод):
Обычно observers и observable друг о друге ничего не знают, а подписку инициирует какая-то третья сторона (назовём её «менеджер»).
Так вот, в случае правильной реализации паттерна Наблюдатель, менеджеру не надо тащить за собой ссылки на подписчиков или источник событий. Скажем, менеджер получил в конструкторе один раз ссылки на подписчиков и издателя, связал их друг с другом и полностью забыл про них:
Для того, чтобы отписаться, ему не нужны ни подписчик, ни источник, достаточно безликого поля IDisposable:
_subscription.Dispose();
В случае отписки от событий C# всё сильно хуже, зачем-то нужны и подписчик, и издатель:
observable.SomeEvent -= observer.HandleSomeEvent;
То есть безосновательно увеличена связность.
Выше уже писали про кривой синтаксис возбуждения событий (raise events). В отсутствие подписчиков (зауряднейшая ситуация) вместо простого «ничего» получаем на ровном месте `NullReferenceException`. Причём нельзя просто так взять, и проверить на `null`, как в статье, потому что между проверкой и возбуждением события может отписаться последний подписчик, и вместо ожидаемого пустого invokation list получаем `null`; выше уже указывали на это.
Далее, события в C# не могут сигнализировать об окончании потока `OnComplete()` или об ошибке `OnError(exception)`. Не могут на лету комбинироваться и преобразовываться Linq-методами. Не предусмотрено библиотечного механизма для разделения «холодных» и «горячих» потоков событий, буферизующих, и т.д.
• А что там с ProBuilder? Никогда не пользовался; он позволяет настраивать уровни в редакторе? Но они же должны случайно генерироваться? При генерации используется детерминированный PRNG, или при каждом запуске результат может быть разным? Что именно случайного генерируется на уровнях, каковы пределы вариации интерьера?
• Тестировал на всех трёх платформах? Были ли проблемы с отличающимся поведением на разных платформах?
• Планируешь ли реализовывать мультиплеер?
• Какие шаги предпринимались для увеличения виральности; игроки как-то поощряются за популяризацию и привлечение новых игроков? Прикручены ли к игре соцсети?
Вообще, я перечитываю свои сообщения, вопросы какие-то «жадные» получились :) Просто потому, что интересная тема, прекрасная игра и хороший пост. Буду ждать соедующих выпусков! Спасибо за поддержку общения в комментариях.
• На какие игры ты ориентировался, когда делал свою игру? В какие игры переиграл, как их анализировал, из каких перенимал те или иные гейм-дизайнерские решения? От каких решений сознательно отказывался?
• Какие шаги принимал по оптимизации игры? Я так понимаю, производительность на ПК в таком сеттинге не особенно ограничивала, но тем не менее: считал ли draw calls, настраивал ли как-то батчинг, что там с тенями и освещением?
• Какие это вообще целевые платформы в Unity — Windows и GNU/Linux?
• Не очень понял комментарий про YouTube: что такое «монетизация видео»?
Собираешься ли портировать на мобильные платформы, и почему сразу на них не ориентировался?
Почему NGUI, а не Daikon Forge?
Где и как покупать/лицензировать арт и музыку?
Как бороться с рвотными позывами, глядя на код или API любого Unity-плагина из Asset Store?
Во-первых, единственный объект тоже является контейнером самого себя с одним тривиальным способом обхода.
Эдак тогда про любой паттерн можно сказать, что он напрямую связан с обходом и посещением. Синглтон, например.
Посетитель — это и идея инъекции алгоритма в алгоритм (полиморфизм со стороны посетителя), и идея взаимодействия с невидимыми извне разнотипными объектами (полиморфизм с принимающей стороны).
Педалировать какую-то одну грань и упускать другую — было бы странно.
Я не упускаю ни одну из этих твоих граней — двойная диспетчеризация (читай, «полиморфизм») по типу-объекта и по типу-операции — но по прежнему считаю, что к каким-либо контейнерам или деревьям паттерн не имеет никакого отношения. Единственный пример, который GoF придумали для своей книжки, был связан с обходом синтаксического дерева; вот они и назвали паттерн «Visitor».
это и идея инъекции алгоритма в алгоритм (полиморфизм со стороны посетителя), и идея взаимодействия с невидимыми извне разнотипными объектами (полиморфизм с принимающей стороны).
Это твоя идея. Моя идея, например, в том, что Посетитель транспонирует иерархию из n исходных классов с m операциями над этими классами в параллельную иерархию из m классов-операций (визиторы), каждый с n методами посещения (по одному на каждый класс исходной иерархии). И всё это как вариант решения т.н. expression problem: в ООП добавление метода в иерархию является ломающим изменением, а добавление класса — нет. Поэтому операцию добавления функции в иерархию классов сводят к добавлению класса в навешенную иерархию визиторов. Ситуация дуальна — теперь сложно добавлять классы в исходную иерархию, так как требуется добавлять функции посещения (ломающее изменение) в иерархию визиторов.
Итератор и Посетитель, конечно, ходят рука об руку.
Visitor вполне может применятся даже к единственному объекту, «контейнеры» и «обходы» вообще к нему никаким боком. На эту тему есть хорошая статья (или глава из книжки) Вендевурда (Vandevoorde), но сходу не могу нагуглить. Тогда приведу цитату другого известного плюсиста, из «My Most Important C++ Aha! Moments… Ever» by Scott Meyers: «Then, one day, I made a fundamental realization: the Visitor Pattern has nothing to do with visitation... Because I was so fixated on the pattern’s name, I couldn’t get past the idea that Visitor had something to do with visitation or iteration or something like that.»
Тут стоит подчеркнуть, что у паттерна Посетитель есть две независимые стороны
— разнести алгоритм работы с контейнером и алгоритм работы с элементом
Разнести алгоритм работы с контейнером и элементом призван паттерн Итератор. Паттерну Посетитель вообще безразличны какие-то контейнеры, графы, деревья, etc. Просто множественная диспетчеризация.
Я не понял, почему не надо класть положительность эпсилон в импликацию слева под квантором и почему утверждение про связанную переменную «внешнее», только и всего.
В последней формуле куски «m > N» и «ε > 0» имеют разную природу. В первом случае есть переменная N. Она не может быть вынесена вне формулы как
...∃m ... (...; m ∈ Z>N)
потому что на этом уровне уже никакой N нет, она связана квантором, не является свободной. Поэтому соответствующий bounded quantifier существования должен раскрываться в конъюнкцию (соответственно, для квантора всеобщности в импликацию).
Можно рассматривать кванторы более общо, чисто синтаксически, как «штуки, уменьшающие арность предиката». По аналогии: абстракция, записываемая через λ — тоже «штука, уменьшающая арность выражения», например: «λx . x + 1». Т.е. в результате получается предикат (в случае кванторов) или выражение (в случае лямбды) с меньшим количеством плейсхолдеров. Если свяжем или зафиксируем все переменные, то такой предикат арности 0 можно называть высказыванием.
при том что это утверждение по схеме выделения равносильно ε ∈ R ∧ ε > 0
А почему тогда не писать «ε ∈ C ∧ (ε ∈ R ∧ ε > 0)»? Это тоже равносильно «ε ∈ R+».
Вообще, не понимаю, что может быть неинтуитивного в разворачивании всеобщности в импликацию и существования — в конъюнкцию
Так я её так и развернул. Неинтуитивность неразвёрнутых bounded quantifiers проявляется, например, при построении отрицания.
Смотрите, вот исходная (простая и правильная на мой взгляд) формула (функция x : N → R введена где-то выше):
∀ε ∃N ∀n ∀m n > N ∧ m > N → |xn − xm| < ε (ε ∈ R+; N, n, m ∈ N)
Длинными горизонтальными интервалами она разбита на три куска. Отрицание строится обращением всех кванторов в левом куске; в среднем куске для удобства отрицание спускается внутрь предиката по законам де Моргана и формуле отрицания импликации ¬(a → b) ⇔ a ∧ ¬b. Правый же кусок, который играет роль «типов», или «области определения», you name it — не претерпевает никаких именений:
∃ε ∀N ∃n ∃m (n > N ∧ m > N) ∧ |xn − xm| ≥ ε (ε ∈ R+, N, n, m ∈ N)
А теперь постройте отрицание для формулы, где правый кусок вшит внутрь в bounded quantifiers или в импликации/коньюнкции. Эти коньюнкции при построении отрицания превратятся в кадавров типа «… ∀mm ∉ N ∨ ¬...»
Схема выделения тут, очевидно, ни при чём — тут даже нет «переменных-множеств». И даже ZF ни при чём — в курсе матана традиционно используется наивная теория множеств, ну и пусть. Речь идёт о том, как от матанистических «ограниченных кванторов» (на которые даже отрицания трудно навешивать) перейти к формуле с обычными логическими кванторами; это ортогонально теориям множеств.
По упомянутой ссылке в последнем разделе говорится про кванторы, которые помимо переменной содержат некий терм t, который может содержать вхождение свободных (несвязанных) переменных. В случае же «ε > 0» подобного терма нет — нужно смотреть, что в упомянутой статье в начале. В этом случае, если мы перепишем более строго «ε ∈ R+», то здесь «∈» понимается как в некотором смысле тип («ε : R+»). В отличие от высказывания вроде «ε ∈ Uδ», которое может быть как истинным, так и ложным, в зависимости от того, попадает ли ε в Uδ при тех или иных условиях (не можем написать «ε : Uδ»).
Они не то чтобы решены, они просто изначально не заложены в дизайн.
События в .NET являются синтаксическим сахаром для паттерна Observer. Причём сахаром довольно никчемным, потому что упрощения от них чуть, а ограничения весьма существенные.
Как правило среди наблюдателей (observers) и наблюдаемого (observable) большим сроком жизни (областью видимости) обладает последний. Это значит, что в их взаимодействии критически важной становится отписка от событий. В противном случае мы сталкиваемся с утечками памяти — подвисанием короткоживущих объектов, которые после своего использования недоступны сборщику мусора, даже если отсутствуют прямые ссылки на них в пользовательском коде (поскольку продолжают оставаться неявные ссылки из источника событий).
В нормальных реализациях паттерна Observer для отписки используются токены подписки (subscriptions), желательно совместимые с идиомой RAII (отписка через деструкторы в C++ или через IDisposable в C#). Именно так это сделано в Boost.Signals2 в C++ или в Rx в C#. Правильный синтаксический сахар в C# должен был выглядеть так (псевдокод):
private IDisposable _subscription; ... _subscription = publisher.SomeEvents += subscriber.HandleSomeEvent; ... _subscription.Dispose();Токен подписки не требует тащить всю дорогу явную ссылку на обработчик, чтобы отписаться. Но в C# последнее сделано иначе:
В частности, это значит, что при подписке нельзя так просто взять и использовать лямбды, скажем, обернуть предыдущий вызов:
publisher.SomeEvents += (sender, e) => { Debug.Log(“Some message.”); subscriber.HandleSomeEvent(sender, e); }потому что:
Обычно observers и observable друг о друге ничего не знают, а подписку инициирует какая-то третья сторона (назовём её «менеджер»).
Так вот, в случае правильной реализации паттерна Наблюдатель, менеджеру не надо тащить за собой ссылки на подписчиков или источник событий. Скажем, менеджер получил в конструкторе один раз ссылки на подписчиков и издателя, связал их друг с другом и полностью забыл про них:
Для того, чтобы отписаться, ему не нужны ни подписчик, ни источник, достаточно безликого поля IDisposable:
В случае отписки от событий C# всё сильно хуже, зачем-то нужны и подписчик, и издатель:
То есть безосновательно увеличена связность.
Выше уже писали про кривой синтаксис возбуждения событий (raise events). В отсутствие подписчиков (зауряднейшая ситуация) вместо простого «ничего» получаем на ровном месте `NullReferenceException`. Причём нельзя просто так взять, и проверить на `null`, как в статье, потому что между проверкой и возбуждением события может отписаться последний подписчик, и вместо ожидаемого пустого invokation list получаем `null`; выше уже указывали на это.
Далее, события в C# не могут сигнализировать об окончании потока `OnComplete()` или об ошибке `OnError(exception)`. Не могут на лету комбинироваться и преобразовываться Linq-методами. Не предусмотрено библиотечного механизма для разделения «холодных» и «горячих» потоков событий, буферизующих, и т.д.
Самого главного-то и не написал: сколько тянешь, приседаешь, жмёшь, и какой собственный вес?
• Тестировал на всех трёх платформах? Были ли проблемы с отличающимся поведением на разных платформах?
• Планируешь ли реализовывать мультиплеер?
• Какие шаги предпринимались для увеличения виральности; игроки как-то поощряются за популяризацию и привлечение новых игроков? Прикручены ли к игре соцсети?
Вообще, я перечитываю свои сообщения, вопросы какие-то «жадные» получились :) Просто потому, что интересная тема, прекрасная игра и хороший пост. Буду ждать соедующих выпусков! Спасибо за поддержку общения в комментариях.
• Какие шаги принимал по оптимизации игры? Я так понимаю, производительность на ПК в таком сеттинге не особенно ограничивала, но тем не менее: считал ли draw calls, настраивал ли как-то батчинг, что там с тенями и освещением?
• Какие это вообще целевые платформы в Unity — Windows и GNU/Linux?
• Не очень понял комментарий про YouTube: что такое «монетизация видео»?
Почему NGUI, а не Daikon Forge?
Где и как покупать/лицензировать арт и музыку?
Как бороться с рвотными позывами, глядя на код или API любого Unity-плагина из Asset Store?
Что можно почитать на тему этих мельчайших механизмов?
Эдак тогда про любой паттерн можно сказать, что он напрямую связан с обходом и посещением. Синглтон, например.
Я не упускаю ни одну из этих твоих граней — двойная диспетчеризация (читай, «полиморфизм») по типу-объекта и по типу-операции — но по прежнему считаю, что к каким-либо контейнерам или деревьям паттерн не имеет никакого отношения. Единственный пример, который GoF придумали для своей книжки, был связан с обходом синтаксического дерева; вот они и назвали паттерн «Visitor».
Это твоя идея. Моя идея, например, в том, что Посетитель транспонирует иерархию из n исходных классов с m операциями над этими классами в параллельную иерархию из m классов-операций (визиторы), каждый с n методами посещения (по одному на каждый класс исходной иерархии). И всё это как вариант решения т.н. expression problem: в ООП добавление метода в иерархию является ломающим изменением, а добавление класса — нет. Поэтому операцию добавления функции в иерархию классов сводят к добавлению класса в навешенную иерархию визиторов. Ситуация дуальна — теперь сложно добавлять классы в исходную иерархию, так как требуется добавлять функции посещения (ломающее изменение) в иерархию визиторов.
Visitor вполне может применятся даже к единственному объекту, «контейнеры» и «обходы» вообще к нему никаким боком. На эту тему есть хорошая статья (или глава из книжки) Вендевурда (Vandevoorde), но сходу не могу нагуглить. Тогда приведу цитату другого известного плюсиста, из «My Most Important C++ Aha! Moments… Ever» by Scott Meyers: «Then, one day, I made a fundamental realization: the Visitor Pattern has nothing to do with visitation... Because I was so fixated on the pattern’s name, I couldn’t get past the idea that Visitor had something to do with visitation or iteration or something like that.»
Разнести алгоритм работы с контейнером и элементом призван паттерн Итератор. Паттерну Посетитель вообще безразличны какие-то контейнеры, графы, деревья, etc. Просто множественная диспетчеризация.
В последней формуле куски «m > N» и «ε > 0» имеют разную природу. В первом случае есть переменная N. Она не может быть вынесена вне формулы как
...∃m ... (...; m ∈ Z>N)
потому что на этом уровне уже никакой N нет, она связана квантором, не является свободной. Поэтому соответствующий bounded quantifier существования должен раскрываться в конъюнкцию (соответственно, для квантора всеобщности в импликацию).
Можно рассматривать кванторы более общо, чисто синтаксически, как «штуки, уменьшающие арность предиката». По аналогии: абстракция, записываемая через λ — тоже «штука, уменьшающая арность выражения», например: «λx . x + 1». Т.е. в результате получается предикат (в случае кванторов) или выражение (в случае лямбды) с меньшим количеством плейсхолдеров. Если свяжем или зафиксируем все переменные, то такой предикат арности 0 можно называть высказыванием.
Почему вместо «ε ∈ R+» должны писать «ε ∈ R ∧ ε > 0»; но при этом не пишем «m ∈ Z ∧ m > 0» вместо «m ∈ N»?
Мне не нужны типы.
А почему тогда не писать «ε ∈ C ∧ (ε ∈ R ∧ ε > 0)»? Это тоже равносильно «ε ∈ R+».
Так я её так и развернул. Неинтуитивность неразвёрнутых bounded quantifiers проявляется, например, при построении отрицания.
Смотрите, вот исходная (простая и правильная на мой взгляд) формула (функция x : N → R введена где-то выше):
∀ε ∃N ∀n ∀m n > N ∧ m > N → |xn − xm| < ε (ε ∈ R+; N, n, m ∈ N)
Длинными горизонтальными интервалами она разбита на три куска. Отрицание строится обращением всех кванторов в левом куске; в среднем куске для удобства отрицание спускается внутрь предиката по законам де Моргана и формуле отрицания импликации ¬(a → b) ⇔ a ∧ ¬b. Правый же кусок, который играет роль «типов», или «области определения», you name it — не претерпевает никаких именений:
∃ε ∀N ∃n ∃m (n > N ∧ m > N) ∧ |xn − xm| ≥ ε (ε ∈ R+, N, n, m ∈ N)
А теперь постройте отрицание для формулы, где правый кусок вшит внутрь в bounded quantifiers или в импликации/коньюнкции. Эти коньюнкции при построении отрицания превратятся в кадавров типа «… ∀m m ∉ N ∨ ¬...»