Почему я всё ещё не написал игру?

Дело не только в самом коде, но и в фантазии. Собирая все дизайн-документы, нашёл вот такой экземпляр заготовки:

Рис. 1. Вырезка из дизайн-документа. Очерк персонажа/существа
Рис. 1. Вырезка из дизайн-документа. Очерк персонажа/существа

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

Разберём это по предложениям:

1. Атакует чистой энергией.

ECW Design Document

И вместо функции:

void unit::damage( value count ){ health -= count; }

, сразу выделяется необходимость типизации урона/резистов и отдельной реализации функции вида:

void unit::damage( value count, int type ){ health -= count * resists[type]; }

А это мы ещё не собрали персональные эффекты персонажа:

value unit::get_resists( int type ){ value a; for( auto& c : effects ) if( c.is( type ) ) a += c.get_val(); return a; } void unit::damage( value count, int type ){ health -= count * ( resists[type] * get_resists(type) ); }

Ради только одной простой возможности разделить тип урона код вырос многократно. Здесь ещё не видно всех объявлений полей классов.

2. Способна собрать сгусток энергии и атаковать им площадь.

ECW Design Document

Пояснение: под "собрать" автор имел ввиду явление, протекающее в течение длительного времени, а не применяемое сразу по клику мышки.

В обычной вселенной множества игр, действие сводится к паре действий "выбор действия + выбор цели". Реализуется одним алгоритмом. При данной же хотелке необходимо разделить само действие на 3 фазы: "выбор умения / подготовка / выбор цели", которые при этом живут в разных временных отметках. Это требует добавления специального флага:

enum{ // ... UNIT_STATE_CHARGING_SKILL // специальное состояние юнита для "зарядки/подготовки" умения }

Более того, это состояние необходимо обработать в основном цикле:

void game::tick(){ // ... if( u.is( UNIT_STATE_CHARGING_SKILL ) ){ u.current_skill_charge += 1; if( u.current_skill_charge >= u.skills[current_skill] ){ u.do_skill( u.current_skill ); return; } } // ... }

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

3. Радиус и сила удара зависит от количества ходов подготовки.

ECW Design Document

Здесь всё тривиально, но нам просто нужно учитывать, что сами величины в функциях могут быть модифицированы. И заряд умения выступает в роли параметра функции:

void unit::do_skill( u.current_skill ){ // ... value strength = skills[current_skill] + current_skill_charge; // И этот на основе этого strength мы уже вычисляем значение урона в дальнейшем // ... }

4. Подготовку заклятия можно сбить нанесением урона.

ECW Design Document

Здесь нужно дополнить второй пункт. Теперь, когда юнит получает урон, с него нужно снимать этот эффект подготовки умения:

void unit::damage( value count, int type ){ state.erase( UNIT_STATE_CHARGING_SKILL ); // добавили удаление health -= count * ( resists[type] * get_resists(type) ); }

Чтобы если в персонажа прилетел урон, прервать это состояние в "мозгах игры".

5. Черная душа увеличивает мораль обратнопропорционально количеству юнитов в отряде.

ECW Design Document

Внедрение пассивок. На каждой итерации мы должны посчитать соотношение живых к изначальному количеству и сменить модификатор эффекта:

void game::tick_effects(){ // ... case PASSIVE_EFFECT_BLACK_SOUL: u.effects( PASSIVE_EFFECT_BLACK_SOUL ).set( u.owner.get_units_count() / u.owner.get_units_count_alive() ); break; // ... }

Здесь выявляется разнообразное поведение пассивок, которое тоже нужно где-то обрабатывать и писать под них код.

6. Преклонение: все разумные существа не могут атаковать магессу, а если находятся в радиусе 2 клеток - перемещаться.

ECW Design Document

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

Поэтому просто порассуждаем. Что означает "все разумные существа"? Тут вообще происходит типизация признаков существ. А вернувшись к п.1 можно вспомнить чем чревато введение типизации туда, где до этого всё считалось единобразно.

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

7. Способна пропускать через себя энергию, восстанавливая концентрацию.

ECW Design Document

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

void game::tick_effects(){ // ... for( auto c : u.autocast ){ // стоп. У нас же каждый автокаст работает отдельно при определённых условиях! } // ... }

То есть, нам надо одни автокасты применять как пассивные умения, а другие - как активные. Например, поджигать стрелы, когда юнит делает атаку...

Почему я всё ещё не написал игру?

СЛОЖНА, НИПАНЯТНА, АСТАНАВИТИСЬ!

8. При пропускании через себя энергии своего сгустка - восстанавливает здоровье свое.

ECW Design Document

А тут мы неожиданно пришли к тому, что при применении действия, описанного в п.1, мы должны сделать ровно наоборот, что опять раздувает код:

Вместо:

void unit::do_skill( u.current_skill ){ // ... get_target_unit().damage( strength * damage_count ); // ... }

Будет:

void unit::do_skill( u.current_skill ){ // ... auto t = get_target_unit(); if( t.get_id() == this->get_id() ) damage( strength * damage_count * .2f, TYPE_DAMAGE_NONE ); else t.damage( strength * damage_count, TYPE_DAMAGE_CLEAN_ENERGY ) // ... }

Как видно, новые фичи в игре требуют написания дополнительного кода. И если этот процесс становится неуправляемым, то код превращается в бесконечные проверки и ветвления, могут появиться конфликты логики, если вдруг мы где-то не в том порядке установим состояния юнитов.

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

Кстати, а тут можно подробнее узнать о концептуальной части проекта и оставить свой голос в опросе:

2
1