Дизайн пошаговой игры, в которой действия имеют побочные эффекты

19

Я пишу компьютерную версию игры Dominion . Это пошаговая карточная игра, в которой карты действий, карты сокровищ и карты победных очков накапливаются в личной колоде игрока. У меня довольно хорошо развита структура классов, и я начинаю разрабатывать игровую логику. Я использую Python, и я могу добавить простой графический интерфейс с Pygame позже.

Последовательность ходов игроков регулируется очень простым конечным автоматом. Повороты проходят по часовой стрелке, и игрок не может выйти из игры, пока она не закончится. Игра одного хода также является конечным автоматом; в общем, игроки проходят через «фазу действия», «фазу покупки» и «фазу очистки» (в таком порядке). На основании ответа на вопрос Как реализовать пошаговый игровой движок? конечный автомат - стандартная техника для этой ситуации.

Моя проблема в том, что во время фазы действия игрока она может использовать карту действия, которая имеет побочные эффекты, либо на себя, либо на одного или нескольких других игроков. Например, одна карта действия позволяет игроку сделать второй ход сразу после завершения текущего хода. Другая карта действия заставляет всех других игроков сбросить две карты из своих рук. Еще одна карта действий ничего не делает для текущего хода, но позволяет игроку взять дополнительные карты в свой следующий ход. Чтобы сделать вещи еще более сложными, в игру часто добавляются новые дополнения, которые добавляют новые карты. Мне кажется, что жесткое кодирование результатов каждой карты действий в конечный автомат игры было бы и уродливым, и непригодным для использования. Ответ на пошаговый стратегический цикл не идет в уровень детализации, который обращается к проектам, чтобы решить эту проблему.

Какую модель программирования я должен использовать, чтобы охватить тот факт, что общий шаблон смены может быть изменен с помощью действий, выполняемых в ходе очереди? Должен ли объект игры отслеживать эффекты каждой карты действий? Или, если карты должны реализовывать свои собственные эффекты (например, посредством реализации интерфейса), какая настройка требуется, чтобы дать им достаточно энергии? Я придумал несколько решений этой проблемы, но мне интересно, есть ли стандартный способ ее решения. В частности, я хотел бы знать, какой объект / класс / что-либо отвечает за отслеживание действий, которые каждый игрок должен выполнять в результате разыгрываемой карты действий, а также как это связано с временными изменениями в нормальной последовательности действий. конечный автомат.

Апис Утилис
источник
2
Привет Апис Утилис и добро пожаловать в GDSE. Ваш вопрос хорошо написан, и это здорово, что вы ссылались на соответствующие вопросы. Тем не менее, ваш вопрос охватывает множество различных проблем, и чтобы полностью его решить, вопрос, вероятно, должен быть огромным. Вы все еще можете получить хороший ответ, но вам и сайту будет полезно, если вы решите проблему еще раз. Может быть, начать с создания более простой игры и до Dominion?
michael.bartnett
1
Я бы начал с того, чтобы дать каждой карточке сценарий, который изменяет состояние игры, и, если ничего странного не происходит, вернемся к правилам хода по умолчанию ...
Яри ​​Комппа

Ответы:

11

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

Чтобы карты могли взаимодействовать с более поздними игровыми событиями, вы можете добавить API сценариев, чтобы добавить «ловушки сценариев» к определенным событиям, таким как начало и окончание этапов игры, или определенные действия, которые могут выполнять игроки. Это означает, что сценарий, который выполняется, когда играется карта, может зарегистрировать функцию, которая вызывается при следующем достижении определенной фазы. Количество функций, которые можно зарегистрировать для каждого события, должно быть неограниченным. Когда их несколько, они затем вызываются в порядке их регистрации (если, конечно, не существует основного правила игры, которое говорит что-то другое).

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

Ваша карта с двойным поворотом будет делать что-то вроде этого:

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(Я понятия не имею, есть ли у Dominion что-то вроде «фазы очистки» - в этом примере это гипотетическая последняя фаза хода игроков)

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

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

Карта, из-за которой целевой игрок теряет хит-пойнт при каждой игре, будет выглядеть следующим образом:

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

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

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

Philipp
источник
1
Вау. Это хаос конфетти вещь.
Яри ​​Комппа
Отличный ответ, @Philipp, и это заботится о многих вещах, сделанных в Доминионе. Тем не менее, существуют действия, которые должны происходить немедленно при разыгрывании карты, то есть при разыгрывании карты, которая заставляет другого игрока перевернуть верхнюю карту своей библиотеки и позволяет текущему игроку сказать «Оставь это» или «Сбрось это». Вы бы написали перехватчики событий, чтобы позаботиться о таких немедленных действиях, или вам нужно было бы придумать дополнительные методы написания сценариев для карт?
фнорд
2
Когда что-то должно произойти немедленно, скрипт должен напрямую вызывать соответствующие функции и не регистрировать функцию перехвата.
Филипп
@JariKomppa: набор Unglued был намеренно бессмысленным и полон сумасшедших карт, которые не имели смысла. Моей любимой была карта, которая заставляла всех получать повреждения, когда они произносили определенное слово. Я выбрал «The».
Джек Эйдли
9

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

Во-первых, сложная карточная игра, такая как Chez Geek или Fluxx (и, я полагаю, Dominion) потребовала бы, чтобы карты были пригодны для сценариев. По сути, каждая карта поставляется со своим набором скриптов, которые могут по-разному изменять состояние игры. Это позволит вам дать системе возможность заглянуть в будущее, поскольку сценарии могут делать то, о чем вы не можете думать прямо сейчас, но могут появиться в будущем расширении.

Во-вторых, жесткий «поворот» может вызывать проблемы.

Вам нужен какой-то «стек ходов», который содержит «специальные ходы», например, «сбросить 2 карты». Когда стек пуст, нормальный ход по умолчанию продолжается.

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

  • Выберите N карт (согласно действующим правилам, которые можно менять с помощью карт)
  • Играйте в N карт (согласно действующим правилам, можно менять с помощью карт)
    • Одна из карт может быть «возьми 3, сыграй 2 из них»
      • Одна из этих карт вполне может быть "сделать еще один ход"
    • Одна из карт может быть "сбросить и взять"
  • Если вы измените правила, чтобы выбрать больше карт, чем было в начале хода, выберите больше карт
  • Если вы измените правила для меньшего количества карт в руке, все остальные должны немедленно сбросить карты
  • Когда ваш ход заканчивается, сбрасывайте карты до тех пор, пока у вас не будет N карт (их можно изменить с помощью карт, снова), а затем сделайте еще один ход (если вы когда-нибудь играли в карту с «другим ходом» в указанном беспорядке).

..и так далее и тому подобное. Поэтому разработка структуры поворота, которая может справиться с вышеуказанным нарушением, может быть довольно сложной. Добавьте к этому многочисленные игры с картами «когда-либо» (как в «chez geek»), в которых карты «всякий раз» могут нарушить нормальный поток, например, отменив ту карту, которая была сыграна последней.

Поэтому я бы начал с разработки очень гибкой структуры поворотов, спроектировав ее так, чтобы ее можно было описать как сценарий (поскольку каждая игра должна иметь свой собственный «основной сценарий», обрабатывающий основную структуру игры). Тогда любая карта должна быть в сценарии; большинство карт, вероятно, не делают ничего странного, но другие делают. Карты также могут иметь различные атрибуты - могут ли они храниться в руке, разыгрываться «когда угодно», могут ли они храниться в виде активов (например, «хранители» fluxx или различные вещи в «чиз-гике», например, еда) ...

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

Яри ​​Комппа
источник
Это отличный ответ, и я бы принял оба, если бы мог. Я разорвал связь, приняв ответ от человека с более низкой репутацией :)
Apis Utilis
Нет проблем, я к этому уже привык .. =)
Яри ​​Комппа
0

Похоже, что Hearthstone делает что-то связанное, и, честно говоря, я думаю, что лучший способ добиться гибкости - это механизм ECS с ориентированным на данные дизайном. Я пытался сделать клон очага, и в противном случае это оказалось невозможным. Все крайние случаи. Если вы сталкиваетесь со многими из этих странных крайних случаев, то это, вероятно, лучший способ сделать это. Я довольно предвзят, хотя из недавнего опыта пробую эту технику.

Изменить: ECS может даже не понадобиться в зависимости от того, какая гибкость и оптимизация вам нужна. Это всего лишь один из способов сделать это. DOD Я ошибочно считал процедурным программированием, хотя они имеют много общего. Я имею в виду. Что вам следует подумать об отказе от ООП полностью или, по крайней мере, в основном, и вместо этого сфокусировать свое внимание на данных и их организации. Избегайте наследования и методов. Вместо этого сосредоточьтесь на общедоступных функциях (системах) для манипулирования данными вашей карты. Каждое действие не является шаблонной вещью или какой-либо логикой, а является необработанными данными. Где ваши системы, то используйте его для выполнения логики. Случай целочисленного переключения или использование целого числа для доступа к массиву указателей на функции помогают эффективно определить желаемую логику из входных данных.

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

Есть преимущества, которые будут иметь это. Каждая карта может иметь значение перечисления (ей) или строку (и) для представления их действия. Этот стажер позволяет создавать карточки с помощью текстовых или json-файлов и позволяет программе автоматически импортировать их. Если вы делаете действия игрока списком данных, это дает еще большую гибкость, особенно если карта зависит от логики прошлого, как это делает Hearthstone, или если вы хотите сохранить игру или переиграть игру в любой момент. Есть потенциал для создания ИИ более легко. Особенно при использовании «служебных систем» вместо «дерева поведения». Сетевое взаимодействие также становится проще, потому что вместо того, чтобы выяснять, как заставить целые, возможно, полиморфные объекты передавать по проводам и как будет настроена сериализация, ваши игровые объекты уже являются не чем иным, как простыми данными, которые в конечном итоге очень легко перемещать. И, наконец, что не менее важно, это позволяет вам легче оптимизировать, потому что вместо того, чтобы тратить время на беспокойство о коде, вы сможете лучше организовать свои данные, чтобы процессору было легче справляться с этим. У Python могут быть проблемы здесь, но посмотрите «строку кэша» и как она связана с разработчиком игры. Возможно, это не важно для прототипирования, но в будущем это пригодится.

Несколько полезных ссылок.

Примечание: ECS позволяет динамически добавлять / удалять переменные (называемые компонентами) во время выполнения. Пример c программы того, как ECS может «выглядеть» (есть масса способов сделать это).

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

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

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}
Blue_Pyro
источник
1
Этот ответ был бы лучше, если бы он более подробно описал, как вы бы порекомендовали настроить ECS, ориентированную на данные, и применить ее для решения этой конкретной проблемы.
DMGregory
Обновлено спасибо за указание на это.
Blue_Pyro
В общем, я думаю, что плохо говорить кому-то «как» настроить такой подход, но вместо этого позволить им разработать свое собственное решение. Это хороший способ практиковать и дает лучшее решение проблемы. Когда мы думаем о данных больше, чем о логике таким образом, получается, что существует множество способов выполнить одно и то же, и все зависит от потребностей приложения. Как и программист времени / знаний.
Blue_Pyro