- Сообщений: 469
- Спасибо получено: 850
ООП на ГеймМейкере
К сожалению, ГеймМейкер поддерживает ООП только частично. Если считать его понятие "Объекта" классом, а понятие "Инстанса" - экземпляром или объектом (чтобы терминология данной статьи совпадала с принятой в ООП), то у объектов в ГеймМейкере могут быть параметры, но туго - с методами. Методами, в принципе, можно считать реакции на события, поскольку там доступен вызов "родительская реакция", то есть то же, что в ООП часто делается при переопределении метода. Но все реакции относятся к тем или иным событиям, таким как отрисовка сцены или столкновение с другим объектом - кроме шестнадцати (!) пользовательских реакций, которые вызываются только специально из кода. Мало того, что их количество ограничено - они ещё и не имеют названия, только номер. Даже QBasic не вызывает функции по номерам!
Можно, конечно, забить кучу констант: ПУЛЯ_ИНИЦИАЛИЗАЦИЯ=1, ПУЛЯ_ПОСЧИТАТЬ_УРОН=2, ПУЛЯ_ПРОИЗВЕСТИ_ВЗРЫВ
... Но ограничение на 16 методов никуда не денется (значит, невозможно будет писать "Чистый код", одним из принципов которого является разбиение на минимальные по смыслу фрагменты), и к тому же я предчувствую, что такой код чреват ошибками, если вызвать у объекта метод не той константой: ты будешь думать, что это пуля производит взрыв (ведь это и написано в коде!), а это самолёт катапультирует экипаж.Мне кажется, я нашла способ лучше.
Допустим, нам нужен класс "Снаряд", у которого будет метод "взрываться" - но каждый вид снаряда взрывается по-своему: кто-то обдаёт врагов краской, кто-то наносит урон, кто-то строит башню, а кто-то производит меньшие снаряды другого типа. В общем, у класса "Снаряд" есть потомки "РадужныйСнаряд" (со своими потомками "СинийСнаряд", "КрасныйСнаряд" и "ЖёлтыйСнаряд"), "Бомба", "СтроительныйСнаряд" (потомки по типам башен) и "ОсколочныйСнаряд", каждый из которых должен реализовывать метод "взрываться" по-своему.
Создаём скрипты "цветной_взрыв", "повреждающий_взрыв", "строительный_взрыв" и "осколочный_взрыв". Они просто висят в группе ассетов Scripts (или можно разместить по подпапкам, например, в папку "Снаряд/Взрывы"). Они не привязаны никак к классам, которые будут обслуживать.
При создании объекта "РадужныйСнаряд" присваиваем его переменной метод_взрыва значение цветной_взрыв. Не строку "цветной_взрыв"! А просто - цветной_взрыв. Потому что это обозначение в коде возвращает уникальный номер скрипта "цветной_взрыв", по которому его можно вызвать. Аналогично поступаем с остальными снарядами. У вторичных потомков "Снаряда" (например, "ЖёлтыйВзрыв") значение переменной "метод_взрыва" уже задано, поскольку реакция на событие "создание объекта" наследуется (если вы переопределяете его, не забудьте добавить сначала вызов родительской реакции).
Когда необходимо взорвать снаряд - например, при столкновении с целью или стенкой, при попадании в поле взрывания снарядов, при клике на снаряд, команде из консоли, да мало ли по каким причинам! - достаточно запустить код:
Не нужно кавычек, не нужно получать ссылку на метод по строковому названию - запись "метод_взрыва" является названием переменной, где уже содержится номер соответствующего скрипта. Главное, чтобы совпадал контекстный объект: нужно чтобы этот код считал за self собственно снаряд. Если же вызов происходит из другого контекста, следует использовать with. Допустим, если номер искомого снаряда находится в переменной "вот_его_взорвать", нужно записать:
Вот и всё: мы взрываем произвольный снаряд (полиморфизм) уникальным для него способом, являющимся для нас чёрным ящиком (инкапсуляция).
[hr]Недостатки способа
Поскольку это всё-таки не полноценное ООП, возникает несколько трудностей.
Во-первых, чтобы вызвать родительский метод, надо знать, что это за метод; или же держать в переменной не просто номер метода, а массив последовательных методов, обрабатывая каждый вызов специальным скриптом. Не знаю, как это скажется на производительности. С другой стороны - в Яваскрипте тоже надо знать родительский метод, чтобы вызвать его (вызов super() поддерживается не всеми браузерами).
Во-вторых, в предложенном варианте каждый отдельный объект (каждый инстанс!) хранит номера всех своих методов. Это позволяет изменять функционал отдельных объектов на лету, что также используется в таких динамических языках как Яваскрипт и Руби, но если мы подобной функции использовать не собираемся - то есть повод поволноваться о производительности. Не уверена, что ГеймМейкер оптимизирует начальные значения переменных у объектов одного типа, и может оказаться, что 100 пуль на экране - это 100 х 2 байт под хранение целого числа с номером одного и того же метода. Умножить на количество методов! Если будет заметно проседание производительности из-за этого, то придётся что-то думать. Как обычно, чем универсальнее решение и удобнее для изменения на лету, тем больше страдает оптимальность (это же и причина, почему ГеймМейкер не самый проворный движок).
Наконец, в ГеймМейкере нет такого понятия как "интерфейс", так что не получится следовать принципу Loose coupling .
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Надо бы открыть гейм-мейкер еще раз и попытаться, наконец, пройти дальше первого туториала. А то так уж вышло, что он оказал влияние на множество других движков, вышедших позднее, и многие конструкторы сильно на него похожи.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
А откуда, коль не секрет, такая информация? Где-то в 2015 промелькнула какая-то хилая новость о том, что у YoYo Games сменился владелец, и они пообещали выпуск в 2016, но тут же сказали что поторопятся, и это будет прямо ещё в 2015. А в итоге вот, последний квартал 2016 наступает, тогда как 31 марта 2016 выпустили новый экспортер - зачем бы, если скоро GM:S должен был прийти конец?EvilCat пишет: Вроде бы они сейчас делают новый ГеймМейкер, который наконец оставит всё это в прошлом. Есть и негатив: поддержка текущей студии будет прекращена, а она стоит куда побольше, чем РПГ Мейкер, например. Лицензию на новый придётся покупать заново, разве что с какой-то скидкой.
По сути поста могу сказать, что идея с нумерацией через ассеты довольно изобретательна, но такой уровень гибкости - для каждого экземпляра свои переназначаемые методы - мне непонятно зачем может потребоваться. Не будет ли это удобней решить через обычное наследование с полиморфизмом?
Правда, я не могу сказать что использование ООП столь важно, и уж точно это не критично, когда стоит реальная цель - сделать игру. Если от глупостей программиста не уберегает документация проекта и его/ей собственный здравый смысл, то сокрытие данных и методов тут ничем не поможет, а навредить не помешает.
До-студийный Game Maker был лучше - в нём была кое-какая рефлексия, можно было добавлять объектам события на лету, и даже интерпретировать код, собранный на лету из любых строк.
В GM8.1 я мог сделать вспомогательный инструмент вон какой . А в GM:S - только этот огрызок . Впрочем, это уже совсем оффтоп.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Я, к сожалению, уже не помню, где читала о грядущей новой версии, но тревога и досада, что распродажа ГМ устроена из-за того, что вот-вот выпустят новую версию и сделают свежекупленное устаревшим, тогда витала много где.
Насчёт полиморфизма. Это действительно один из самых удобных подходов в программировании игровой логики. Не говоря уже о том, что один из самых распространённых... В статье я вроде постаралась описать, зачем это может понадобиться.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Я сам постоянно использую полиморфизм на практике, в нашей игре, которую я тут показал в этом же разделе, его порядочно много. Всё это достигается, увы, только через скрипты. Но Гамак того правда стоит, уж поверьте. Скоро вот в Стим выходим, а сколько мы бы торчали на ЯП общего назначения, даже не знаю.
В общем, пробуйте, люди, Гамак. Он с точки зрения типичного программирования странный, но для своих целей вполне функциональный.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
- strelokhalfer
-
- Не в сети
- Живу я здесь
-
- Знатный грамотей
- Сообщений: 1640
- Спасибо получено: 1080
P.S. В руби нестрогая)
"Стрелок, что-то ты неочень похож на свой аватар..."(с)
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Для начала, это не просто "в ту переменную можно записать только числа, а в другую - только строки". Числа отличаются по наличию знака, минимальному и максимальному значению, точности после запятой. Причём некоторые типы - это или очень большое число, или очень много знаков после запятой. Если думал, что HP влезет в 32 000, а потом оказалось, что нужно поднять до 50 000 - придётся ползать по всей программе, менять какой-нибудь int на longint во всех полях и аргументах, которые оперируют хитами. В противном случае - либо останов программы, либо, что хуже, молчаливая ошибка. С точки зрения естественного рассуждения этот нонсенс, потому что ты хотел просто работать с _целым числом_, не думая о том, два байта оно занимает в памяти или три.
В строго типизированных языках есть такие сюрпризы как "деление целого на целое всегда даёт целое". Поделили 5 на 2, получили 2, а не 2.5, даже если записали в дробную переменную. Надо было делить 5.0 на 2.0 или (float)a/(float)b. А если потом понял, что возможностей float не хватает, придётся и тут менять на double какой-нибудь!
Объекты - отдельная песня. Объявил список животных: List<Animal>. Положил туда один объект - класса Dog. Достал объект, удостоверился, что он класса Dog, и попросил его залаять: if (animals[0] is Dog) animals[0].Bark(). Ошибка! Программа считает, что обращается к нему как к Animal, а не Dog, а родительский класс Animal ещё не умеет лаять. Но Animal умеет издавать звуки. Вызываем animals[0].Cry() - но даже если у Dog есть метод Cry, где сказано "гав!", вместо этого вызовется родительский метод класса Animal, скажем, "пика-пика!" - если программист забыл написать в заголовке родительского метода, что метод виртуальный, то есть зависит от объекта, а не класса обращения. А в первом случае нужно было делать (animals[0] as Dog).Bark().
Строгая типизация хороша при отлове некоторых ошибок, для оптимизации работы с памятью и для того, чтобы удобнее писать методы, рассчитанные на разные входные данные (сигнатуры). А если цель - просто писать игру, это лишний груз (я считаю).
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
+ ненужное усложнение сборки
От них может быть польза только в скриптах.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Большая часть случаев, когда переменная должна принимать значения разных типов - это переменные вида "true, false или null" (троичная логика), ленивые значения (null - если ещё не вычислено), всеядные аргументы функций и пользовательский ввод (считали "1", привели к 1 в ту же переменную). Очень редко когда шёл-шёл и внезапно присвоил числовой переменной строку! Нестрогая типизированность заключается в том, что язык или компилятор заботится о том, сколькими байтами описывать число.
Так или иначе, факт остаётся фактом. Если начнёшь объяснять вчерашнему пользователю ивентов на командах, что нельзя поделить 5 на 2 и получить 2.5, он просто не будет пользоваться этим минным полем. И правильно сделает. Хотя, возможно, в твоём понимании уровень использования языка, не отягощающий себя думами о байтах, и есть скрипты.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
И я выскажу свое мнение, Амфи я или кто? В целом - это побоку вообще. Динамическая (нестрогая, я так буду изъясняться, ибо) типизация позволяет не запарываясь реализовывать всякие полиморфизмы, ибо суть объекта можно представить словарем с методами и свойствами, индексируемых по имени. И при этом не надо париться по поводу наследований или интерфейсов.
Статическая, конечно же, помогает в отладке, ибо ты получишь жирное сообщение об ошибке при первых проявлениях симптомов. Приятно, да. Ну и плюшек производительности как правило больше.
Но блин, так и в обоих случаях минусы же тоже! Для динамической вряд ли будет полный отчет по переменной в вашей любимой ide, гадание что лежит в самой переменной (в мукере меня особенно радует гадание - ид ли героя тут, или сам герой?) и в целом неуверенность что метод\свойство у объекта попросту существует.
В статических не всегда есть встроенная рефлексия (впрочем уже редко), требуется везде озабачиваться интерфейсами, и строить лапшу для примера с псом, описанным ранее (если пес, то явное приведение + гавкнуть, если утка и т.п.). Далее курить паттерны, радоваться тому, что объясняешь программе как ей быть с памятью, хотя в другом ЯПе не заморачиваясь расписывал бы сами алгоритмы.
Но Амфи, скажете вы, что насчет игорь?
И тут самое главное. Играм побоку на чем они работают.
Но Амфи, вы снова перебьете меня, что насчет программистов?
Захотят приспособятся к ЯПу. Не захотят - не приспособятся.
Да, методы разные, да, каждый имеет свои плюсы/минусы, нет, играм глубоко наплевать, а прогеры пусть сами разберутся. Не нравится ЯП в конструкторе - не используй или меняй конструктор. Заставляют - учи или уходи с команды. Эта хрень не стоит холивара.
Ну и p.s. с гамаком не знаком, топик не читал, так что ничего по теме не скажу. И мне даже не стыдно.
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Вобщем краткий пересказ:
1. Про излишнее усложнение сборки и было сказано к ненужной нагрузке на компилятор, что приведет к увеличению времени сборки, шансу появления ошибок и псевдоошибок из-за недопонимания.
2. Вчерашний ивентист с той же легкостью передаст в функцию вместо строки число и будет долго не понимать, что-за нафиг.
3. Гораздо проще объяснить разницу между 2 и 2.0, чем между строкой и числом
4. Скрипты в моем понимании - относительно небольшие фрагменты кода, описывающие логику на верхних уровнях абстракции, найти несоответствие типов в которых гораздо проще и из-за малого размера, не сильно влияющие на проект в целом.
Амфи, приводить вобще не обязательно - есть виртуальные методы.
Описывать то, как программе быть с памятью тоже не обязательно, если есть сборшик мусора.
Про "представлять как словарем, индексируемым по имени и не нужно заморачиваться полиморфизмом" вобще не понял к чему это. Выглядит как добавление хаоса и непредсказуемости.
Играм вобще не наплевать на производительность. Каким-нибудь прогам, просто тянущим данные с интернета или составляющим список дел - возможно, но не играм.
Программистам тоже не наплевать. Представь игру, которая собирается 10 минут на строго типизированом языке.
Тогда какой ппц будет, если из-за усложнений придется это делать хотябы полчаса.
+ Если залезешь в уже готовый код, всегда сможешь понять где и что (как ты уже сказал) и не гадать, что ж там лежит и откуда оно там взялось
++ Какой-нибудь странный напарник не испортит тебе все, переписав в своей части переменную с объектом Player на его ID.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
И тут порог вхождения в язык программирования отнюдь не последнее дело, стоит только сравнить трудности объяснения классов в Яваскрипте с его "Game_Object.prototype.someFunc = function(...)" и в Руби, где просто def someFunc. По моей оценке, всего человека три на форуме разобрались/знакомы с этим, а скрипты на Руби продолжают выкладываться. И если честно, я не понимаю, как это - проще объяснить разницу между 2 и 2.0, чем между строкой и числом.
Чтобы залезть в чужой код и понять, где там и что, нужно при написании придерживаться принципов чистого кода, одних типов недостаточно, а если соблюдать чистый код - то они и не требуются. В частности, по чистому коду надо писать PlayerId, если в переменной - айди.
P.S. В php, если передаёшь строку туда, где ожидается число, ничего плохого не происходит, "2"+"2" всё равно будет 4. Чтобы сделать "22", нужно использовать строковое сложение - точку. В Яваскрипте по-другому, но это не из-за типизации, а из-за принятых особенностей языка.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
То это значит, что плохо думал. Вариант 1 - выбирать тип данных с запасом. Вариант 2, "не думая о том, два байта оно занимает в памяти или три" - везде писать double (или Decimal). А вот если будет тормозить - тогда и оптимизировать. К слову, int в C# 32-битный, а не 16-битный, а количество хитпоинтов более миллиона - как-то редкость.EvilCat пишет: Если думал, что HP влезет в 32 000, а потом оказалось, что нужно поднять до 50 000
В Game Maker'е вообще все числа это real, то есть вещественные, целочисленного типа данных в принципе нет, а true и false определены как константы 1 и 0. И прекрасно работает. Конечно, на нём не сделаешь каких-то крутых вычислений, но для этого есть 1) встроенная система шейдеров, если для графики 2) подключаемые DLL и врапперы.
Хотел было возразить про автоматический рефакторинг, но вспомнил, что это было переименование переменных, а не смена их типа. И всё же, неправильный выбор типа - это ошибка на уровне проектирования. Чтобы не мучаться с типами переменных для вычислений, не обязательно переходить на другой язык - опыт с GM'ом показывает что в наш век N-ядерных N-гигагерцовых процессоров можно почти всё закатать в плавающие точки, а критичные к скорости вычисления (которых обычно нет) вынести за пределы среды, заточенной под разработку именно игры. А тут как раз были подняты вопросы написания движка, а не игры. В GM'е писать движок не нужно, нужно только наполнить его игровой логикой. Ну и поставить пару костылей там и вот там... Но где ж без этого, скажите мне?EvilCat пишет: придётся ползать по всей программе, менять какой-нибудь int на longint
Сюрприз "деление целого на целое всегда даёт целое" таким образом тоже мгновенно пропадает, так как везде будут изначально вещественные числа.
Вот на это возразить нечего - да, там нужно делать так. И это плавно подводит к мысли "так почему всё-таки не Game Maker?".EvilCat пишет: Объекты - отдельная песня. Объявил список животных: List<Animal>. Положил туда один объект - класса Dog. Достал объект, удостоверился, что он класса Dog, и попросил его залаять: if (animals[0] is Dog) animals[0].Bark(). Ошибка! Программа считает, что обращается к нему как к Animal, а не Dog, а родительский класс Animal ещё не умеет лаять. Но Animal умеет издавать звуки. Вызываем animals[0].Cry() - но даже если у Dog есть метод Cry, где сказано "гав!", вместо этого вызовется родительский метод класса Animal, скажем, "пика-пика!" - если программист забыл написать в заголовке родительского метода, что метод виртуальный, то есть зависит от объекта, а не класса обращения. А в первом случае нужно было делать (animals[0] as Dog).Bark().
Из перечисленных, половина недостатков решается подходом к вопросу с другой стороны. Но вот с классами - нет. На этот счёт был какой-то шаблон (паттерн) проектирования, собирающий разные виды (фрагменты) поведения в один класс, чтобы не создавать разные классы, и вести себя различным образом в зависимости от внутреннего состояния (как бы флагов принадлежности тех или иных поведений к текущему экземпляру), и вообще не использовать ни is, ни as, но я забыл, как он называется. (ну естественно, так как игры на C# я и не делаю, хотя когда-то пытался начинать)EvilCat пишет: Строгая типизация имеет много недостатков для игроделов-любителей.
Ну и да, вот это - золотые слова.Amphilohiy пишет: Но Амфи, скажете вы, что насчет игорь?
И тут самое главное. Играм побоку на чем они работают.
Но Амфи, вы снова перебьете меня, что насчет программистов?
Захотят приспособятся к ЯПу. Не захотят - не приспособятся.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Я согласна с Кси, лучше в такие ситуации не попадать потому, что среда просто оперирует "числами" или хотя бы "целыми"/"дробными". Современные компы могут себе это позволить, не тормозя. Если не ошибаюсь, не только GML, но и Руби с Яваскриптом думают так же.
На этот счёт был какой-то шаблон (паттерн) проектирования, собирающий разные виды (фрагменты) поведения в один класс, чтобы не создавать разные классы, и вести себя различным образом в зависимости от внутреннего состояния (как бы флагов принадлежности тех или иных поведений к текущему экземпляру),
Этот паттерн называется "God Object", то есть объект, который знает слишком многое, и это антипаттерн. Фактически это выбрасывание всего ООП в окно. Подобные вещи начинают требоваться, когда в одном месте нужен стреляющий летающий враг, в другом - хилящий летающий, а втретьем - хилящий стреляющий неподвижный. Ты как бы начинаешь думать, что враг должен уметь и стрелять, и хилить, и летать, просто некоторые враги решают этого не делать. Но решать это обычно следует композицией объектов, как, например, и делает Юнити, и даже в GM это можно сделать (собственно, в таком проекте мне и захотелось полифорфизма в GM).
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
- strelokhalfer
-
- Не в сети
- Живу я здесь
-
- Знатный грамотей
- Сообщений: 1640
- Спасибо получено: 1080
И в случае чего достаточно изменить дефайн. Но я соглашусь, если работать с серьезным ЯП и\или движком, то об этом надо думать заранее.
Ещё, если с++ и дргие умеющие языки, то можно делать перегрузку метода, с параметром нужного типа.
Или как в руби, получать аргументы в хеш\массив. Но оба эти решения откровенно плохие.
"Стрелок, что-то ты неочень похож на свой аватар..."(с)
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
