Розробка покрокової гри, де дії мають побічні ефекти


19

Я пишу комп’ютерну версію гри Dominion . Це покрокова карткова гра, де картки дій, картки скарбів та карт перемоги накопичуються в особистій колоді гравця. У мене структура класу досить розвинена, і я починаю розробляти логіку гри. Я використовую python, і я можу пізніше додати простий GUI з pygame.

Послідовність повороту гравців регулює дуже проста державна машина. Повороти проходять за годинниковою стрілкою, і гравець не може вийти з гри до її закінчення. Гра в один поворот - це також державна машина; загалом гравці проходять через "фазу дій", "фазу купівлі" та "фазу очищення" (у такому порядку). На підставі відповіді на питання Як реалізувати покрокову ігрову систему? , державна машина - це стандартна методика для даної ситуації.

Моя проблема полягає в тому, що під час фази дії гравця вона може використовувати карту дій, яка має побічні ефекти, як на себе, так і на одного або декількох інших гравців. Наприклад, одна картка дій дозволяє гравцеві зробити другий хід відразу після завершення поточного ходу. Інша карта дій змушує всіх інших гравців викинути дві картки з рук. Ще одна картка дій нічого не робить для поточного ходу, але дозволяє гравцеві намалювати зайві картки на наступному кроці. Щоб зробити речі ще складнішими, в гру часто з’являються нові розширення, які додають нові карти. Мені здається, що жорстке кодування результатів кожної картки дій в державній машині гри було б і потворним, і непридатним. Відповідь на покрокову стратегію циклу не вникає в рівень деталізації, який стосується конструкцій для вирішення цієї проблеми.

Яку модель програмування я повинен використовувати, щоб охопити той факт, що загальну схему поворотів можна змінювати діями, що відбуваються в межах черги? Чи повинен ігровий об’єкт відслідковувати ефекти кожної картки дій? Або, якщо картки повинні реалізовувати власні ефекти (наприклад, реалізуючи інтерфейс), яка установка потрібна, щоб дати їм достатню потужність? Я придумав декілька рішень цієї проблеми, але мені цікаво, чи існує стандартний спосіб її вирішення. Зокрема, я хотів би дізнатися, який об’єкт / клас / все, що відповідає за відстеження дій, які повинен виконувати кожен гравець внаслідок відтворення картки дій, а також, як це стосується тимчасових змін у звичайній послідовності машина повороту.


2
Привіт, Апіс Утіліс, і ласкаво просимо на GDSE. Ваше запитання добре написане, і це чудово, що ви посилалися на відповідні запитання. Однак ваше запитання охоплює безліч різних проблем, і щоб повністю його охопити, питання, ймовірно, повинно бути величезним. Ви все-таки зможете отримати хорошу відповідь, але ви самі і веб-сайт отримають користь, якщо ви ще трохи зламаєте свою проблему. Може, почати зі створення простішої гри та побудувати до Dominion?
michael.bartnett

1
Я б почав із надання кожної картки сценарію, який змінює стан гри, і якщо нічого дивного не відбувається, поверніться до правил повороту за замовчуванням ...
Jari Komppa

Відповіді:


11

Я погоджуюсь з Ярі Komppa, що визначення ефектів карт за допомогою потужної мови сценаріїв - це шлях. Але я вважаю, що запорукою максимальної гнучкості є керованість подіями.

Щоб картки могли взаємодіяти з пізнішими ігровими подіями, ви можете додати сценарій API, щоб додати "гачки сценарію" до певних подій, наприклад, початку та закінчення фаз гри чи певних дій, які можуть виконувати гравці. Це означає, що сценарій, який виконується під час відтворення карти, здатний зареєструвати функцію, яка викликається наступного разу, коли буде досягнута конкретна фаза. Кількість функцій, які можна зареєструвати для кожної події, має бути необмеженою. Коли їх більше, вони викликаються в порядку їх реєстрації (якщо, звичайно, немає основного правила гри, яке говорить щось інше).

Потрібно мати можливість зареєструвати ці гачки лише для всіх гравців або лише для певних гравців. Я б також запропонував додати можливість гачкам вирішити самостійно, чи потрібно їх називати чи ні. У цих прикладах зворотне значення функції гака (справжнє або хибне) використовується для вираження цього.

Тоді ваша подвійна картка зробить щось подібне:

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()функції (яка, до речі, також була б хорошим кандидатом на приголомшливу подію).

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


1
Ого. Ця річ з хаосом.
Jari Komppa

Відмінна відповідь, @Philipp, і це піклується про багато речей, зроблених у Домініоні. Однак є дії, які повинні відбутися негайно, коли грається картка, тобто грається картка, яка змушує іншого гравця перевернути верхню карту своєї бібліотеки і дозволяє поточному гравцеві сказати "Зберігати" або "Відкинути його". Ви б написали гачки подій, щоб подбати про такі негайні дії, або вам потрібно було б придумати додаткові способи написання карт?
fnord

2
Коли щось має відбутися негайно, скрипт повинен викликати відповідні функції безпосередньо, а не реєструвати функцію гака.
Філіп

@JariKomppa: Набір Unglued був навмисно безглуздим і повним шалених карток, які не мали сенсу. Моєю улюбленою була картка, яка змусила всіх нанести шкоду шкоді, сказавши певне слово. Я вибрав "той".
Джек Едлі

9

Я дав цю проблему - гнучку комп'ютеризовану машину з картковими іграми - деякі думали деякий час тому.

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

По-друге, жорсткий "поворот" може спричинити проблеми.

Вам потрібен якийсь "стек повороту", який містить "спеціальні обороти", наприклад "відкинути 2 карти". Коли стек порожній, нормальний поворот за замовчуванням продовжується.

У Fluxx цілком можливо, що один поворот іде щось на зразок:

  • Pick N карти (як зазначено в діючих правилах, що змінюються за допомогою карт)
  • Грати в N карт (як зазначено в діючих правилах, змінюється за допомогою карт)
    • Однією з карт може бути "взяти 3, зіграти 2 з них"
      • Однією з таких карток цілком може бути "по черзі"
    • На одній з карток може бути "відкинути та намалювати"
  • Якщо ви змінили правила, щоб вибрати більше карток, ніж ви робили, коли почалася ваша черга, виберіть більше карток
  • Якщо ви зміните правила щодо меншої кількості карт у руці, всі інші повинні негайно відмовитися від карт
  • Коли ваша черга закінчується, відкиньте карти, доки у вас не буде N карт (знову можна змінити через карти), а потім зробіть ще один виток (якщо ви грали в карту «зробіть іншу чергу» колись у вищезгаданому безладі).

..і так далі, тощо. Тож спроектувати структуру повороту, яка може впоратися з вищезгаданими зловживаннями, може бути досить складно. Додайте до цього численні ігри з картками "коли-небудь" (наприклад, у "chez geek"), де карти "коли-небудь" можуть порушити нормальний потік, наприклад, скасувавши будь-яку карту востаннє.

Отже, я б почав з розробки дуже гнучкої структури повороту, спроектував її так, щоб її можна було описати як сценарій (так як для кожної гри знадобиться власний "головний сценарій", що обробляє основну структуру гри). Тоді будь-яка карта повинна бути написана сценарієм; Більшість карток, ймовірно, не роблять нічого дивного, але інші. Картки також можуть мати різні атрибути - чи їх можна тримати в руці, грати "коли завгодно", чи можна їх зберігати як активи (як флюкс "зберігачі", або різні речі в "chez geek", як їжа) ...

Я ніколи насправді не починав реалізовувати щось із цього, тому на практиці ви можете знайти безліч інших проблем. Найпростіший спосіб почати - почати з того, що ви знаєте про систему, яку ви хочете впровадити, та впроваджувати їх чітко прописаними способами, встановлюючи якомога менше каменю, тож коли розширення не відбудеться, вам не потрібно буде переглядати базова система - багато. =)


Це чудова відповідь, і я б прийняв обидва, якби міг. Я розірвав краватку, прийнявши відповідь від людини з нижчою репутацією :)
Apis Utilis

Жодної проблеми, я до цього звик .. =)
Jari Komppa

0

Hearthstone, здається, робить відносні речі, і чесно кажучи, я вважаю, що найкращий спосіб досягти гнучкості - через двигун ECS з дизайном, орієнтованим на дані. Намагалися зробити клон для вогнища, інакше це неможливо. Усі крайові корпуси. Якщо ви стикаєтеся з цією кількістю дивних крайових справ, то, мабуть, це найкращий спосіб зробити це. Я досить упереджений, хоча з недавнього досвіду випробував цю техніку.

Редагування: ECS може навіть не знадобитися залежно від типу гнучкості та оптимізації, яку ви бажаєте. Це лише один із способів досягти цього. DOD Я помилково вважав процедурне програмування, хоча вони пов'язані багато. Що я маю на увазі, це. Що вам слід подумати, щоб повністю або здебільшого відмовитися від OOP і замість цього зосередити свою увагу на даних та тому, як це організовано. Уникайте успадкування та методів. Замість цього зосередьтеся на публічних функціях (системах) для маніпулювання даними вашої картки. Кожна дія - це не будь-яка шаблонна річ або логіка, а натомість є необробленими даними. Там, де ваші системи використовують його для виконання логіки. Випадок перемикання цілочисника або використання цілого числа для доступу до масиву функціональних покажчиків допомагають ефективно визначити потрібну логіку з вхідних даних.

Основні правила, які слід дотримуватися, - це те, що ви повинні уникати пов'язування логіки безпосередньо разом із даними, ви повинні уникати того, щоб дані залежали один від одного якомога більше (можливі винятки), а також, коли ваша бажаюча гнучка логіка, яка відчуває себе недосяжною ... Подумайте про перетворення його в дані.

Є користь від цього. Кожна картка може мати значення перерахунків (enum (s)) або рядки (и), які відображають їх дії. Цей стажер дозволяє розробляти картки за допомогою текстових або json-файлів та дозволяє програмі автоматично імпортувати їх. Якщо ви складаєте список дій гравця, це дає ще більшу гнучкість, особливо якщо карта залежить від минулої логіки, як це робить вогнище, або якщо ви хочете зберегти гру або повторити гру в будь-який момент. Є потенціал, щоб створити AI простіше. Особливо при використанні "корисних систем" замість "дерева поведінки". Мережа також стає простішою, тому що замість того, щоб розібратися, як змусити цілі, можливо, поліморфні об'єкти переноситись по дроту та як встановлювати серіалізацію після факту, у вас вже є ваші ігрові об’єкти - це не що інше, як прості дані, які в кінцевому підсумку дуже легко пересуватися. І останнє, але, безумовно, не в останню чергу це дозволяє вам оптимізувати простіше, оскільки замість того, щоб витрачати час, турбуючись про код, ви зможете краще організувати ваші дані, щоб процесор матиме простіший час з цим. Тут може виникнути проблеми у 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);
    }
}

1
Ця відповідь буде кращою, якби детальніше розповісти про те, як ви рекомендуєте встановити свій орієнтований на дані ECS та застосувати його для вирішення цієї конкретної проблеми.
DMGregory

Оновлено дякую, що вказали на це.
Blue_Pyro

Взагалі, я думаю, що погано сказати комусь "як" налаштувати такий підхід, але натомість дозволяти їм розробити власне рішення. Це виявляється як хорошим способом практики, так і дозволяє потенційно краще вирішити проблему. Якщо таким чином думати про дані більше, ніж про логіку, то виявляється, що існує багато способів здійснення того ж самого, і все залежить від потреб програми. А також час / знання програміста.
Blue_Pyro
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.