Форум Flasher.ru

Форум Flasher.ru (http://www.flasher.ru/forum/index.php)
-   ActionScript 3.0 (http://www.flasher.ru/forum/forumdisplay.php?f=83)
-   -   Вопрос по проектированию (http://www.flasher.ru/forum/showthread.php?t=214513)

Appleman 06.09.2017 00:24

Вопрос по проектированию
 
Камрады!

Сразу признаюсь, я пока порядочный чайник в ООП в целом и в AC3 в частности, но тема меня искренне прёт, поэтому честно пытаюсь грызть гранит. Последние полгода потихоньку писал диздок простенькой игрушки, параллельно штудируя Колина Мука со товарищи. Сейчас почувствовал, что абстрактно продумывать и читать надоело, хочется хоть немножко реализовать. Начал проектировать первый же пользовательский класс и сразу застрял. Need help.

Имеем класс Character, экземплярами которого станут игровые персонажи. В его составе планировался ряд свойств для ключевых игровых показателей: условно "сила", "ловкость", "красота" и т.п. Изначально не видел в этой части никаких проблем - придумывай имена свойств, да добавляй set и get методы. Потом, почесав репу, тормознул. Мне подумалось, что каждое из этих свойств - само представляет некую систему, т.к. для каждого предусмотрено игровое название и текст всплывающей подсказки, которые нужно где-то хранить, словесное описание величины, которое будет разным для различных свойств ("слабак", "силач" для силы, "дурак", "гений" для ума и т.д.). По всему просится создание отдельного класса, потрохами чую. А вот как реализовать - не понимаю.

То ли сделать отдельный класс и связать его с классом Character отношением композиции. Но в этом случае выходит, что каждый экземпляр класса Character должен иметь несколько различных экземпляров нового класса (для тех же "силы", "ловкости", ума" и т.п.) и работать с ними независимо. Не могу понять, возможно ли это реализовать в рамках ООП и правильно ли это делать?

Или сделать некий единый класс для игровой характеристики, а все реально создаваемые унаследовать от него? Тогда получается чёткое присваивание каждой переменной в экземпляре класса персонажа конкретного подкласса (т.е. "сила" = new Strength, "ум"= new Wisdom и т.п.), никакой путаницы. Но как-то громоздко выглядит.

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

Надеюсь, понятно объяснил. Буду признателен за комментарии. Уверен, что описанный мною вопрос - "общее место" для опытных ООП программистов. Спасибо.

Wolsh 06.09.2017 10:52

Цитата:

Мне подумалось, что каждое из этих свойств - само представляет некую систему, т.к. для каждого предусмотрено игровое название и текст всплывающей подсказки, которые нужно где-то хранить, словесное описание величины, которое будет разным для различных свойств ("слабак", "силач" для силы, "дурак", "гений" для ума и т.д.). По всему просится создание отдельного класса, потрохами чую. А вот как реализовать - не понимаю.
Выглядит как класс-список строковых констант. Сразу бросается в глаза, что эти данные — справочные, то есть они не участвуют в рассчетах, в математике (скорости движений, нанесенного ущерба, сопротивления атаке и т.п.). В контексте персонажа его свойства — это числа, величины. Мало того, эти текстовые описания ОДИНАКОВЫ для всех, а величины свойств — индивидуальны для каждого. Налицо необходимость абстрагировать все эти descriptions в отдельный класс-хранитель описаний (не обязательно константами — если есть желание в будущем сделать возможность выбора языка интерфейса, то может быть тексты будут подгружаться извне в виде XML). Этот же класс может реализовывать несколько статических методов для получения описаний, получающих идентификатор свойства и, если надо, его величину, и возвращающих строку описания.
Ну, типа Descriptions.getHint(propID:String, propValue:int):String
Нет никакого смысла хранить в экземпляре персонажа библиотеку со словарями. Это ВООБЩЕ к персонажу не относится, это относится, если хотите, к интерфейсу, к показу на экране описаний того, что происходит. Этим не персонаж занимается, это не его ответственность.

Appleman 06.09.2017 14:39

Цитата:

Сообщение от Wolsh (Сообщение 1201758)
Это ВООБЩЕ к персонажу не относится, это относится, если хотите, к интерфейсу, к показу на экране описаний того, что происходит. Этим не персонаж занимается, это не его ответственность.

Огромное спасибо! Очень доходчиво, логику уловил. Ох и тяжко же укладывается в голову философия ООП...

Цитата:

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

Цитата:

Налицо необходимость абстрагировать все эти descriptions в отдельный класс-хранитель описаний (не обязательно константами — если есть желание в будущем сделать возможность выбора языка интерфейса, то может быть тексты будут подгружаться извне в виде XML)
Да, всё понятно. Наверное, пока константами, а потом уже можно будет и мультиязычность добавить. Здесь такой встречный вопрос. Что, по твоему экспертному мнению, правильнее хранить в подобном классе-хранителе описаний: только инфо по однотипным объектам (в нашем примере игровым характеристикам персонажа) или вообще все описания, встречающиеся в игре? Какова общепринятая практика: собирать все текста приложения в некий общий класс, продумав систему идентификаторов, и по ним выдёргивать, или хранить описания в разных классах? Сейчас подумалось, что если у персонажа есть имя и фамилия (сейчас это свойства firstName и lastName в классе Character), то следуя приведённой логике, это тоже лишняя инфо, место которой в неком отдельном классе, откуда мы будем доставать её исключительно для вывода пользователю в нужный момент. Верно рассуждаю?

Цитата:

Этот же класс может реализовывать несколько статических методов для получения описаний, получающих идентификатор свойства и, если надо, его величину, и возвращающих строку описания.
Ну, типа Descriptions.getHint(propID:String, propValue:int):String
Я правильно понимаю, что свой propID должен назначаться каждой игровой характеристике в классе Character как статическое свойство? Тогда получается, что в метод getHint класса Descriptions мы передаём propID нужной характеристики из класса Character и текущее значение данной характеристики из экземпляра класса Character (propValue)? По ним лезем в двумерный массив с текстовыми описаниями (типа первый индекс - наименование свойства, второй - нужная расшифровка для полученного значения) и вытаскиваем оттуда нужный текст, верно?

Здесь у меня тоже возникает уточняющий вопрос. Насколько я понимаю, все идентификаторы правильнее задавать константами, дабы не путаться. Продолжая наш пример, сделаем propID для силы константу const CHARACTER_STRENGTH_ID = 1, и т.д., чтобы потом не запоминать, где ноль, а где 25. Вопрос, учитывая систему областей видимости, в каком месте программы должны объявляться подобные константы? Ведь они будут использоваться уже как минимум в 2х отдельных классах: Character и Description.

Nooob 06.09.2017 22:34

Еще рекомендую почитать "ActionScript 3.0. Шаблоны проектирования (Сандерс Уильям, Кумаранатунг Чандима)" или
"Приёмы объектно-ориентированного проектирования. Паттерны проектирования (Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес)" старая правда, но неплохо так вставляет.

Я предлагаю для начала архитектуру разделить на 3 основных компонента.
Data - библиотека классов статической константной информации, к которым можно обратиться по ссылке, не изменная и общедоступная, в целом это то что Wolsh назвал Description
Model - классы логики игры, все сущности, весь жизненный цикл, вся логика всех сущностей
View - отображение текущего состояния Model, является наблюдателем
В основном для всех сущностей есть классы во всех этих трех компонентах.
Затем нужно игру разбить на сущности, скорее всего есть сцена/бой/симуляция которая знает все обо всем, и есть сущности которые содержат в себе уникальную логику, например это Character
Итого получается следующая иерархия:
-data
--BattleData содержится информация например сколько должно быть персонажей, время боя и тд.
--CharacterData содержится информация о первоначальном здоровье, скорости, силе и тд.
-model
--BattleModel здесь игровой цикл, старт боя, создание остальных сущностей, обновление, завершение, знает о BattleData, создает CharacterModel
--CharacterModel здесь логика перемещения, атаки, расчет урона, взаимодействия с остальными сущностями, знает о BattleModel и CharacterData
-view
--BattleView отображение боя, изометрия/3d/вид сверху, знает о BattleModel, создает CharacterView
--CharacterView отображение персонажа, его анимация, эффекты, знает о BattleView и CharacterModel

Далее с расширением функциональности, к примеру появляется сущность сундук добавляешь СhestData, ChestModel, ChestView. затем понимаешь что у модели сундука и персонажа есть одинаковые поля например position, делаешь родительский класс например EntityModel и наследуешь сущности от него (не рекомендую делать иерархию наследования больше 2-3).
Для того что-бы понять как правильно: делаешь все в лоб, затем улучшаешь уменьшая количество полей/методов, затем раскидываешь область ответственности персонаж знает только о персонаже, сундук знает только о себе, бой знает обо всех сущностях и скрываешь все в них что не должны знать другие.

Ты сначала правильно сделал, но зачем-то начал усложнять, не усложняй, разделил на Data/Model/View достаточно, все дополнительные финтиплюшки накручивай по необходимости, нужно отображать health над персонажем тогда и сделал get health у CharacterModel, не раньше, будет чище и сам во всем разберешься. что-бы строить чистую-правильную архитектуру надо прогнозировать, что-бы прогнозировать нужен опыт, нету опыта скрывай все поля/методы пока не понадобятся, наработаешь опыт объективного предсказания будешь наперед делать все как надо.
Если хочешь сопровождай этот тестовый проект на github, я (может кто-то еще) могу оставлять комментарии, рекомендации на рефакторинг

С Descriptions.getHint идея конечно хорошая, функциональный подход все дела, но я бы не рекомендовал так хранить константные данные, так как все превратиться в одномерную таблицу, лучше сделать нормальную иерархию классов-сущностей и её использовать, туда потом также можно накрутить десериализацию из json/xml/yaml, логику/стратегию чтения каких-то данных, приятнее будет с этим работать и очевиднее

Можешь еще почитать про Entity-component-system, но с ней сложнее работать в угоду концепции компонентов в качестве множественного наследования CharacterModel extends IMovable, IAttackable

Wolsh 06.09.2017 23:15

Цитата:

Продолжая наш пример, сделаем propID для силы константу const CHARACTER_STRENGTH_ID = 1
Сделать константы нормально, но только "1" это бессмысленная вещь. Подумай о том, как ты будешь это использовать, в какой ситуации? По этой константе ты сможешь вытянуть описание из класса Descriptions, но сможешь ли вытянуть значение из Персонажа? Нет)) Для этого надо знать название свойства. Так что логичнее было бы хранить в такой константе название свойства, например строку "strength".
Да, во многих играх используются идентификаторы-числа, типа 0xFD12A8. Для всего подряд, для классов предметов: оружия, одежды; для свойств персонажа; для квестов и их уровней (эпизодов), для локаций и т.п. Но в этом случае как раз 0xFD12A8 является ключом, а строка — значением, а не наоборот, как у тебя)) Это довольно сложная система, она оправдывает себя когда в игре тысячи понятий, но прямого отношения к ООП не имеет, просто один из инструментов наведения порядка в огромном хаосе. Суть ООП в правильном абстрагировании, распределении уникальности и общности.
Цитата:

По ним лезем в двумерный массив с текстовыми описаниями (типа первый индекс - наименование свойства, второй - нужная расшифровка для полученного значения) и вытаскиваем оттуда нужный текст, верно?
Вообще, я не зря предложил оформить это как метод — то есть предполагается, что результат не является однозначным, как константа. Допустим, тебе надо вывести описание уровня Интеллекта для данного персонажа, тогда ты напишешь
Код AS3:

_hintTXT.text = Descriptions.getHint(Hash.CHARACTER_INTELLIGENCE, _character[Hash.CHARACTER_INTELLIGENCE]);

Метод getHint() должен будет ВЫБРАТЬ описание из нескольких вариантов (массива), подходящее для данного значения свойства, и вернуть его. Скажем, если _character.intelligence больше 60, то метод вернет строку "умный был детина", если < 30 — "вовсе был дурак", иначе — "и так и сяк".
Цитата:

если у персонажа есть имя и фамилия (сейчас это свойства firstName и lastName в классе Character), то следуя приведённой логике, это тоже лишняя инфо, место которой в неком отдельном классе, откуда мы будем доставать её исключительно для вывода пользователю в нужный момент. Верно рассуждаю?
Да, если имя и фамилия это игровая константа, как Гордон Фримен в Half Life. Но, например в Oblivion или Skyrim игрок сам придумывает имя своему протагонисту. (В Обливион игрок мог даже придумывать собственные названия результатам крафтинга — зачарованным им доспехам и оружию, сваренным им эликсирам и ядам, и даже заклинаниям, которые составил сам). Так что на уровне кода это переменные, которые хранятся только в сейвах игрока, не в коде игры)

Appleman 07.09.2017 01:13

Цитата:

Сообщение от Nooob (Сообщение 1201774)
Еще рекомендую почитать "ActionScript 3.0. Шаблоны проектирования (Сандерс Уильям, Кумаранатунг Чандима)" или
"Приёмы объектно-ориентированного проектирования. Паттерны проектирования (Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес)" старая правда, но неплохо так вставляет.

За книжки спасибо, уже качнул, почитаю в спокойном режиме. Но всё остальное, если честно, больше запутало, чем внесло ясности.

Цитата:

Data - библиотека классов статической константной информации, к которым можно обратиться по ссылке, не изменная и общедоступная, в целом это то что Wolsh назвал Description
Как понимать "библиотека классов"? Это фигура речи или есть какая-то специальная реализация подобных вещей? В любом случае, это получается несколько классов со статичной информацией, типа справочников, из которых определённым образом выдёргиваются нужные элементы. Я правильно понимаю?

Цитата:

Итого получается следующая иерархия:
-data
--BattleData содержится информация например сколько должно быть персонажей, время боя и тд.
--CharacterData содержится информация о первоначальном здоровье, скорости, силе и тд.
Извини, если вопрос покажется идиотским, но я во второй раз о терминах. "Иерархия" - это условное понятие для проектировщика или ты имеешь в виду иерархию типа наследования классов?

Цитата:

Сообщение от Wolsh (Сообщение 1201774)
Сделать константы нормально, но только "1" это бессмысленная вещь. Подумай о том, как ты будешь это использовать, в какой ситуации? По этой константе ты сможешь вытянуть описание из класса Descriptions, но сможешь ли вытянуть значение из Персонажа? Нет)) Для этого надо знать название свойства. Так что логичнее было бы хранить в такой константе название свойства, например строку "strength".

Это я понял. Естественно, отдельный класс с соответствующим методом для выбора по идентификатору и значению. У меня логика следующая. Если думать о будущей поддержке нескольких языков, то нужен некий идентификатор не только для описаний тех или иных значений свойства персонажа: "умный", "дурак", "так и сяк", но и для самого названия игровой характеристики ("Ум" в русской версии, "Intelligence" в английской). Поэтому я рассудил, что для каждой игровой характеристики должен быть уникальный идентификатор, по которому можно будет вытащить как её название для интерфейса, так и описание к возможным значениям. Получается, что в классе Description метод getHint будет вызываться с передачей в него ID характеристики и её значении для конкретного персонажа (featureID:uint, value:number). И он из первого массива featureNames с названиями самих характеристик по featureID=1 со второй позиции вытащит "ум", чтобы вывести пользователю, а затем обратится в массив с описаниями, где по этому же featureID=1 выдаст нам ещё один массив с пороговыми значениями и описаниями (10, "полный кретин", 20, "дурачок", 30, "имбецил"), из которого мы по value подберём нужный вариант.

Прочитав написанное тобой, возникло два очень важных вопроса:
1. Категорически не понял, как можно по строковому идентификатору вытащить значение из персонажа? Получается, что если в классе персонажа прописано свойство public var strength, то в другом месте программы мы можем создать строковую переменную var feature:String = "strength" и, написав, 'имя экземпляра'.feature, мы сможем обратиться к экземпляру класса? А как же использование Set и Get методов, инкапсуляция и всё такое?
2. Вторая идея, почему я предпочёл использовать в качестве идентификаторов целые положительные числа uint, заключается в том, что я не могу представить себе иного способа организованно хранить данные и обращаться к ним, кроме как с помощью массивов. А индекс массива - это всегда число типа uint. Даже в твоём примере ты обращаешься к массиву, т.е. должен в какой-то момент перейти от строки к числу. Или нет?

Цитата:

Да, если имя и фамилия это игровая константа, как Гордон Фримен в Half Life. Но, например в Oblivion или Skyrim игрок сам придумывает имя своему протагонисту.
А в чём принципиальная разница? Опять же, если задавать для персонажа только идентификатор, а имена хранить в отдельном классе, то значения в этот класс могут попасть как из заранее заготовленного XML на нужном языке, так и в результате ввода пользователем. Разве одно мешает другому? Этот же идентификатор может использоваться и для подтягивания графики (условно портрета в интерфейс), и всего остального. Главное - обеспечить, чтобы во всех классах, по которым разбросаны данные, связанные с персонажем, соблюдалось единство этих самых идентификаторов. Поэтому я и спрашивал, где объявлять соответствующие константы или переменные.

Wolsh 07.09.2017 01:59

Цитата:

1. Категорически не понял, как можно по строковому идентификатору вытащить значение из персонажа?
Я даже использовал синтаксис в своем примере: _character[Hash.CHARACTER_INTELLIGENCE]
то есть это равнозначно обращению _character.intelligence, если константа CHARACTER_INTELLIGENCE в классе Hash имеет значение "intelligence". И да, это может быть геттер или сеттер.

Цитата:

2. Вторая идея, почему я предпочёл использовать в качестве идентификаторов целые положительные числа uint, заключается в том, что я не могу представить себе иного способа организованно хранить данные и обращаться к ним, кроме как с помощью массивов. А индекс массива - это всегда число типа uint.
Кроме массивов, есть еще как более низкоуровневые хэши, например Object, так и более сложные, например Dictionary.
То есть, ты предлагаешь ВООБЩЕ ВСЁ хранить в одном бесконечном массиве, а константы класса, вместо того чтобы просто содержать строку "strength", будут содержать индекс массива, по которому из него можно вытащить этот "strength"? Это уместно, когда "strength" может оказаться не "strength", например при смене языка мы заменяем весь массив текстов, а идентификаторы фраз остаются в коде как были. Но если ты начнешь на каждую фразу создавать константу, хранящую ее uint-идентификатор, то это будет адский перебор. Потому что этой константой ты будешь пользоваться ровно в одном месте, где прекрасно обошелся бы самим uint, если надо — с комментарием что это за фрукт)).

Цитата:

Даже в твоём примере ты обращаешься к массиву, т.е. должен в какой-то момент перейти от строки к числу. Или нет?
К "числу" переходит метод, и переходит он, анализируя величину свойства персонажа. Из первого параметра метод находит массив, относящийся во-первых к понятию "хинт", а во-вторых к параметру "интеллект". Далее из величины второго параметра метод определяет индекс, по которому берет из массива строку и возвращает ее. Как находит массив по строковому идентификатору? Да может быть просто объект _hints (или статическая константа HINTS:Object) содержащая массивы вариантов по строковым ключам. То есть HINTS["intelligence"] это массив ["дурак", "ни так ни сяк", "умный"].

Nooob 07.09.2017 03:09

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

минималистичный пример:
Код AS3:

package
{
        import flash.display.Sprite;
        import flash.display.StageAlign;
        import flash.display.StageScaleMode;
        import flash.events.Event;
        import flash.geom.Point;
 
        import game.data.BattleData;
        import game.data.BattleDataSpawnPoint;
        import game.data.CharacterData;
        import game.model.BattleModel;
        import game.view.BattleView;
 
        [SWF(frameRate="60", width="800", height="600")]
        public class Game extends Sprite
        {
                private const _model:BattleModel = new BattleModel();
                private const _view:BattleView = new BattleView();
 
                public function Game()
                {
                        //init data
                        var battle_1:BattleData = BattleData.find("battle_1");
                        battle_1.time = 60 * 30;
 
                        battle_1.spawnPoints = new Vector.<BattleDataSpawnPoint>(4, true);
                        battle_1.spawnPoints[0] = new BattleDataSpawnPoint(new Point(-100, -100), CharacterData.find("character_1"));
                        battle_1.spawnPoints[1] = new BattleDataSpawnPoint(new Point(100, 150), CharacterData.find("character_2"));
                        battle_1.spawnPoints[2] = new BattleDataSpawnPoint(new Point(-100, 100), CharacterData.find("character_3"));
                        battle_1.spawnPoints[3] = new BattleDataSpawnPoint(new Point(180, -100), CharacterData.find("character_4"));
 
                        var character_1:CharacterData = CharacterData.find("character_1");
                        character_1.damage = 3;
                        character_1.health = 10000;
                        character_1.speed = 0.4;
                        character_1.aggroRadius = 260;
                        character_1.attackRadius = 20;
                        character_1.color = 0xff0000;
 
                        var character_2:CharacterData = CharacterData.find("character_2");
                        character_2.damage = 2;
                        character_2.health = 70;
                        character_2.speed = 0.3;
                        character_2.aggroRadius = 160;
                        character_2.attackRadius = 25;
                        character_2.color = 0x00ff00;
 
                        var character_3:CharacterData = CharacterData.find("character_3");
                        character_3.damage = 3;
                        character_3.health = 700;
                        character_3.speed = 0.1;
                        character_3.aggroRadius = 130;
                        character_3.attackRadius = 40;
                        character_3.color = 0x0000ff;
 
                        var character_4:CharacterData = CharacterData.find("character_4");
                        character_4.damage = 4;
                        character_4.health = 300;
                        character_4.speed = 0.1;
                        character_4.aggroRadius = 100;
                        character_4.attackRadius = 35;
                        character_4.color = 0xff00ff;
 
                        //start battle
                        _model.start(BattleData.find("battle_1"));
 
                        //init view
                        _view.setup(_model);
                        addChild(_view);
 
                        stage.align = StageAlign.TOP_LEFT;
                        stage.scaleMode = StageScaleMode.NO_SCALE;
                        stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
                }
 
                private function onEnterFrame(event:Event):void
                {
                        _model.update();
 
                        _view.x = stage.stageWidth / 2;
                        _view.y = stage.stageHeight / 2;
                        _view.update();
                }
        }
}
 
package game.data
{
        import flash.utils.Dictionary;
 
        public class BattleData
        {
                private static const map:Dictionary = new Dictionary();
                public static function find(key:String):BattleData
                {
                        return map[key] ||= new BattleData(key);
                }
 
                public var key:String;
                public var spawnPoints:Vector.<BattleDataSpawnPoint>;
                public var time:int;
 
                public function BattleData(key:String)
                {
                        this.key = key;
                }
        }
}
 
package game.data
{
        import flash.geom.Point;
 
        public class BattleDataSpawnPoint
        {
                public var point:Point;
                public var character:CharacterData;
 
                public function BattleDataSpawnPoint(point:Point, character:CharacterData)
                {
                        this.point = point;
                        this.character = character;
                }
        }
}
 
package game.data
{
        import flash.utils.Dictionary;
 
        public class CharacterData
        {
                private static const map:Dictionary = new Dictionary();
                public static function find(key:String):CharacterData
                {
                        return map[key] ||= new CharacterData(key);
                }
 
                public var key:String;
                public var health:uint;
                public var speed:Number;
                public var damage:Number;
                public var aggroRadius:Number;
                public var attackRadius:Number;
                public var color:uint;
 
                public function CharacterData(key:String)
                {
                        this.key = key;
                }
        }
}
 
package game.model
{
        import game.data.BattleData;
        import game.data.BattleDataSpawnPoint;
 
        public class BattleModel
        {
                private var _data:BattleData;
                public const characters:Vector.<CharacterModel> = new Vector.<CharacterModel>();
 
                public function BattleModel()
                {
                }
 
                public function start(data:BattleData):void
                {
                        _data = data;
                        for each (var point:BattleDataSpawnPoint in data.spawnPoints)
                        {
                                characters.push(new CharacterModel(this, point.character, point.point));
                        }
                }
 
                public function finish():void
                {
                        characters.length = 0;
                        _data = null;
                }
 
                public function update():void
                {
                        for each (var character:CharacterModel in characters)
                        {
                                character.update();
                        }
                }
 
                public function findEnemy(character:CharacterModel):CharacterModel
                {
                        var distance:Number = Number.MAX_VALUE;
                        var target:CharacterModel = null;
                        for each (var testCharacter:CharacterModel in characters)
                        {
                                if(testCharacter == character || testCharacter.health == 0) continue;
 
                                var deltaX:Number = character.x - testCharacter.x;
                                var deltaY:Number = character.y - testCharacter.y;
                                var testDistance:Number = deltaX * deltaX + deltaY * deltaY;
                                if(testDistance < distance)
                                {
                                        distance = testDistance;
                                        target = testCharacter;
                                }
                        }
                        return target;
                }
        }
}
 
package game.model
{
        import flash.geom.Point;
 
        import game.data.CharacterData;
 
        public class CharacterModel
        {
                private var _battle:BattleModel;
                private var _data:CharacterData;
                private var _health:uint;
                private var _speed:Number;
                private var _damage:uint;
                private var _x:Number;
                private var _y:Number;
                private var _rotation:Number;
                public function get data():CharacterData
                {
                        return _data;
                }
                public function get health():uint
                {
                        return _health;
                }
                public function get x():Number
                {
                        return _x;
                }
                public function get y():Number
                {
                        return _y;
                }
                public function get rotation():Number
                {
                        return _rotation;
                }
 
                public function CharacterModel(battle:BattleModel, data:CharacterData, position:Point)
                {
                        _battle = battle;
                        _data = data;
                        _health = data.health;
                        _speed = data.speed;
                        _damage = data.damage;
                        _x = position.x;
                        _y = position.y;
                        _rotation = 0;
                }
 
                public function update():void
                {
                        if(_health == 0) return;
                        var enemy:CharacterModel = _battle.findEnemy(this);
                        if(enemy == null) return;
 
                        var deltaX:Number = enemy.x - x;
                        var deltaY:Number = enemy.y - y;
                        var distance:Number = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
                        if(distance < _data.aggroRadius)
                        {
                                _rotation = Math.atan2(deltaY, deltaX);
                                if(distance < _data.attackRadius)
                                {
                                        enemy.hit(_damage);
                                }
                                else
                                {
                                        deltaX /= distance;
                                        deltaY /= distance;
 
                                        _x += deltaX;
                                        _y += deltaY;
                                }
                        }
                }
 
                public function hit(damage:uint):void
                {
                        if(_health <= damage)
                        {
                                _health = 0;
                        }
                        else
                        {
                                _health -= damage;
                        }
                }
        }
}
 
package game.view
{
        import flash.display.Sprite;
 
        import game.model.BattleModel;
        import game.model.CharacterModel;
 
        public class BattleView extends Sprite
        {
                private var _model:BattleModel;
                private const _characters:Vector.<CharacterView> = new Vector.<CharacterView>();
 
                public function BattleView()
                {
                }
 
                public function setup(model:BattleModel):void
                {
                        _model = model;
                        for each (var characterModel:CharacterModel in model.characters)
                        {
                                var characterView:CharacterView = new CharacterView(characterModel);
                                _characters.push(characterView);
                                addChild(characterView);
                        }
                }
 
                public function update():void
                {
                        for each (var characterView:CharacterView in _characters)
                        {
                                characterView.update();
                        }
                }
        }
}
 
package game.view
{
        import flash.display.Sprite;
 
        import game.model.CharacterModel;
 
        public class CharacterView extends Sprite
        {
                private var _model:CharacterModel;
 
                public function CharacterView(model:CharacterModel)
                {
                        _model = model;
                }
 
                public function update():void
                {
                        if(_model.health == 0)
                        {
                                visible = false;
                                return;
                        }
 
                        graphics.clear();
                        graphics.beginFill(_model.data.color, _model.health / _model.data.health);
                        graphics.drawCircle(0, 0, 8);
                        graphics.endFill();
                        graphics.lineStyle(1, _model.data.color, _model.health / _model.data.health);
                        graphics.drawCircle(0, 0, _model.data.aggroRadius);
                        graphics.lineStyle(2, _model.data.color, _model.health / _model.data.health);
                        graphics.drawCircle(0, 0, _model.data.attackRadius);
                        graphics.moveTo(0, 0);
                        graphics.lineTo(_model.data.attackRadius, 0);
 
                        x = _model.x;
                        y = _model.y;
                        rotation = _model.rotation * 180 / Math.PI;
                }
        }
}


Appleman 07.09.2017 13:00

Цитата:

Сообщение от Nooob (Сообщение 1201781)
Под иерархией проекта я говорю про содержимое проекта, то какие классы в нем содержатся и какие пакеты есть. минималистичный пример:

Фига себе минималистичный! Надеюсь, это ты не за чашкой кофе за 10 минут ночью налабал? :)

Получается, что в пакете data - вся справочная информация, в model - вся игровая механика, расчёты и т.п., а view - пакет для визуализации всего этого хозяйства. Понятно. Вопрос, в чём преимущество создания отдельных пакетов и постоянного импорта двух других в каждый, по сравнению с созданием единого пакета для всей игры, и настройки взаимодействия на уровне отношений исключительно между классами?

И ещё ряд вопросов по коду:

Код AS3:

public class Game extends Sprite
        {
                private const _model:BattleModel = new BattleModel();
                private const _view:BattleView = new BattleView();

Я так понимаю, что _model и _view = это экземпляры классов BattleModel и BattleView для быстрого обращения к математике и графике. Почему в виде констант?

Код AS3:

public class BattleData
        {
                private static const map:Dictionary = new Dictionary();
                public static function find(key:String):BattleData
                {
                        return map[key] ||= new BattleData(key);
                }

Получается, что у тебя тут некое хранилище данных, организованное с помощью класса Dictionary, и весь функционал класса - это вытащить и вернуть нужное значение по полученному key. Опять не понимаю, почему map записана в форме константы. И выражение return map[key] ||= new BattleData(key); взрывает мой мозг :wacko: До логического OR более-менее понимаю (это судя по всему как раз обращение к экземпляру map класса Dictionary по ключу key), а вот в чём соль правой части - совсем не понятно.

Последний мини-вопрос. Обратил внимание, что у тебя и у Wolsh многие переменные в коде записаны с символа "_", но не все. Какая тут логика? Я такое в некоторых книгах встречал (не у Мука).

undefined 07.09.2017 13:30

Цитата:

Почему в виде констант?
Это называется константный указатель. Т.е.view и model всегда гарантировано будут ссылаться на экземпляр указанный при инициализации. А если где-то появится еще одна запись
Код AS3:

_model= new BattleModel();

Компилятор выдаст ошибку. Защита от дурака.

Nooob 07.09.2017 14:36

Цитата:

Сообщение от Appleman (Сообщение 1201786)
Вопрос, в чём преимущество создания отдельных пакетов и постоянного импорта двух других в каждый, по сравнению с созданием единого пакета для всей игры, и настройки взаимодействия на уровне отношений исключительно между классами?

разделив все по пакетам ты тем самым выделяешь отдельные блоки взаимоотношений и классы с полями/методами internal в общем пакете могут работать с ними как с публичными, классы в разных пакетах эти поля видеть не будут, например в model методы findEnemy/hit можно смело пометить как internal

Цитата:

Сообщение от Appleman (Сообщение 1201786)
Код AS3:

public class BattleData
        {
                private static const map:Dictionary = new Dictionary();
                public static function find(key:String):BattleData
                {
                        return map[key] ||= new BattleData(key);
                }

И выражение return map[key] ||= new BattleData(key);

это короткая запись
Код AS3:

var data:BattleData = map[key];
if(data == null)
{
  data = new BattleData(key);
  map[key] = data;
}
return data;
//или
return map[key] || map[key] = new BattleData(key);

оператор ||= обозначает что если выражение слева ложное false/null/0, то приравнять к нему выражение справа, ну и любая операция приравнивания так же возвращает это значение, поэтому его можно передать в return

Цитата:

Сообщение от Appleman (Сообщение 1201786)
Последний мини-вопрос. Обратил внимание, что у тебя и у Wolsh многие переменные в коде записаны с символа "_", но не все. Какая тут логика? Я такое в некоторых книгах встречал (не у Мука).

переменными "_" принято обозначать приватные поля класса которые затем могут быть открыты для чтения/записи через get/set, просто удобство в случае пересечения имен публичных и приватных полей класса

Appleman 07.09.2017 15:36

Цитата:

Сообщение от Wolsh (Сообщение 1201779)
Я даже использовал синтаксис в своем примере: _character[Hash.CHARACTER_INTELLIGENCE]
то есть это равнозначно обращению _character.intelligence, если константа CHARACTER_INTELLIGENCE в классе Hash имеет значение "intelligence". И да, это может быть геттер или сеттер.

Круто! Вот тут нужно из скайпа ставить трёх бабушек с маракасами. Я и забыл про такую форму записи :rtfm: И что касается геттера, то он получается должен быть записан именно в виде жёсткого get, а варианты типа метода с произвольным названием типа GetStrength(), не канает.

Цитата:

То есть, ты предлагаешь ВООБЩЕ ВСЁ хранить в одном бесконечном массиве, а константы класса, вместо того чтобы просто содержать строку "strength", будут содержать индекс массива, по которому из него можно вытащить этот "strength"? Это уместно, когда "strength" может оказаться не "strength", например при смене языка мы заменяем весь массив текстов, а идентификаторы фраз остаются в коде как были. Но если ты начнешь на каждую фразу создавать константу, хранящую ее uint-идентификатор, то это будет адский перебор.
Не совсем так. Я предполагал группу классов-хранителей, внутри которых будут храниться однотипные данные. Если, например, имеем 10 характеристик персонажа, для каждой из которых предусмотрен набор расшифровок, то будет система из 10 индетификаторов-констант, по которым будем вылавливать. Если у персонажа умения имеют похожую структуру (числовое значение и текстовая расшифровка), то и их тужа же, чтобы не дублировать код. Ещё допустим штук 15, итого 25 идентификаторов - вроде вполне терпимо.

Другой класс - для имён, где хранятся имена, фамилии и прозвища. Там идентификаторов служит ID персонажа. Фактически тоже пачка массивов, забитых именами (на нужном языке).

И да, я почитав твои предыдущие советы задумался о том, что "strength" может быть и "силой", а значит и само наименование свойства нужно не задавать жёстко, а брать откуда-то по некоему идентификатору. Хотя, насколько я понимаю, использование строковых идентификаторов такому подходу тоже не противоречит.

Цитата:

Как находит массив по строковому идентификатору? Да может быть просто объект _hints (или статическая константа HINTS:Object) содержащая массивы вариантов по строковым ключам. То есть HINTS["intelligence"] это массив ["дурак", "ни так ни сяк", "умный"].
Можно поподробнее? Получается, что для каждого свойства в классе с хинтами создаётся свой отдельный массив, присвоенный переменной, имя которой совпадает со строковым идентификатором? Учитывая, что у меня планируется игрушка не RTS и совсем не военная, а скорее социальное RPG, выбор всякой инфо по строковым ключам скорее всего будет центральной технологией :)

Wolsh 07.09.2017 18:04

Я вот вообще не хотел опускаться в разговоре до конкретного кода, просто потому что всегда есть 100500 способов решить задачу, и какой из них оптимальный, зависит от множества факторов. А когда начинаешь приводить код, выглядит так что ты настаиваешь на конкретно таком вот решении. А я вообще никак не настаиваю, просто показываю возможный вариант, ОК?
Код AS3:

package 
{
        public class Character
        {
                private var _strength:int = 50;
                private var _intelligence:int = 50;
                private var _agility:int = 50;
 
                public function Character()
                {
 
                }
 
                public function get strength():int
                {
                        return _strength;
                }
 
                public function get intelligence():int
                {
                        return _intelligence;
                }
 
                public function get agility():int
                {
                        return _agility;
                }
        }
 
}
 
////--------------------------
 
package
{
        public class Hash
        {
                static public const AGILITY:String = "agility";
                static public const STRENGTH:String = "strength";
                static public const INTELLIGENCE:String = "intelligence";
 
                static private const HINTS:Object =
                {
                        strength:                ["слабак", "атлет", "силач"],
                        intelligence:        ["дурак", "умник", "ботан"],
                        agility:                ["тюфяк", "ловкач", "гимнаст"]
                }
 
                static public function getHint(propName:String, propValue:int) : String
                {
                        if (propValue < 30) return HINTS[propName][0];
                        if (propValue > 70) return HINTS[propName][2];
                        return HINTS[propName][1];
                }
        }
}
 
////----------------------
 
package
{
        import flash.display.Sprite;
 
        public class Main extends Sprite
        {
                private var _character:Character;
 
                public function Main()
                {
                        _character = new Character();
                        trace(Hash.getHint(Hash.STRENGTH, _character[Hash.STRENGTH]));
                        trace(Hash.getHint(Hash.INTELLIGENCE, _character[Hash.INTELLIGENCE]));
                        trace(Hash.getHint(Hash.AGILITY, _character[Hash.AGILITY]));
                }
        }
 
}


Appleman 07.09.2017 20:18

Уважаемые Nooob и Wolsh!

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

Цитата:

Я вот вообще не хотел опускаться в разговоре до конкретного кода, просто потому что всегда есть 100500 способов решить задачу, и какой из них оптимальный, зависит от множества факторов. А когда начинаешь приводить код, выглядит так что ты настаиваешь на конкретно таком вот решении. А я вообще никак не настаиваю, просто показываю возможный вариант, ОК?
Да, конечно. Мне пока вообще любой рабочий код, связанный с моей задачей, крайне полезен на посмотреть.

Вот кстати ещё один методологический вопрос, над которым я размышляю. У всех персонажей есть одинаковые свойства, что логично. При этом у главного героя их больше всего (ибо ему условно не только руками махать, но и девок охмурять), у второстепенных NPC их поменьше, а у "проходных" - буквально 3-4 типа имени с фамилией. Вопрос, как в данной ситуации правильнее организовать: наследовать классы и потом воспользоваться преимуществами полиморфизма (ибо расчёты для центральных персонажей производятся иначе, чем для второстепенных) или создавать единый класс и просто не заполнять неиспользуемые поля значениями для второстепенных персонажей? От чего может зависеть решение?

И вопрос камраду Nooob. Я правильно понял логику, что условно все шаблоны и правила расчёта игровой механики "живут" в data, а в model производятся только непосредственные "живые" расчёты для конкретных игровых ситуаций? То есть если действие "пендель" условно наносится с расстояния полметра, а его урон зависит от массы тапка персонажа и его умения бить ногами, то всё это хозяйство прописывается в качестве методов класса game.data.pendel, чтобы потом быть импортированным в model для расчёта по актуальным значениям. Или нет?

Godwarlock 08.09.2017 00:12

Цитата:

При этом у главного героя их больше всего (ибо ему условно не только руками махать, но и девок охмурять), у второстепенных NPC их поменьше, а у "проходных" - буквально 3-4 типа имени с фамилией. Вопрос, как в данной ситуации правильнее организовать: наследовать классы
Все твои данные - это библиотека. Поэтому кроме метода get у определенной Data, для того, чтобы получить статы, указанные в параметрах функции тебе ничего не надо.

Nooob 09.09.2017 16:26

Цитата:

Сообщение от Appleman (Сообщение 1201798)
Я правильно понял логику, что условно все шаблоны и правила расчёта игровой механики "живут" в data, а в model производятся только непосредственные "живые" расчёты для конкретных игровых ситуаций? То есть если действие "пендель" условно наносится с расстояния полметра, а его урон зависит от массы тапка персонажа и его умения бить ногами, то всё это хозяйство прописывается в качестве методов класса game.data.pendel, чтобы потом быть импортированным в model для расчёта по актуальным значениям. Или нет?

Если расчеты основываются на константных данных, то они живут в data. к примеру есть статические препятствия и нужен метод который определяет пересекает ли луч эти препятствия, этот метод пишется в ObstaclesData, для того что-бы можно было применить эту математику в любом независимо от модели месте, во view, на кнопке атаки или в прицеле, или рисовать ли препятствие на сцене. или другой пример есть некий персонаж у него есть дерево умений, каждое умение можно изучить выполнив некий список условий (предыдущее умение изучено, достаточно энергии, есть нужный предмет в инвентаре), этот метод я бы тоже располагал в data, так как на этот момент еще нету модели этого конкретного умения (оно еще не изучено), а его состояние нужно рисовать в интерфейсе. альтернативный пример есть кнопка атаки другого персонажа, которая становится активной когда в радиусе есть враги, этот метод проверки доступности атаки я бы расположил в model.
твой пример с пенделем можно располагать в data, но если в игре появляются бафы которые влияют на массу тапка и его умения и его урон, то это перестает быть константными данными, и по большей части все данные уже берутся из модели, и логичнее всего такой метод переложить в model

Tails 09.09.2017 17:13

Nooob,
Можно узнать ваш способ организаций ответа на действия пользователя? (Controller) Как именно вы реализуете этот процесс. На любом простом примере, например, кнопка изучить скилл.

Appleman 11.09.2017 19:24

Цитата:

Сообщение от Wolsh (Сообщение 1201791)
Я вот вообще не хотел опускаться в разговоре до конкретного кода, просто потому что всегда есть 100500 способов решить задачу, и какой из них оптимальный, зависит от множества факторов. А когда начинаешь приводить код, выглядит так что ты настаиваешь на конкретно таком вот решении. А я вообще никак не настаиваю, просто показываю возможный вариант, ОК?
Код AS3:

        package 
{
        public class Hash
        {
                static public const AGILITY:String = "agility";
                static public const STRENGTH:String = "strength";
                static public const INTELLIGENCE:String = "intelligence";
 
                static private const HINTS:Object =
                {
                        strength:                ["слабак", "атлет", "силач"],
                        intelligence:        ["дурак", "умник", "ботан"],
                        agility:                ["тюфяк", "ловкач", "гимнаст"]
                }
}


Уважаемый Wolsh!

Потихоньку разбирался и пробовал, в итоге из всех 100500 разнообразных решений для хранения нужных мне строковых данных выбрал всё-таки предложенный тобой код, что не удивительно :) Сделал в целом похожую конструкцию, она работает.

Возникло 2 вопроса. Во-первых, каково преимущество статических переменных и методов перед обычными для подобных классов-"справочников"? В своём примере коллега Nooob, например, сразу после объявления класса создавал в нём константу с экземпляром класса-"справочника" и потом обращался непосредственно к ней.

Во-вторых, я попробовал в классе Object с массивами расшифровок заменить прямое указание строковых идентификаторов ("agility", "strength") на константы, содержащие те же значения (попытка записи типа [GlobalValues.FEATURES_STRENGTH], имеющей значение "strength"). Ничего не вышло, выдаёт ошибку. Есть способ непрямого указания идентификаторов в теле Object?

ZackMercury 11.09.2017 19:30

Цитата:

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

Appleman 11.09.2017 21:04

Спасибо, понял. А по второму вопросу есть какие-нибудь идеи?

ZackMercury 11.09.2017 21:20

Код AS3:

const s:String = "hello";
const obj:Object =
{
        (s as String):"world"
}
 
trace(obj.hello); // world


Appleman 11.09.2017 22:01

ZackMercury, обалдеть, кратко и эффективно. Всё заработало, спасибо!
Пошёл восхищаться и дальше учить мат. часть.

Добавлено через 3 часа 29 минут
Ещё раз поклон камраду ZackMercury, реализовал его совет в классе-хранителе игровых имён с выдачей оных в нужном падеже. Там же решено было складировать фамилии, наименования локаций и все прочие имена существительные, которые придётся по ходу игры склонять.

Сначала написал несколько однотипных методов-получателей элементов созданных объектов по идентификатору и номеру падежа. Примерно так:

Код AS3:

public class NamePool 
        {
 
                static private var firstName:Object = // Имена всех персонажей во всех падежах
                {
                        (GlobalValues.CHARACTER_HERO as String):        ["Джек", "Джека", "Джеку", "Джека", "Джеком", "Джеке"],
                        (GlobalValues.CHARACTER_APRIL as String):        ["Маша", "Машу", "Маше", "Машу", "Машей", "Маше"]
                }
 
                public function NamePool():void {}
 
                static public function getfirstName(characterID:String, languageCase:uint):String //Получить имя по ID персонажа
                {return firstName[characterID][languageCase];
}

Всё работает прекрасно. Но потом подумалось, что зачем писать идентичный код для каждого массива (имена, фамилии, клички, локации и много чего ещё), если само наименование переменной (в нашем случае firstName:Object), содержащей нужную инфо, можно точно также получать в качестве строкового идентификатора, чтобы потом к ней обратиться по полученному значению. Тогда сигнатура метода должна выглядеть вот так:

Код AS3:

static public function getWordCase(object:String, identificator:String, languageCase:uint):String

А вот как синтаксически сделать корректное обращение, мне не допереть. Просьба помочь. Спасибо.

ZackMercury 12.09.2017 17:46

Вообще, как правило, подобное не хранят в коде. Эта инфа выгружается в какой-то декларативный язык, вроде JSON/XML. Но как синтаксически возможный вариант:
Код AS3:

static public function getWordCase(object:String, identificator:String, languageCase:uint):String
{
        return NamePool[object][identificator][languageCase];
}

P.S. В ASC2 скобки при объявлении ключа ассоциативного массива не работают.

Appleman 12.09.2017 18:41

Спасибо, будем пробовать.

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

ZackMercury 12.09.2017 18:59

Руками не формируют, в AS3 есть специальный класс для этого.
http://help.adobe.com/ru_RU/ActionSc...0204-7ff5.html

Wolsh 13.09.2017 01:14

Цитата:

Но потом подумалось, что зачем писать идентичный код для каждого массива (имена, фамилии, клички, локации и много чего ещё)
А я бы не советовал настолько обесчеловечивать код. И да, я бы не хранил "локации" даже в одном классе с "именами фамилиями кличками"..
Еще "поиграться":
Код AS3:

package 
{
        public class Quest
        {
                private static const EPISODES_TEXT:Array =
                [
                /* 000 */        "Если бы кто-то сказал Волку, что Красная Шапочка окажется дочкой *$n1 *$N1, он вряд-ли бы решился на преступление. Перейти дорогу *$n2 совсем не входило в его планы.",
                /* 001 */        "Волк уже однажды встречался с *$N4, и эта встреча до сих пор отзывалась в его памяти болью унижения.",
                /* 002 */        "Новой встречи с *$n4 Волк боялся как огня."
                ]
 
                public static function getText(episode:int, character:String) : String
                {
                        var text:String = EPISODES_TEXT[episode];
                        for (var i:uint = 0; i < 6; i++)
                        {
                                text = text.replace("*$n" + i, Names.getName(character, Names.FIRST_NAME, i));
                                text = text.replace("*$N" + i, Names.getName(character, Names.FAMILY_NAME, i));
                        }
                        return text;
                }
        }
}
////*********************************************
package
{
        public class GlobalValues
        {
                static public const CHARACTER_HERO:String = "hero";
                static public const CHARACTER_APRIL:String = "april";
        }
}
////*********************************************
package
{
        public class Names
        {
                static public const FIRST_NAME:String = "firstName";
                static public const FAMILY_NAME:String = "familyName";
 
                static private const firstName:Object = // Имена всех персонажей во всех падежах
                {
                        (GlobalValues.CHARACTER_HERO as String):        ["Джек", "Джека", "Джеку", "Джека", "Джеком", "Джеке"],
                        (GlobalValues.CHARACTER_APRIL as String):        ["Маша", "Маши", "Маше", "Машу", "Машей", "Маше"]
                }
 
                static private const familyName:Object = // Фамилии всех персонажей во всех падежах
                {
                        (GlobalValues.CHARACTER_HERO as String):        ["Николсон", "Николсона", "Николсону", "Николсона", "Николсоном", "Николсоне"],
                        (GlobalValues.CHARACTER_APRIL as String):        ["Шарапова", "Шараповой", "Шараповой", "Шарапову", "Шараповой", "Шараповой"]
                }
 
                static public function getName(character:String, nameType:String, nameCase:int) :String
                {
                        return Names[nameType][character][nameCase];
                }
        }
}
////*********************************************
package
{
        import flash.display.Sprite;
 
        public class Main extends Sprite
        {
                public function Main()
                {
                        trace(Quest.getText(0, GlobalValues.CHARACTER_HERO));
                        trace(Quest.getText(1, GlobalValues.CHARACTER_HERO));
                        trace(Quest.getText(2, GlobalValues.CHARACTER_HERO));
                        trace("\n");
                        trace(Quest.getText(0, GlobalValues.CHARACTER_APRIL));
                        trace(Quest.getText(1, GlobalValues.CHARACTER_APRIL));
                        trace(Quest.getText(2, GlobalValues.CHARACTER_APRIL));
                }
        }
}


Appleman 13.09.2017 19:07

Wolsh, ну ты просто добрый волшебник! :drinks: Я как раз начал задумываться о том, как можно технологично предусмотреть подстановку строковых переменных в заранее созданные куски текста. Спасибо.

Скажи, а добавлять в текст заготовки под будущую подстановку значений в виде *$n1 *$N1 - это чисто твой креатив или подобный формат *$ - результат требований AC3 или какого-либо соглашения, принятого среди программистов?

Код AS3:

public static function getText(episode:int, character:String) : String
                {
                        var text:String = EPISODES_TEXT[episode];
                        for (var i:uint = 0; i < 6; i++)
                        {
                                text = text.replace("*$n" + i, Names.getName(character, Names.FIRST_NAME, i));
                                text = text.replace("*$N" + i, Names.getName(character, Names.FAMILY_NAME, i));
                        }
                        return text;
                }

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

Цитата:

А я бы не советовал настолько обесчеловечивать код. И да, я бы не хранил "локации" даже в одном классе с "именами фамилиями кличками".
Не совсем понял, что ты подразумеваешь под "обесчеловечиванием" кода и почему ты не стал бы хранить "локации" и "имена" в одном классе. Я рассуждал так. Если у нас есть определённое количество имён существительных в единственном числе (имена, наименования свойств персонажей и вещей, названия локаций и многое другое), которые должны выдаваться в нужном падеже в самых разных ситуациях, то почему бы их не забить все в единый класс? Ведь механизм хранения и методы выдачи - идентичные. Дальше только идентификаторы выставляй. А введя осмысленные константы на уровне всей программы (статические переменные класса GlobalValues), я гарантированно не запутаюсь. Ибо задав единожды CHARACTER_HERO:String = "hero", я использую эту константу при создании персонажа в качестве идентификатора, поставлю её же в массив, хранящий имена, буду по нему обращаться в любом месте программы...

Wolsh 13.09.2017 20:22

Цитата:

это чисто твой креатив или подобный формат *$ - результат требований AC3
Да в принципе любое сочетание можешь использовать, только чтобы оно гарантировано не могло появиться в тексте по надобности. Я как то пролетел с #, набил текстов а потом оказалось что надо текст форматировать с HTML, задавая кое-где цвет шрифта)) К тому же *$ хорошо бросается в глаза и находится в тексте.
Цитата:

А они тупым перебором в цикле стоят - на выходе будет хрень.
Ну, так то я компилировал, хрени не было)) Тут не "тупой перебор", данный цикл заменяет ВСЕ алиасы от *$n0 до *$n5 и от *$N0 до *$N5 в тексте на имена в соответствующих падежах.
Цитата:

Не совсем понял, что ты подразумеваешь под "обесчеловечиванием" кода
Не люблю, когда названия методов (и свойств конечно) отражают больше "технический" контекст происходящего, нежели "исторический", смысловой. То есть getWordInCase() хорошо, когда возвращает что угодно, то есть если бы этот метод мог склонять ЛЮБОЕ слово. Но для каких-то более конкретных ситуаций мне бы хотелось более конкретных названий. Для имен я бы предпочел иметь getName(), для локаций — getLocation(), для цветов getColor() и тп. Никакого технического смысла в этом нет. Только удобочитаемость. Читать код тебе, нет смысла скрывать человеческую логику за абстрактными названиями. Иначе ты докатишься до "идеального" кода типа get(where:*, what:*, how:*, for:*, from:*, why:*):*

Appleman 13.09.2017 21:45

Цитата:

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

С этим понятно. С падежами тоже разобрался. Весьма элегантно получилось. Но у тебя всё равно напутано, ибо именительный - это 0, а у тебя в тексте стоит *$n1, я потому и обратил внимание.

На счёт своего вопрос я имел в виду другую твою реплику, которую не процитировал:
Цитата:

И да, я бы не хранил "локации" даже в одном классе с "именами фамилиями кличками".
Почему ты так считаешь? Свою аргументацию, почему я решил собрать все существительные в едином классе, я привёл.

Wolsh 13.09.2017 23:18

Цитата:

Но у тебя всё равно напутано, ибо именительный - это 0, а у тебя в тексте стоит *$n1
Потому что в моем тексте нет именительного. "окажется дочкой Маши Шараповой" это Родительный.

Цитата:

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

Appleman 14.09.2017 11:46

Цитата:

Сообщение от Wolsh (Сообщение 1201911)
Потому что в моем тексте нет именительного. "окажется дочкой Маши Шараповой" это Родительный.

И всё равно есть ошибка, т.к. i перебирается плоть до значения 6, а значит, включая 0 для именительного падежа, один должен быть лишний, т.к. падежей всего шесть :D

Я с позволения задам ещё пару вопросов по проектированию. К сожалению, пока никакого чутья на это дело нет, и перед реализацией каждой даже самой проходной задачи приходится долго тупить на предмет того, в какой класс разместить код или вообще создавать новый класс и т.п. Рекомендации уважаемых Nooob и Wolsh - ощутимо помогают (хоть отчасти противоречат друг другу), то пока тоже не смогли успокоить мою душу.

Уже кратко спрашивал, теперь постараюсь подробнее сформулировать суть моего вопроса и альтернатив. Продумывая игровых персонажей и их механики, вывел 3 группы: npc, ключевые npc и сам игрок. Количество свойств увеличивается от npc до игрока. Когда дошла очередь до проектирования классов, я нарисовал табличку, где в столбцы поставил эти группы, а в строки - свойства. А дальше ставил на пересечении галочки, используется ли данное свойство или нет. По итогу получился такой расклад: совсем небольшое количество базовых свойств попали во все три группы (имя, ID, пол и т.п.), некоторое количество оказалось уникальным для персонажа игрока, ещё небольшая группа оказалась общей для npc и ключевых npc, наконец, пачка уникальных свойств только для ключевых npc. Итого 4 "профиля вхождения". Они определили вот такую структуру наследования классов:

Код AS3:

Charactrer - базовые свойства, общие для всех персонажей
          |  |
          |    - Character_hero - главный герой
          |
          - Character_npc - общие свойства для неписей и ключевых неписей
                      |
                      - Character_npc_key - уникальные свойства для ключевых неписей

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

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

Что остаётся? Первое, оставить как есть сейчас, для расчётов игровой механики писать классы с абстрактными методами для каждого из подклассов персонажей, т.е. воспользоваться прелестями полиморфизма (или зря я столько книжек прочитал!). Но смущает, что во-первых, дерево наследование кривое получается (вопрос, повлияет ли этот факт на работу абстрактных методов и вообще, насколько подобный перекос допустим), плюс много писанины - по 3 подкласса на каждый класс с расчётами. При условии, что ни в близком, ни в отдалённом времени я не планирую вводить другие группы персонажей (для которых было бы как раз удобно добавить отдельных потомков), всё это выглядит перебором и необоснованным усложнением.

Вторая альтернатива - забить на наследование, собрать все свойства в один класс Character, а для того, чтобы различать, кто есть ху, добавить в класс соответствующее свойство-указатель, типа "player", "npc" или "key_npc" и присваивать его при создании каждого нового персонажа. Избавляемся от всего головняка с наследованием, но обрекаем себя на добавление конструкции switch или подобной во все методы, различающиеся для групп персонажей.

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

Буду признателен за развёрнутый ответ.

Wolsh 14.09.2017 18:26

Цитата:

i перебирается плоть до значения 6
Это еще с какого перепугу? Учите язык.

Appleman 14.09.2017 21:23

Цитата:

Сообщение от Wolsh (Сообщение 1201925)
Это еще с какого перепугу? Учите язык.

Ещё раз посмотрел. Не прав. Сам дурак. :)

Appleman 23.09.2017 01:43

Друзья!

Я продолжил свои изыскания в проектировании классов будущей игры и возник новый вопрос. Появился он при переходе от базовых свойств персонажа к специфическим: которые выделяются для пола, профессии и т.п. Пусть условно для мужчин учитывается длина бороды, а для женщин - объём талии. У меня получилось следующее. Я создал новый класс Gender с единственным свойством genderID, которое может принимать значения 0 для женского пола и 1 для мужского, и унаследовал от него два класса GenderMale и GenderFemale. В первом соответственно свойство beardLength, во втором - waistVolume, плюс set- и get-методы для каждого.

Тогда в классе персонажа Character добавился такой set-метод:
Код AS3:

public function set gender(newGenderID:uint):void //Пол
                {
                switch (newGenderID)
                        {
                                case 1:
                                        _gender = new GenderMale(newGenderID);
                                        break;
                                case 0:
                                        _gender = new GenderFemale(newGenderID);
                                        break;
                                default:
                                        throw new Error ("Пол персонажа может иметь значение 0-female или 1-male");
                        }
                }

Экземпляр класса Character теперь имеет экземпляр класса-потомка Gender в приватной переменной _gender.
Вопрос такой. Чтобы задать либо получить значения свойства, связанного с полом, приходится писать такой код. Например, для нашей бороды:

В классе Character:
Код AS3:

public function set beardLength(bewBeardLength:Number):void {_gender.beardLength = bewBeardLength; }

Теперь в Gender:
Код AS3:

public function set beardLength(bewBeardLength:Number):void {}

Наконец, в GenderMale:
Код AS3:

override public function set beardLength(bewBeardLength:Number):void {_beardLength = bewBeardLength}

Аналогично с get-методом. В принципе работает, но как-то чересчур громоздко выглядит. И этим активно не нравится. Если на каждое подобное свойство писать методы в трёх местах, то это получится мясо и фарш, а не код. Обращаться напрямую character._gender.beardLength тоже не выходит, т.к. переменная _gender объявлена как private и менять её статус не вижу никакого резона. в общем, need help. Заранее спасибо.

Wolsh 23.09.2017 12:18

1. Если у тебя в Gender объявлена function set beardLength() то она будет и у Женщин, так?
2. Зачем в конструкторы Мужского и Женского пола передается еще и идентификатор? Ты же не собираешься делать экземпляр Мужчины со значением "женщина"? Да вроде и switch не позволит. Тогда зачем?
3. Конвенции просят не называть параметр сеттера как Бог черепаху, но всегда просто "value". Почему так? Потому что название сеттера уже несет необходимый смысл, а если нет то это плохое название сеттера. Он принимает значение для свойства с этим названием — ведь он хоть и функция, но прикидывается свойством. Поэтому как-то еще глубокомысленно называть параметр, который станет этим самым свойством с этим самым названием как-то очень странно. Так же принято называть внутреннее хранилище (приватную переменную, к которой доступ через геттер/сеттер) точно так же, как геттер/сеттер (только с подчеркиванием), потому что она собственно и есть это самое СВОЙСТВО. У тебя, в частности, грубейшее нарушение этого принципа: есть сеттер gender типа uint, а свойство _gender имеет тип Gender. Таким образом у твоего характера снаружи "гендер" это просто число 1 или 0, а внутри это целый экземпляр Мужика с бородой и талией.

Теперь о смысловой нагрузке.
Я думаю ты и сам видишь, что вместо абстрагирования получилась какая-то каша. До тех пор, пока пол Характера не имеет значения, все прекрасно, ты можешь обращаться с ним как просто с Человеком. Но как только встает вопрос пола, начинается катавасия — тебе приходится протаскивать в класс Характер ВСЕ половые признаки, и мужские и женские, и совершенно непонятно зачем вообще был нужен класс Gender, да еще и с подклассами Джентельмен и Мадмуазель, если ВСЕ их свойства вынужден представлять класс Характер, и даже как транспортный дата-обжект (для обмена данными) эти гендеры использовать нельзя, ибо доступ к ним как объектам наглухо закрыт.
Лично мне ошибкой видится именно эта паническая инкапсуляция Гендера. Надо открыть к нему доступ напрямую, а не прокидывать все сеттеры/геттеры в Характер. В чем смысл, если доступ к этим свойствам ВСЕ-РАВНО есть, только с бубном вокруг деревни. Скрывать надо то, что другим НЕ НУЖНО знать. В чем смысл сокрытия гендера, мне непонятно.
Сама идея собирать какие-то свойства определенного назначения в отдельные блоки нормальная. Это используется в нативных классах, например такие свойства визуальных объектов как .filters или .transform, или .defaultTextFormat у TextField.

Appleman 23.09.2017 16:43

Wolsh, спасибо за очередную порцию ликбеза. Отвечаю по пунктам:

Цитата:

1. Если у тебя в Gender объявлена function set beardLength() то она будет и у Женщин, так?
Типа того, я опять перепутал тёплое с мягким:) Оставляем бороду только для мужиков, а талию - только для женщин. Вопрос снят.

Цитата:

2. Зачем в конструкторы Мужского и Женского пола передается еще и идентификатор? Ты же не собираешься делать экземпляр Мужчины со значением "женщина"? Да вроде и switch не позволит. Тогда зачем?
Разумеется, не собираюсь. Я исходил из того, что должен быть идентификатор. Если посмотреть эту тему выше, то например, у нас есть класс-хранитель расшифровок к значениям свойств. Мне кажется, очень удобно и логично добавлять к идентификатору свойства идентификатор пола, чтобы получать:

Код AS3:

static private const HINTS:Object =
{
("Intelligence" + GlobalValues.GENDER_MALE as String):        ["умный", "дурак"],
("Intelligence" + GlobalValues.GENDER_FEMALE as String):        ["умная", "дура"]
}

Сделал без передачи в конструктор. Объявил переменную _genderID как protected и в подклассы добавил назначение идентификатора прямо в метод-конструктор:

Код AS3:

public function GenderMale():void 
                {
                        _genderID = GlobalValues.GENDER_MALE;
 
                }

Цитата:

3. Конвенции просят не называть параметр сеттера как Бог черепаху, но всегда просто "value". Почему так? Потому что название сеттера уже несет необходимый смысл, а если нет то это плохое название сеттера. Он принимает значение для свойства с этим названием — ведь он хоть и функция, но прикидывается свойством. Поэтому как-то еще глубокомысленно называть параметр, который станет этим самым свойством с этим самым названием как-то очень странно. Так же принято называть внутреннее хранилище (приватную переменную, к которой доступ через геттер/сеттер) точно так же, как геттер/сеттер (только с подчеркиванием), потому что она собственно и есть это самое СВОЙСТВО. У тебя, в частности, грубейшее нарушение этого принципа: есть сеттер gender типа uint, а свойство _gender имеет тип Gender. Таким образом у твоего характера снаружи "гендер" это просто число 1 или 0, а внутри это целый экземпляр Мужика с бородой и талией.
Всё, сообразил. Получается, что метод для присвоения пола лучше обозвать как-то иначе, раз он не подходит под понятие сеттера. А геттер в таком случае должен выдавать непосредственно хранящийся в переменной _gender экземпляр одного из подклассов Gender, верно? То есть правильный код для геттера:

Код AS3:

                public function get gender():Gender
                {
                        return _gender;
 
                }

Хотя в контексте того, что ты написал далее, само наличие сеттеров и геттеров для гендера в классе Character вызывает вопрос.

Цитата:

Лично мне ошибкой видится именно эта паническая инкапсуляция Гендера. Надо открыть к нему доступ напрямую, а не прокидывать все сеттеры/геттеры в Характер.
Спасибо, разобрался. Переименовал переменную _gender в gender и объявил её как public в классе Character, чтобы напрямую обращаться к свойствам как character.gender.'имя свойства'. Ты ведь это подразумевал?

Теперь появилась проблема. Если раньше было легко по идентификатору вытащить значения свойства, написав character[propID], где строковая переменная propID содержит название соответствующей переменной в классе Character, например "intelligence", то теперь, указав в propID = "gender.beardLength", выдаёт ошибку :( Можно как-то подтянуть?

Wolsh 23.09.2017 21:38

Цитата:

объявил её как public
Тут есть одна проблема, аж двухэтажная. Этаж первый: даже если ты полностью заменишь экземпляр Гендер на другой, Характер об этом не узнает. Он просто будет хранить на борту этот новый гендер. Для того и делаются сеттеры, чтобы что-то предпринять в случае замены значения свойства, например активировать перерисовку бабы в мужика. Второй этаж точно такой же, но еще тоньше) Характер никогда не узнает о изменении какого-то из свойств своего гендера. Это общая проблема всех свойств со сложным типом: всегда приходится "вручную" делать обновление, если требуется моментальное применение новых свойств сложного свойства ;) То есть, если объект, хранящий сложное свойство, должен как-то среагировать на изменение одного из параметров этого свойства. Недостаточно написать _textField.defaultTextFormat.bold = true; ведь TextField никак не узнает об этом изменении свойства bold. Применение свойств объекта TextFormat в текстФилде происходит в сеттере set defaultTextFormat(value:TextFormat), то есть когда текстФилду назначается новый текстФормат, его свойства "разбираются" и применяются. А изменение какого-то свойства "на ходу" никак не отрабатывается текстФилдом — он об этом даже не узнает. Приходится делать не очень красивый финт типа
Код AS3:

_textField.defaultTextFormat.bold = true;
_textField.defaultTextFormat = _textField.defaultTextFormat;

Как более продвинутый вариант можно устроить отправку События CHANGE из такого объекта (гендера, например) и в обработчике выполнить заново сеттер set gender(value:Gender) с разбором и применением всех свойств объекта гендер.

По поводу character[propID] — ну чтож, теперь надо считаться с тем что это свойства не Характера а Гендера, то есть character.gender[propID].

Appleman 24.09.2017 01:17

Цитата:

Сообщение от Wolsh (Сообщение 1202052)
Тут есть одна проблема, аж двухэтажная. Этаж первый: даже если ты полностью заменишь экземпляр Гендер на другой, Характер об этом не узнает.

Идею понял, но в деталях пока разбираться не стал. Полагаю, рано или поздно у меня произойдёт встреча с этой проблемой, тогда плотнее подумаю над этим. На данном этапе операций по перемене пола персонажа не планируется :)

Цитата:

Приходится делать не очень красивый финт типа
Код AS3:

_textField.defaultTextFormat.bold = true;
_textField.defaultTextFormat = _textField.defaultTextFormat;


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

Цитата:

По поводу character[propID] — ну чтож, теперь надо считаться с тем что это свойства не Характера а Гендера, то есть character.gender[propID].
Получается, написать в строковом идентификаторе "gender.'имя свойства'" не прокатит. Тогда мне видится два возможных варианта. Либо создавать отдельный класс для подбора и вывода словесных описаний для гендерных свойств, по факту полный дубликат уже имеющегося класса, где вместо обращения character[propID] будет прописано character.gender.[propID]. Либо всё-таки "протаскивать" гендерные свойства через класс character, то есть например, для нашей бороды писать в классе Character:

Код AS3:

public function get beardLength():Number
{
return(gender.beardLength);
}

Что будет предпочтительнее? Вернее, чем будет определяться выбор того или иного варианта?

Wolsh 24.09.2017 21:55

Цитата:

Не совсем понял результат выполнения данного кода.
В первой строке меняется значение свойства bold объекта TextFormat, который хранится в данном экземпляре TextField. При этом сам TextField ничего об изменении не знает, ведь его собственным свойствам никакие новые значения не присваивались и его методы не вызывались.
Во второй строке вызывается сеттер самого TextField: "_textField.defaultTextFormat = " и ему отдается объект TextFormat, полученный из геттера _textField.defaultTextFormat. То есть тот самый объект, в котором значение свойства .bold теперь true. Cеттер это функция, она не просто присваивает этот ТекстФормат внутреннему хранилищу (он итак там лежит). Сеттер делает что-нибудь еще: считывает свойства переданного ему объекта ТекстФормат и "применяет" их "прямо сейчас".
(может пример с ТекстФорматом и не совсем корректен ввиду того что там не требуется это "прямо сейчас", но вот если ты захочешь рычажком слайдера менять длину бороды в редакторе персонажа, то просто менять одно из свойств объекта Гендер будет недостаточно, чтобы видеть изменение в реальном времени).

Цитата:

Что будет предпочтительнее?
Знать, что в этом месте программы тебе нужны настройки внешности персонажа и они лежат в объекте класса Гендер. Не надо чрезмерно увлекаться "String-типизацией" в ущерб strong-типизации. Не лишай себя удовольствия получать сообщения об ошибках на этапе компиляции, а не где-то в глубине игрового процесса.

Appleman 24.09.2017 22:55

Цитата:

Знать, что в этом месте программы тебе нужны настройки внешности персонажа и они лежат в объекте класса Гендер. Не надо чрезмерно увлекаться "String-типизацией" в ущерб strong-типизации. Не лишай себя удовольствия получать сообщения об ошибках на этапе компиляции, а не где-то в глубине игрового процесса.
Получается, всё-таки рекомендуешь более прямолинейно писать методы по отдельности для character и gender. В принципе, ничего экстраординарного тут нет, т.к. заранее всегда известно, какие свойства где живут. Просто не хотелось дублировать код.

Мне тут ещё один вариант придумался. Может быть в классе AttributeHints первым делом выяснять, в каком классе "живёт" нужное нам свойство, и в зависимости от результата формировать обращение либо в character, либо в character.gender. Единственное, что смог найти, это метод hasOwnProperty(), но он работает только для классов типа Object. Есть ли что-то подобное для использования в своих классах?


Часовой пояс GMT +4, время: 17:54.

Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2026, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.