|
|
|||||
Регистрация: Dec 2014
Адрес: Санкт-Петербург
Сообщений: 479
|
Вопрос по проектированию
Камрады!
Сразу признаюсь, я пока порядочный чайник в ООП в целом и в AC3 в частности, но тема меня искренне прёт, поэтому честно пытаюсь грызть гранит. Последние полгода потихоньку писал диздок простенькой игрушки, параллельно штудируя Колина Мука со товарищи. Сейчас почувствовал, что абстрактно продумывать и читать надоело, хочется хоть немножко реализовать. Начал проектировать первый же пользовательский класс и сразу застрял. Need help. Имеем класс Character, экземплярами которого станут игровые персонажи. В его составе планировался ряд свойств для ключевых игровых показателей: условно "сила", "ловкость", "красота" и т.п. Изначально не видел в этой части никаких проблем - придумывай имена свойств, да добавляй set и get методы. Потом, почесав репу, тормознул. Мне подумалось, что каждое из этих свойств - само представляет некую систему, т.к. для каждого предусмотрено игровое название и текст всплывающей подсказки, которые нужно где-то хранить, словесное описание величины, которое будет разным для различных свойств ("слабак", "силач" для силы, "дурак", "гений" для ума и т.д.). По всему просится создание отдельного класса, потрохами чую. А вот как реализовать - не понимаю. То ли сделать отдельный класс и связать его с классом Character отношением композиции. Но в этом случае выходит, что каждый экземпляр класса Character должен иметь несколько различных экземпляров нового класса (для тех же "силы", "ловкости", ума" и т.п.) и работать с ними независимо. Не могу понять, возможно ли это реализовать в рамках ООП и правильно ли это делать? Или сделать некий единый класс для игровой характеристики, а все реально создаваемые унаследовать от него? Тогда получается чёткое присваивание каждой переменной в экземпляре класса персонажа конкретного подкласса (т.е. "сила" = new Strength, "ум"= new Wisdom и т.п.), никакой путаницы. Но как-то громоздко выглядит. Наконец, может быть я зря вообще пытаюсь городить огород и усложнять без необходимости? Возможно, нужно сделать, как изначально и планировалось - ограничиться свойствами класса Character, а всё "мясо", связанное с игровыми характеристиками, сделать-таки отдельным классом, но не пихать в класс персонажа, а связывать их посредством каких-то других способов (т.е. если нужно вывести инфо о силе персонажа, то отдельно выдёргиваем значение соответствующего свойства экземпляра класса персонажа и отдельно всю "воду", соответствующую этой характеристике из экземпляра класса игровых характеристик)? Надеюсь, понятно объяснил. Буду признателен за комментарии. Уверен, что описанный мною вопрос - "общее место" для опытных ООП программистов. Спасибо. |
|
|||||
Нуб нубам
модератор форума
Регистрация: Jan 2006
Адрес: Бердск, НСО
Сообщений: 6,445
|
Цитата:
Ну, типа Descriptions.getHint(propID:String, propValue:int):String Нет никакого смысла хранить в экземпляре персонажа библиотеку со словарями. Это ВООБЩЕ к персонажу не относится, это относится, если хотите, к интерфейсу, к показу на экране описаний того, что происходит. Этим не персонаж занимается, это не его ответственность.
__________________
Reality.getBounds(this); |
|
|||||
Регистрация: Dec 2014
Адрес: Санкт-Петербург
Сообщений: 479
|
Цитата:
Цитата:
Цитата:
Цитата:
Здесь у меня тоже возникает уточняющий вопрос. Насколько я понимаю, все идентификаторы правильнее задавать константами, дабы не путаться. Продолжая наш пример, сделаем propID для силы константу const CHARACTER_STRENGTH_ID = 1, и т.д., чтобы потом не запоминать, где ноль, а где 25. Вопрос, учитывая систему областей видимости, в каком месте программы должны объявляться подобные константы? Ведь они будут использоваться уже как минимум в 2х отдельных классах: Character и Description. |
|
|||||
Регистрация: Mar 2007
Сообщений: 319
|
Еще рекомендую почитать "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
__________________
RocketJump Последний раз редактировалось Nooob; 06.09.2017 в 23:15. |
|
|||||
Нуб нубам
модератор форума
Регистрация: Jan 2006
Адрес: Бердск, НСО
Сообщений: 6,445
|
Цитата:
Да, во многих играх используются идентификаторы-числа, типа 0xFD12A8. Для всего подряд, для классов предметов: оружия, одежды; для свойств персонажа; для квестов и их уровней (эпизодов), для локаций и т.п. Но в этом случае как раз 0xFD12A8 является ключом, а строка — значением, а не наоборот, как у тебя)) Это довольно сложная система, она оправдывает себя когда в игре тысячи понятий, но прямого отношения к ООП не имеет, просто один из инструментов наведения порядка в огромном хаосе. Суть ООП в правильном абстрагировании, распределении уникальности и общности. Цитата:
_hintTXT.text = Descriptions.getHint(Hash.CHARACTER_INTELLIGENCE, _character[Hash.CHARACTER_INTELLIGENCE]); Цитата:
__________________
Reality.getBounds(this); |
|
||||||
Регистрация: Dec 2014
Адрес: Санкт-Петербург
Сообщений: 479
|
Цитата:
Цитата:
Цитата:
Цитата:
Прочитав написанное тобой, возникло два очень важных вопроса: 1. Категорически не понял, как можно по строковому идентификатору вытащить значение из персонажа? Получается, что если в классе персонажа прописано свойство public var strength, то в другом месте программы мы можем создать строковую переменную var feature:String = "strength" и, написав, 'имя экземпляра'.feature, мы сможем обратиться к экземпляру класса? А как же использование Set и Get методов, инкапсуляция и всё такое? 2. Вторая идея, почему я предпочёл использовать в качестве идентификаторов целые положительные числа uint, заключается в том, что я не могу представить себе иного способа организованно хранить данные и обращаться к ним, кроме как с помощью массивов. А индекс массива - это всегда число типа uint. Даже в твоём примере ты обращаешься к массиву, т.е. должен в какой-то момент перейти от строки к числу. Или нет? Цитата:
|
|
|||||
Нуб нубам
модератор форума
Регистрация: Jan 2006
Адрес: Бердск, НСО
Сообщений: 6,445
|
Цитата:
то есть это равнозначно обращению _character.intelligence, если константа CHARACTER_INTELLIGENCE в классе Hash имеет значение "intelligence". И да, это может быть геттер или сеттер. Цитата:
То есть, ты предлагаешь ВООБЩЕ ВСЁ хранить в одном бесконечном массиве, а константы класса, вместо того чтобы просто содержать строку "strength", будут содержать индекс массива, по которому из него можно вытащить этот "strength"? Это уместно, когда "strength" может оказаться не "strength", например при смене языка мы заменяем весь массив текстов, а идентификаторы фраз остаются в коде как были. Но если ты начнешь на каждую фразу создавать константу, хранящую ее uint-идентификатор, то это будет адский перебор. Потому что этой константой ты будешь пользоваться ровно в одном месте, где прекрасно обошелся бы самим uint, если надо — с комментарием что это за фрукт)). Цитата:
__________________
Reality.getBounds(this); |
|
|||||
Регистрация: Mar 2007
Сообщений: 319
|
Под иерархией проекта я говорю про содержимое проекта, то какие классы в нем содержатся и какие пакеты есть
минималистичный пример: 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; } } }
__________________
RocketJump |
|
|||||
Регистрация: Dec 2014
Адрес: Санкт-Петербург
Сообщений: 479
|
Цитата:
Получается, что в пакете data - вся справочная информация, в model - вся игровая механика, расчёты и т.п., а view - пакет для визуализации всего этого хозяйства. Понятно. Вопрос, в чём преимущество создания отдельных пакетов и постоянного импорта двух других в каждый, по сравнению с созданием единого пакета для всей игры, и настройки взаимодействия на уровне отношений исключительно между классами? И ещё ряд вопросов по коду: public class Game extends Sprite { private const _model:BattleModel = new BattleModel(); private const _view:BattleView = new BattleView(); public class BattleData { private static const map:Dictionary = new Dictionary(); public static function find(key:String):BattleData { return map[key] ||= new BattleData(key); } Последний мини-вопрос. Обратил внимание, что у тебя и у Wolsh многие переменные в коде записаны с символа "_", но не все. Какая тут логика? Я такое в некоторых книгах встречал (не у Мука). |
|
|||||
Регистрация: Oct 2006
Сообщений: 2,281
|
Цитата:
Компилятор выдаст ошибку. Защита от дурака. |
Часовой пояс GMT +4, время: 15:58. |
|
« Предыдущая тема | Следующая тема » |
|
|