Я уже некоторое время работаю над 2-й RPG и понял, что принял несколько плохих дизайнерских решений. В частности, есть несколько вещей, которые вызывают у меня проблемы, поэтому мне было интересно, какие конструкции использовали другие люди, чтобы их преодолеть или использовать.
Для небольшой предыстории я начал работать над этим в свободное время прошлым летом. Я изначально делал игру на C #, но около 3 месяцев назад решил перейти на C ++. Я хотел получить хорошее представление о C ++, так как прошло много времени с тех пор, как я активно использовал его, и подумал, что такой интересный проект будет хорошим мотиватором. Я широко использую библиотеку наддува и использую SFML для графики и FMOD для аудио.
У меня есть немало написанного кода, но я собираюсь отказаться от него и начать все сначала.
Вот основные проблемные области, которые у меня есть, и я хотел бы получить некоторые мнения о том, как другие решили или решили бы их.
1. Циклические зависимости Когда я делал игру на C #, мне не пришлось об этом беспокоиться, так как это не проблема. Переход на C ++, это стало довольно серьезной проблемой и заставило меня думать, что я, возможно, спроектировал вещи неправильно. Я не могу себе представить, как разделить мои классы, и при этом заставить их делать то, что я хочу. Вот несколько примеров цепочки зависимостей:
У меня есть статус эффекта класса. В классе есть несколько методов (Apply / Unapply, Tick и т. Д.) Для применения его эффектов к персонажу. Например,
virtual void TickCharacter(Character::BaseCharacter* character, Battles::BattleField *field, int ticks = 1);
Эти функции будут вызываться каждый раз, когда персонаж, на которого наложен эффект статуса, принимает ход. Он будет использоваться для реализации таких эффектов, как Regen, Poison и т. Д. Однако он также вводит зависимости от класса BaseCharacter и класса BattleField. Естественно, класс BaseCharacter должен отслеживать, какие эффекты состояния в настоящее время активны для них, так что это циклическая зависимость. Battlefield должен отслеживать сражающиеся стороны, а у класса участников есть список BaseCharacters, представляющий другую циклическую зависимость.
2 - События
В C # я широко использовал делегатов для привязки к событиям на персонажах, полях сражений и т. Д. (Например, был делегат для изменения состояния персонажа, изменения статистики, добавления / удаления эффекта состояния и т. Д. .) и поле битвы / графические компоненты будут подключаться к этим делегатам для усиления их эффектов. В C ++ я сделал нечто подобное. Очевидно, что нет прямого эквивалента делегатам C #, поэтому вместо этого я создал что-то вроде этого:
typedef boost::function<void(BaseCharacter*, int oldvalue, int newvalue)> StatChangeFunction;
и в моем классе персонажей
std::map<std::string, StatChangeFunction> StatChangeEventHandlers;
всякий раз, когда менялась статистика персонажа, я повторял и вызывал каждую функцию StatChangeFunction на карте. Хотя это работает, я боюсь, что это плохой подход к выполнению задач.
3 - Графика
Это большая вещь. Это не связано с графической библиотекой, которую я использую, но скорее концептуально. В C # я сочетал графику со многими своими уроками, что, как я знаю, ужасная идея. Желая сделать это развязанным на этот раз, я попробовал другой подход.
Чтобы реализовать мою графику, я представлял всю графику, связанную с игрой, как серию экранов. То есть есть экран заголовка, экран статуса персонажа, экран карты, экран инвентаря, экран битвы, экран графического интерфейса битвы, и, в принципе, я мог бы размещать эти экраны поверх друг друга по мере необходимости для создания игровой графики. Каким бы ни был активный экран, он обладает входом в игру.
Я разработал менеджер экранов, который будет выдвигать и выдвигать экраны на основе пользовательского ввода.
Например, если вы находитесь на экране карты (обработчик ввода / визуализатор для карты листов) и нажали кнопку «Пуск», это вызовет диспетчер экрана, чтобы переместить экран главного меню на экран карты и отметить карту. экран не будет нарисован / обновлен. Проигрыватель будет перемещаться по меню, которое будет выдавать дополнительные команды диспетчеру экрана, в зависимости от ситуации, чтобы вставить новые экраны в стек экрана, а затем выскочит их по мере того, как пользователь изменяет экраны / отменяет их. Наконец, когда игрок выходит из главного меню, я выскакиваю его и возвращаюсь к экрану карты, отмечаю, что его нужно рисовать / обновлять, и оттуда идти.
Боевые экраны были бы более сложными. У меня был бы экран в качестве фона, экран для визуализации каждой стороны в битве и экран для визуализации интерфейса битвы. Пользовательский интерфейс будет подключаться к символьным событиям и использовать их, чтобы определить, когда обновлять / перерисовывать компоненты пользовательского интерфейса. Наконец, каждая атака, имеющая доступный сценарий анимации, будет вызывать дополнительный слой, чтобы анимировать себя перед тем, как выскочить из экрана. В этом случае каждый слой последовательно помечается как рисуемый и обновляемый, и я получаю стек экранов, обрабатывающих мою боевую графику.
Хотя я еще не смог заставить менеджер экрана работать идеально, я думаю, что смогу со временем. У меня вопрос по этому поводу: стоит ли вообще этого делать? Если это плохой дизайн, я хочу знать сейчас, прежде чем тратить слишком много времени на создание всех необходимых мне экранов. Как вы строите графику для своей игры?
источник
Ваши циклические зависимости не должны быть проблемой, если вы будете заранее объявлять классы в заголовочных файлах и включать их в .cpp (или любые другие) файлы.
Для системы событий, два предложения:
1) Если вы хотите сохранить шаблон, который используете сейчас, подумайте о переключении на boost :: unordered_map вместо std :: map. Отображение строк в качестве ключей медленное, тем более что .NET делает некоторые полезные вещи, чтобы помочь ускорить процесс. Использование unordered_map хеширует строки, поэтому сравнение обычно происходит быстрее.
2) Подумайте о переключении на что-то более мощное, например, boost :: сигналы. Если вы сделаете это, вы можете сделать приятные вещи, например, сделать ваши игровые объекты отслеживаемыми, производными от boost :: signal :: trackable, и позволить деструктору позаботиться об очистке всего, вместо того, чтобы вручную отменить регистрацию в системе событий. Вы также можете иметь несколько сигналов , указывающих на каждый слот (или наоборот, я не помню точную номенклатуру) , так что это очень похоже делать
+=
наdelegate
в C #. Самая большая проблема с boost :: signal заключается в том, что он должен быть скомпилирован, а не просто заголовками, поэтому, в зависимости от вашей платформы, может возникнуть боль при запуске и запуске.источник