Перейти до основного вмісту
Сервіси навколо гри

Цю сторінку ще не перекладено — показано англійську версію.

Catalog & Commerce

Предмети, ціни, гаманці, вітрини, покупки та права володіння. Справжні інтеграції з магазинами там, де платформи це дозволяють (Stripe, App Store, Google Play, Steam, Xbox). Вітрини за розкладом і для обраної аудиторії. Покупка, кожен крок якої можна перехопити хуком.

Коли це потрібно

  • Ви щось продаєте: за реальні гроші через Stripe, App Store, Google Play, Steam чи Xbox або за валюту з гаманця.
  • Вітрину (storefront) треба формувати під кожного гравця окремо: розклад, аудиторію й ціну обчислює сервер, а клієнт ніколи сам не вирішує, що гравцеві доступно.
  • Правила ціноутворення мають жити в одному хуку, який можна протестувати: знижки, зміна ціни й заборона покупки спрацьовують ще до списання коштів.
  • Чек не можна використати повторно, а повернення коштів має відкликати право володіння (entitlement) через ті самі події, якими його видали.
  • Не знадобиться, якщо ви нічого не продаєте. Нагороди однаково надходять через єдиний Grant модуля Catalog & Commerce із джерелом reward (саме так приходять скрині за цикл у Leaderboards), тож навіть гра без магазину веде один журнал видач, який можна перевірити.

Хто що робить

АкторЩо робить на цій сторінці
playerпереглядає вітрини, купує, керує гаманцем, активує коди
sellerналаштовує каталог (catalog), ціни й розклад вітрин
backend-serviceперевіряє чеки; у хуках покупки змінює ціну або видає предмети

Коротко

Отримати вітрину main, уже сформовану для цього гравця, і купити предмет за валюту з гаманця
// client — the storefront arrives already resolved for this player
var front = await playserv.Commerce.Storefront("main");
var order = await playserv.Commerce.Purchase(front.Items.First(), pay: Pay.Wallet("gems"));
// client — the storefront arrives already resolved for this player
const front = await playserv.commerce.storefront('main');
const order = await playserv.commerce.purchase(front.items[0], { pay: Pay.wallet('gems') });
# client — the storefront arrives already resolved for this player
front = await playserv.commerce.storefront("main")
order = await playserv.commerce.purchase(front.items[0], pay=Pay.wallet("gems"))
Незабаром — Go

Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.

// client — the storefront arrives already resolved for this player
Client->Commerce->Storefronts->Select().Then(
    TPSOnResult<TPSPage<FPSStorefront>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<FPSStorefront>>& Result)
    {
        if (!Result.HasValue()) { return; }
        const FPSStorefront& Front = Result.Value().Rows[0];
        Client->Commerce->Orders->Create(FPSIdempotencyKey(CartId), Front.Items[0], FPSPay::Wallet(TEXT("gems")));
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — в Unity працює без змін зі згенерованими типами
// client — the storefront arrives already resolved for this player
var front = await playserv.Commerce.Storefront("main");
var order = await playserv.Commerce.Purchase(front.Items.First(), pay: Pay.Wallet("gems"));
Два хуки покупки: before змінює ціну першої покупки, after видає предмет
[Before(Commerce.Purchase)]                    // veto or reprice
public static Verdict FirstBuyDiscount(PurchaseIntent p) =>
    p.Player.Purchases == 0 ? Hook.Continue(p.WithPrice(p.Price * 0.5m)) : Hook.Continue(p);

[After(Commerce.Purchase)]                     // grant — side effects only
public static Task Grant(Purchase done) =>
    done.Player.Inventory.Grant(done.Item, done.Count);
// veto or reprice
export const firstBuyDiscount = before(Commerce.purchase, (p: PurchaseIntent) =>
  p.player.purchases === 0 ? Hook.continue(p.withPrice(p.price * 0.5)) : Hook.continue(p));

// grant — side effects only
export const grant = after(Commerce.purchase, (done: Purchase) =>
  done.player.inventory.grant(done.item, done.count));
@before(commerce.purchase)                     # veto or reprice
def first_buy_discount(p: PurchaseIntent) -> Verdict:
    return Hook.continue_(p.with_price(p.price * 0.5)) if p.player.purchases == 0 else Hook.continue_(p)

@after(commerce.purchase)                      # grant — side effects only
async def grant(done: Purchase):
    await done.player.inventory.grant(done.item, done.count)
Незабаром — Go

Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unreal підписується на події покупки та зміни права володіння. Згодом можна буде писати функції і в рушії: графами Blueprint або на C++ (такий код перекладатимуть у скриптовий формат, який перевіряє платформа). Обидва варіанти проходять аналіз і відправку на платформу, як будь-яке оголошення, а виконуються все одно на платформі.

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unity підписується на події покупки та зміни права володіння.

Модель

Що оголошує предмет каталогу.

ОголошенняЩо це
keyпредмет — це авторський контент, і звертаються до нього за ключем, тож перейменування в коді лишається просто перейменуванням
kindconsumable — витрачається; durable — купують один раз, і він лишається в гравця
pricesціна — це грошова сума: ціле число в мінімальних одиницях валюти плюс код валюти, ніколи не число з плаваючою комою. Цін у предмета може бути кілька — і в ігровій, і в реальній валюті
зовнішній ідентифікатор для кожного провайдерапо одному оголошеному слоту на провайдера, бо магазин знає предмет під власним id
на що вказуєза бажанням — сутність (entity) будь-якого оголошеного типу; тоді покупка предмета передає гравцеві володіння цією сутністю

Якою може бути композиція.

Що це
що можна купитилише предмет каталогу, а не довільну сутність: у покупки має бути хтось, хто видає право і відповідає за повернення коштів
два рівні, третього немаєнабір (bundle) — це предмет каталогу, зібраний з інших предметів. Вітрина — це сукупність пропозицій (offer): пропозиція вказує на предмет і може перевизначити його ціну та склад набору
регіональна ціназадає її вітрина, а не сам предмет

Що оголошує вітрина.

ОголошенняЩо це
offersсамі пропозиції, кожна вказує на предмет
scheduleколи вікно вітрини відкривається і закривається — за календарним часом, завжди в UTC
audienceпредикат, а не список гравців. Аудиторія — це правило, яке й далі лишається чинним, а не снапшот на момент створення

Якщо гравець не входить в аудиторію вітрини, для нього ця вітрина не існує.

Стани замовлення.

ІзДо
createdawaiting payment
awaiting paymentpaid · declined · expired
paidgranted
paid або grantedrefunded
ЗавждиЩо це
ціназамовлення фіксує її в момент створення, тож пізніша зміна ціни не змінює того, про що домовилися
awaiting paymentмає оголошений термін, і цей термін оголошують для кожного провайдера окремо, бо в різних провайдерів він різний
видачавідокремлена від оплати: paid і granted — різні стани. Надходження грошей і поява предмета — це два окремі факти, і якщо їх змішати, не видно, котрий із кроків зламався
право володіннязберігає своє джерело — покупку, промокод, нагороду, подарунок, — тож і через рік можна відповісти, звідки воно взялося
витратне право володіннянакопичується: змінюється інкрементом із ключем ідемпотентності (idempotency key), а не перезаписом прочитаного значення
володіннязадає предикат власника: право володіння належить гравцеві за тим самим механізмом, що й будь-який рядок із власником
каталогоголошують у коді, і за замовчуванням він потрапляє в панель у режимі володіння seed: код створює те, чого ще немає, а правки геймдизайнера переживають наступне завантаження
секрети провайдерівзберігаються на операторському рівні (operator plane) — ніколи в оголошенні й ніколи в репозиторії
можливості провайдератеж оголошують: чи має провайдер узагалі придатний API і що він уміє. Так каталог не обіцяє сценарію, який магазин не зможе обслужити

Повернення коштів може надійти будь-коли

Повернення коштів — це зовнішній перехід: він надходить без жодного запиту від гри. Що станеться з уже виданим, оголошують заздалегідь: варіантів відповіді три, і типового немає.

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

Помилки

  • Поза аудиторією — відповідь not found, і повторювати марно. Поза розкладом — теж not found, але запит варто повторити, коли вікно відкриється.
  • Провайдер недоступний і провайдер відхилив платіж — навмисно різні відповіді. Перша — unavailable, і запит можна повторити з наростаючою затримкою. Друга — конфлікт, який повтор не виправить.
  • Недійсний чек — це відмова валідації. Чек, який уже використало інше замовлення чи інший гравець, — конфлікт: саме тому повторне використання чека нічого не дає.
  • Ціна змінилася між читанням вітрини й покупкою — це precondition failure: перечитайте вітрину й вирішіть ще раз, щоб нова ціна не списалася мовчки.
  • Брак ігрової валюти — це конфлікт, а не заборона: дозвіл на покупку у гравця є, просто не вистачає балансу. Після поповнення варто повторити.
  • Постійне право володіння, яке вже є в гравця, — конфлікт.
  • Регіон або вік не дозволяють покупку — відповідь forbidden, і повторювати марно.
  • Минув термін замовлення — конфлікт: створіть нове замовлення.
  • Вичерпано ліміт витрат — конфлікт або обмеження частоти, залежно від того, який саме ліміт спрацював. У відповіді вказано, коли ліміт оновиться.

Обмеження

Конкретні числа з’являться в розділі про ліміти платформи, а що відбувається на кожній межі, визначено вже зараз.

  • Розмір каталогу: публікацію ще одного предмета відхиляють як конфлікт.
  • Вітрини на проєкт: створення нової вітрини відхиляють.
  • Пропозиції у вітрині: додавання нової відхиляють; вітрину ніколи не обрізають мовчки.
  • Час життя замовлення, що чекає оплати: перехід у expired із подією.
  • Частота спроб покупки: обмеження частоти з указаним терміном.
  • Ліміт витрат за період: конфлікт, у якому вказано, коли ліміт оновиться.
  • Зберігання замовлень: після оголошеного періоду замовлення вже не можна прочитати, але воно не зникає без пояснень.
  • Права володіння на гравця: нову видачу відхиляють, а вже видане ніколи не витісняють.
  • Точність ціни — не ліміт, а тип: ціле число в мінімальних одиницях.