Як реалізувати турніри в казуальній грі

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт, мене звати Юрій Коваленко, я Node.js-розробник у компанії OBRIO, яка входить в екосистему бізнесів Genesis. Команда розвиває чотири продуктові напрями: GameDev, SaaS, Mobile та Web. Ми — одна з перших команд у Genesis, яка послідовно розвиває геймдев-напрям. Сам Genesis — це компанія-співзасновник, яка будує та інвестує в IT-бізнеси та надає їм усю необхідну інфраструктуру для розвитку на початкових стадіях.

Наразі я відповідаю за всю екосистему сервісів для гри Factory Empire. До OBRIO був частиною команди, яка займається сервісом розсилки SMS-повідомлень у складі європейського B2B-продукту для електронного маркетингу. На початку своєї кар’єри займався автоматизацією бізнес-процесів та розробляв Web-клієнти на Angular.

Рік тому ми з нуля почали збирати GameDev-команду і розробляти казуальну гру. На початкових етапах розробки команда замислилася про те, як зацікавити гравців. У цій статті детально розповім, як реалізували механіку активності.

Матеріал буде цікавим тим, хто лише починає розробку бекенду гри та проєктує механіку, що передбачає сезонні або постійні лідерборди.

Завдання

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

Працює все просто:

  • граєш у гру, отримуєш очки;
  • піднімаєшся у турнірній таблиці, випереджаючи інших гравців;
  • у процесі отримуєш проміжні нагороди за активність;
  • у кінці отримуєш «соковиту» нагороду залежно від місця в таблиці.

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

З вищеописаної механіки можна виділити такі етапи реалізації:

  • турнірна таблиця;
  • турнірні групи;
  • боти;
  • видача нагород.

Далі зупинюся на технічних деталях реалізації.

Деталі стеку. Екосистема сервісів гри розроблена з використанням стеку таких технологій: Node.js + NestJS, TypeORM, PostgreSQL, Bull, Redis, RabbitMQ, Docker, Angular. Для реалізації цієї фічі, окрім програмного коду, знадобиться саме Redis. (Дані гравців вже зберігаються в PostgreSQL).

У ролі основного сховища інформації турнірів обрали Redis, оскільки це динамічні дані, які потрібно часто оновлювати, видаляти і створювати заново для подальших турнірів, що є не настільки ефективним в SQL базах даних. Також у зв’язці Redis + Node.js існують рішення, які розв’язують деякі проблеми, описані далі.

Реалізація

Турнірна таблиця

Турнірна таблиця — це відсортований список, який містить ідентифікатор гравця і число набраних очок. Водночас у такій таблиці відбувається постійне оновлення балів і повна вибірка. Для описаної задачі добре підходить Redis Sorted Set. Така структура даних автоматично сортується при вставці нового елемента чи оновленні наявного, і вибірка за замовчуванням є впорядкованою, що дозволяє не витрачати ресурси сервера та клієнта на сортування. Вставка та оновлення в Sorted Set відбувається однією командою ZADD (подібно операції upsert в БД).

У нашому випадку гравець отримує турнірні очки за рейд і атаку. За «ідеальний рейд» найбільше. Набравши пороговий бал, гравець приєднується до турніру. Незалежно від місця кожен гравець, набравши певну кількість балів, отримує проміжні нагороди.

Групи гравців

Аби створювати адекватну конкуренцію серед гравців, турнірна таблиця не може бути одна: якщо гравець звалиться кудись на глибоке «дно» таблиці та не вміщатиметься у вибірку, яка не може бути необмеженою зі зрозумілих причин, зникне мотивація конкурувати, а отже, зацікавленість грою знизиться. Тому ми вдалися до розділення гравців на групи. У кожної групи своя окрема турнірна таблиця. Турнір конфігурується за допомогою таких змінних: розмір таблиці, розмір квоти реальних гравців, поріг очок для участі, мінімальний рівень локації гравця. Усі ці значення можна поміняти без змін в коді чи перезапуску середовища за допомогою сервісу «адмінки». На початку турніру створюється таблиця, наповнена ботами. Вони потрібні для того, щоб зацікавити гравців.

Перші боти в списку — активні, їх порівняно мало, і їм дається певна кількість очок для емуляції активності. Кількість активних ботів дорівнює різниці розміру таблиці й розміру квоти реальних гравців. Зараз їх відсоток невеликий. Друга частина ботів — неактивні, їхня кількість дорівнює максимальному числу реальних гравців. Очки кожного такого бота відповідають порогу входу, кожен новий гравець «виштовхує» його з таблиці.

Коли таблиця повністю заповнюється і всі неактивні боти вибули, автоматично створюється нова, знову заповнена лише ботами. І так далі. Варто зазначити, що окремо створювати таблицю як Sorted Set не потрібно. Досить почати додавати в неї записи командою ZADD. Якщо ключ у Redis відсутній, він буде створений.

Зв’язок «гравець → таблиця» зберігається в Redis Hash.

Структура виглядає так: key → field → value, де field — ідентифікатор гравця, а value — ідентифікатор турнірної таблиці. Це дає змогу швидко отримати ідентифікатор таблиці, до якої належить гравець. Якщо такий зв’язок ще не створений, гравець вважається новим.

Також із Redis Hash можна отримати список значень всіх ключів командою HKEYS — це відкидає необхідність зберігати окремий список учасників.

Для кожної таблиці створюється Redis Sorted Set з її ідентифікатором у назві ключа. Тому, зберігаючи зв’язок «гравець → таблиця», можна легко отримати потрібну турнірну таблицю, щоб відправити в гру за допомогою двох операцій з Redis:

  • HGET для отримання ідентифікатора таблиці за допомогою ідентифікатора гравця;
  • ZREVRANGE для отримання списку очок, відсортованого за спаданням.

Список ботів таблиці зберігається в Redis List, де з кінця видаляється бот під час приєднання гравця.

Список таблиць зберігається також в Redis List. Кожна нова таблиця додається згори списку, тому новий гравець приєднується в першу за списком таблицю — вона вважається завжди поточною.

Масштабування сервісу, що містить логіку турнірів, і відсутність зручного механізму блокувань в Redis створює челендж під назвою race condition у процесі конкурентного доступу до даних турніру.

Контроль конкурентності без використання блокувань може вирішити черга задач, але черга в оперативній пам’яті на кшталт p-queue або самописний «велосипед» на rxjs не впорається при горизонтальному масштабуванні, оскільки в кожному екземплярі сервера існуватиме своя черга, яка жодним чином не знає про «сусідні».

Тут на допомогу приходить Bull. Ця бібліотека реалізує чергу задач поверх Redis, який є спільним для всіх екземплярів. Її використання вирішує одразу дві проблеми: масштабування і контроль конкурентності. А обгортка від фреймворку NestJS допоможе легко вбудувати API для роботи з чергою в код програми.

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

Боти

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

Для персоналізації поведінки використовуються сегменти — мітки, які присвоюються гравцю за його активність протягом життя у грі. На основі сегментів будуються правила, які регулюють стрімкість наздоганяння ботів. Це полегшить можливість виграти пасивному гравцю, що дасть ресурси й бажання грати активніше і закріпитися в наступному турнірі чи пройти декілька локацій, а отже, повертатися в гру частіше.

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

Завершення турніру і присвоєння нагород

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

Останній крок — очистити дані проведення турніру, більше вони не знадобляться. Для цього лише потрібно передати в команду DEL список ключів незалежно від типу.

Турніри на клієнті

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

Передача даних турніру на клієнт

Для взаємодії клієнта з сервером використовується WebSocket для мінімізації затримки та забезпечення взаємодії в режимі реального часу.

Для отримання даних турніру ми використовуємо повідомлення TournamentSync, яке містить дані такого вигляду:

{
  "event": "TournamentSync",
  "data": {
    "tournament": {
      "id": 1,
      "name": "Grand Tournament",
      "type": "GrandTournament",
      "endDate": "2020-11-13T18:00:00Z"
    },
    "scoreThreshold": 10,
    "scores": [
      {
        "player": {
          "id": "c2cbff05-1f02-492a-837c-69d090e4a447",
          "nickname": "Player1",
          "avatar": "https://avatar.com/a1"
        },
        "score": "130"
      },
      {
        "player": {
          "id": "ed59d0c8-8823-46da-8380-1fefee1f6b18",
          "nickname": "Player2",
          "avatar": "https://avatar.com/a2"
        },
        "score": "120"
      }
    ]
  }
}

У разі, якщо гравець не встиг набрати кількість очок, необхідну для приєднання до турніру, поле scores має значення null. Якщо в конкретний момент активного турніру немає, поле tournament також має значення null.

Дані в полі tournament необхідні, щоб:

  • довантажити потрібний арт та елементи UI;
  • відобразити іконку в інтерфейсі гри, щоб показати можливість взяти участь у турнірі;
  • показати таймер, щоб мотивувати гравця не гаяти час і зайняти якомога вище місце.

Дані в полі scores потрібні для безпосереднього відображення лідерборда.

І відповідно поле scoreThreshold дає розуміння гравцю, яку кількість очок потрібно набрати, щоб приєднатися до турніру. Для оптимізації TournamentSync відправляється лише на початку гри, щоб дізнатися про наявність активного турніру. А також щоразу по кліку на іконку турніру в інтерфейсі гри.

Оновлення кількості очок

Для зміни кількості очок використовується повідомлення BalanceUpdate — те саме, що і для зміни будь-якої валюти в грі. Клієнт передає кількість очок, які потрібно додати гравцю, а сервер рахує суму й оновлює запис в лідерборді турніру. І вже з наступним повідомленням TournamentSync по кліку на іконку турніру гравець отримує актуальний розклад лідерборду.

Сповіщення гравця про турнір

Є необхідність сповіщати гравця про початок і кінець турніру. Для офлайн-гравців усе просто: всім відправляється пуш-сповіщення про початок турніру і лише учасникам поточного — про його кінець.

Для онлайн-гравців використовуються повідомлення по WebSocket із назвою TournamentStart і TournamentEnd. Їх необхідно відправити, оскільки онлайн-гравець вже у грі, а отже, не може дізнатися про початок турніру або наявність нагород за щойно завершений.

TournamentStart змушує гру відправити повідомлення TournamentSync, щоб отримати його дані. TournamentEnd спричиняє відправку повідомлення RewardsSync, щоб видати нагороди гравцю.

Планування розкладу турнірів

Для керування турнірами розробили графічний інтерфейс у рамках раніше згаданої «адмінки» у вигляді календаря. Заплановані турніри моніторить кронджоба і сповіщає основний ігровий сервер про необхідність запуску чи завершення.

Підведемо підсумки

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

Варто зазначити, що «адмінка» є не обов’язковим інструментом, фічу можна реалізувати і без неї, використавши конфігурацію через змінні середовища (хардкодити не раджу). Але наявність такого сервісу в екосистемі гри мінімізує участь розробника і спрощує керування LiveOps-ом гри спеціально виділеною людиною — LiveOps-менеджером.

Також я окремо акцентував на користі та великому спектрі можливостей, які дає Redis (насправді їх іще більше).

Ігровим проєктам, що не мають повноцінного бекенду, для реалізації турнірів можна скористатися готовими рішеннями від Google Play Services, Game Center (iOS), Facebook. Оскільки в нас він уже був і всі дані гравців зберігаються на нашому сервері, то послідовним рішенням була реалізація з нуля власними силами.

👍НравитсяПонравилось7
В избранноеВ избранном3
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дуже цікава та корисна стаття. Юрій — харош!

Очень полезная статья. Боты же тут не для вытеснения игроков, а наоборот поддерживают активность при её недостатке. Далеко не всегда люди активны и это не напрямую зависит от «крутости» игры или маркетинговой компании, неинтересно заходить в игры/приложения где все рассчитано на взаимодействие с другими игроками, а его нет потому что например попал на пассивный сервер или в мертвую гильдию. Часто активность теряется от недостатка активности и получается замкнутый круг. Спасибо за статью!

Заходить

в игры/приложения где все рассчитано на взаимодействие с другими игроками

и играть с ботами — это ужасный юзер экспириенс.

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

Так что спорно, но лучше чем «никак»

Ну, а кто им виноват ? Играют же люди на нормальных портативных платформах(ПСП, Свитч, будет Стим Дек в этом году) и хватает времени на все, и играть удобно, и игры нормальные, а не так что каждая первая — это «казино» с донатом и лутбоксами...

Виноват в чём? Что есть аудитория этих игр и они в это играют? Странный вопрос, если честно.

Классная статья! Спасибо, Юра:)

Игра-доильня, коих тысячи и тысячи на практически идентичных механизмах, будет рассказывать как организовать «турнир» с ботатми. Чота ржу.

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

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

Да ладно, на самом деле часто вводят особенно во всякую не популярную муть. Человек должен видеть что он не один в онлайне. Хотя по факту состязается с кучей ботов :-)

Я просто не играю в мобайл игры. Но мне кажется, надо работать над тем, чтобы люди сами хотели попасть на турнирные соревнования (мотивировать их вкусно, маркетинг и т.д.), а не запускать ботов под видом людей xD
Я бы, например, удалил такую игру, узнав бы об этом. Хотя если представлять, что в казуальщину играют домохозяйки и люди с IQ банана, то тогда можно и ботов, особенно если онлайна нет.

Уже в офтоп пошли
Ну таков рынок. Полноценный геймплей очень плохо дружит с сенсором. Изначально старались его упростить и вышел Angry Birds. Сейчас к сожалению вообще убрали :-) И современные игры это красочные кликеры.
Ну вроде рынок портативных консолей оживает. У меня есть Nintendo Switch, к которой есть претензии, но вроде сонька тоже скоро представит свою — уже рынок растормошат и хорошие игры снова можно будет брать с собой в дорогу

Скоро (а может и уже) выйдет Steam Deck. По заявлению Valve, на нём все игры пойдут минимум в 30 фпс что есть в стиме

В том-то и дело, что первыми в турнирах как раз-то и отстреливаются люди с IQ банана — в любых играх, где есть общение и от людей в игре зависит хоть что-то.

Но всё меняется радикально, если игру делать командной.

Да, такие игры ПЕРЕСТАЮТ быть казуальными, от слова «жрут время».

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

О том что всё вложенное УЖЕ потеряно — живые люди не догадываются. Особенно дети. Но узнают, когда такие как я, просто исследования ради, заходят сеточкой ботов. И живые люди проигрывают, несмотря на вложения. И больше играть не хотят. Даже в аналоги.

В тому і справа, що ботів стає мінімум, коли все більше гравців приєднується до турніру, набравши порогову кількість очок. Коли таблиця заповнюється реальними гравцями, ботів всього пару штук. Боти служать в першу чергу плейсхолдером, а вже в другу підігрівають інтерес гравців.

Такий ринок мобільних ігор, турніри — популярна фіча, що впливає на всі продуктові метрики гри. Значить, цільовій аудиторії подобається, тому відмова від її реалізації означає свідомо уступити конкурентам. Задача розробника створити Smooth Experience для гравця, щоб він вибирав нашу гру, а не конкурента (як і в будь-якому продукті).

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

Интересное решение, спасибо за статью!

Подписаться на комментарии