ECW Dev. Пост нытья

Просто обожаю переписывать с нуля:

Рис. 1. Всё сломалось
Рис. 1. Всё сломалось

...

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

А теперь весь этот код не валиден.

Поскольку всё его внутреннее взаимодействие нужно перевести на новый базовый класс. Раньше это была связка game.attributes и game.values. Теперь это всё (и многое другое) будет объединять game.entity. Стоит ли пояснять, что из-за этого структура вообще всех игровых объектов сломана?

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

switch( p.SUB() ){ case PACKET_SUB::CUR_PLAYER: // -- ID of current player // !! Mode with player control // -- unblock selecting unit // state = SELECT_UNIT break; case PACKET_SUB::CUR_UNIT: // -- ID of current unit // !! Mode with pririty selecting unit // -- unit already selecting // state = SELECT_ACTION break; case PACKET_SUB::CUR_TURN: // -- Num of current turn // -- update break; case PACKET_SUB::CUR_ROUND: // -- Num of current round // -- updata break; case PACKET_SUB::RECON_START: // -- Enable recon logic break; case PACKET_SUB::RECON_END: // -- Disable recon logic break; case PACKET_SUB::RECON_CELL: // -- Place or delete cell from placements if( p.answer.x != 0 ) // -- add m_sets[SETS_RECON].insert( game::hex::t_coord{ p.answer.y,p.answer.z } ); else // -- remove m_sets[SETS_RECON].erase( game::hex::t_coord{ p.answer.y,p.answer.z } ); break; default: APP_WARN( "Undefined key with command!" ); break; }

И всё это необходимо только по факту того, что игра в жанре пошаговой тактики/стратегии. Конечно, есть примеры хороших пошаговых одиночных игр, но у меня-то идея в комбинировании юнитов и их эффектов.

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

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

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

Рис. 3. Ещё какой-то вариант билда, просто показал
Рис. 3. Ещё какой-то вариант билда, просто показал

Ссылаясь на этот пост, можно выделить такие механизмы поведения при наличии особенности "Преклонение":

1. Если существо разумное, оно не может атаковать персонажа и не может двигаться в радиусе 2 клеток.

2. Если существо не разумное - ему пофиг.

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

Кстати, об обучаемости, имеется ещё один вариант угнетения ИИ: сокрытие данных. Конечно, ИИ может рассчитывать вероятности и так далее, но если мы используем сокрытие для некоторых действий (как ловушка), то будь расчёт хоть 100 раз правильным, это можно использовать для вынуждения оппонента совершить неправильный ход.

А ещё...

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

Велик

struct PACKET{ // ** Determine level of destination & target component enum class e_logic: uint8_t{ // [ hidden ] } ; // ** Define unique command key enum class e_command: uint8_t{ // [ hidden ] } ; // ** Locking order type enum class e_dest: uint8_t{ // [ hidden ] } ; // ** Configuration key enum class e_sub: uint8_t{ // [ hidden ] } ; union{ PACKET_TYPE type; struct{ e_logic logic; e_dest dest; e_command command; e_sub sub; } head; }; uint32_t num; // -- number of packet union{ char bytes[PACKET_BYTES_SIZE] = {}; MESSAGE message; ANSWER answer; ACTION action; STRING string; }; void clear(){ memset(this,0,sizeof(PACKET)); } static PACKET form( PACKET::e_logic logic = PACKET::e_logic::NONE, PACKET::e_command cmd = PACKET::e_command::SYS_PING, PACKET::e_dest order = PACKET::e_dest::ORDERED, PACKET::e_sub sub = PACKET::e_sub::NONE ){ static uint32_t num; PACKET p; p.clear(); p.num = ++num; p.head.dest = order; p.head.logic = logic; p.head.command = cmd; p.head.sub = sub; return p; } auto LEVEL()const noexcept{ return head.logic; } auto ORDER()const noexcept{ return head.dest; } auto KEY()const noexcept{ return head.command; } auto SUB()const noexcept{ return head.sub; } };

А это обёртка для передачи данных из сессии в интерфейс. И это тоже душная .. история:

1. Выбрать, должны ли данные отправляться клиенту.

2. Выбрать, должен ли конкретный клиент видеть эти данные.

3. Записать данные в пакет.

4. Как-нибудь там пакетики через эмуляцию сетевого интерфейса дойдут до клиента.

5. Получить пакет в правильном месте.

6. Считать данные из пакета и записать куда нужно.

7. Сменить состояние интерфейса на основе полученных данных.

В чём же нытьё?

Да потому что если игровая логика пишется прямо внутри клиента, можно уложить всё в два шага:

1. Обновили игровые данные.

2. Интерфейс когда ему нужно, где нужно, почитал полностью актуальные валидные данные.

Рис. 2. UI::Battle::Update()
Рис. 2. UI::Battle::Update()

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

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

А ещё анимации нужно додумывать в отрисовщике, а ещё смену одного отрисовщика на другой отрисовщик с поддержкой анимации.

Ещё вспомнил, что

Необходимо разделить логику игровой сессии и интерфейса так, чтобы можно было вызывать реализацию сражения без реализации истории. То есть, у нас как бы game::battle, который может существовать либо сам по себе, либо в рамках game::mission. И как бы ладно, ок, главное только, чтобы сетевое общение с интерфейсом происходило одинаковыми каналами.

А это:

struct s_value_v3{ constexpr static int invalid = INT_MIN; constexpr static int accuracy_cur = 10000; constexpr static int accuracy_fact = 1000; constexpr static size_t icur{ 0 }, imax{ 2 }, imin{ 1 }; constexpr static size_t ibase{ 0 }, ibapp{ 1 }, ifact{ 2 }, ifapp{ 3 }, imoda{ 4 }, isize{ 5 }; mutable int calced = invalid; int off = 0; // ** Relative offset int cur = accuracy_cur; // ** Relative current value int def = accuracy_cur; int min{ 0 }; int max{ 1000000 }; // ...

, - реализация, вымученная, выстраданная, которая вместе с другими штуками пойдёт в урну. Я там писал где-то в начале про game::values? Вот s_value_3 как раз оттуда. Чтобы мы могли не просто числа прибавлять-убавлять. А добавлять нелинейные эффекты, баффы, дебаффы, корректно вычитать их, не ломая базовое значение и ограничивать, чтобы нельзя было накастовать 9991154214 ед. здоровья юниту.

Кстати говоря, возможно именно из-за этой проблемы вычисления модификаторов я начал переписывать когда-то давно проект в первый раз, заменяя поля:

// Unit.h Attribute aHealth, aArmor, aAttack, aDefense, aMovement, aActions, aSize, aMoral, aDamage; // Value.h namespace Engine{ typedef float val; class Value{ private: val fMin, fMax, fDefault, fCurrent, fFact; public: Value(); Value( float fmin, float fmax, float fdefault, float fcurrent, float ffact ); Value( const Value& v ); Value( const float& v ); ~Value(); Value operator+( const Value& v ); Value operator*( const Value& v ); Value operator/( const Value& v ); Value operator-( const Value& v ); Value operator=( const Value& v ); void Set( val f ); void SetMax( val f ); void SetMin( val f ); void SetCurrent( val f ); void SetDefault( val f ); void SetFact( val f ); val Get(); val& Fact() { return fFact; }; val& Min() { return fMin; }; val& Max() { return fMax; }; val& Current() { return fCurrent; }; val& Default() { return fFact; }; val GetMin() { return fMin; }; val GetMax() { return fMax; }; val GetDefault() { return fDefault; }; val GetCurrent() { return fCurrent; }; val GetFact() { return fFact; }; }; class Attribute{ private: int iType; float fFact; Value vBasic, vAdditional, vInvulAdditional; public: Attribute(); Attribute( const Attribute& v ); Attribute( const int& v ); Attribute( const float& v ); Attribute operator=( Attribute& v ); void Set( int type, float ffact, Value value ); void SetBasic( float value ); void SetAdditional( float value ); void SetInvulAdditional( float value ); void SetMultBasic(); void SetMultAll(); void SetMultAdditional(); int GetType(); const Value& GetBasic(); const Value& GetAdditional(); const Value& GetInvulAdditional(); float Get(); }; }

.....

Хотя, казалось бы, здесь же есть всё, что нужно??? Иногда кажется, что самая первая версия проекта идеально сочетала в себе вообще всё, и была более продуманной и законченной чем то, что есть сейчас...

1
5 комментариев