Цю сторінку ще не перекладено — показано англійську версію.
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, поки не вирішено (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.// 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, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unreal підписується на події покупки та зміни права володіння. Згодом можна буде писати функції і в рушії: графами Blueprint або на C++ (такий код перекладатимуть у скриптовий формат, який перевіряє платформа). Обидва варіанти проходять аналіз і відправку на платформу, як будь-яке оголошення, а виконуються все одно на платформі.
Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unity підписується на події покупки та зміни права володіння.
Модель
Що оголошує предмет каталогу.
| Оголошення | Що це |
|---|---|
key | предмет — це авторський контент, і звертаються до нього за ключем, тож перейменування в коді лишається просто перейменуванням |
kind | consumable — витрачається; durable — купують один раз, і він лишається в гравця |
prices | ціна — це грошова сума: ціле число в мінімальних одиницях валюти плюс код валюти, ніколи не число з плаваючою комою. Цін у предмета може бути кілька — і в ігровій, і в реальній валюті |
зовнішній ідентифікатор для кожного провайдера | по одному оголошеному слоту на провайдера, бо магазин знає предмет під власним id |
на що вказує | за бажанням — сутність (entity) будь-якого оголошеного типу; тоді покупка предмета передає гравцеві володіння цією сутністю |
Якою може бути композиція.
| Що це | |
|---|---|
що можна купити | лише предмет каталогу, а не довільну сутність: у покупки має бути хтось, хто видає право і відповідає за повернення коштів |
два рівні, третього немає | набір (bundle) — це предмет каталогу, зібраний з інших предметів. Вітрина — це сукупність пропозицій (offer): пропозиція вказує на предмет і може перевизначити його ціну та склад набору |
регіональна ціна | задає її вітрина, а не сам предмет |
Що оголошує вітрина.
| Оголошення | Що це |
|---|---|
offers | самі пропозиції, кожна вказує на предмет |
schedule | коли вікно вітрини відкривається і закривається — за календарним часом, завжди в UTC |
audience | предикат, а не список гравців. Аудиторія — це правило, яке й далі лишається чинним, а не снапшот на момент створення |
Якщо гравець не входить в аудиторію вітрини, для нього ця вітрина не існує.
Стани замовлення.
| Із | До |
|---|---|
created | awaiting payment |
awaiting payment | paid · declined · expired |
paid | granted |
paid або granted | refunded |
| Завжди | Що це |
|---|---|
ціна | замовлення фіксує її в момент створення, тож пізніша зміна ціни не змінює того, про що домовилися |
awaiting payment | має оголошений термін, і цей термін оголошують для кожного провайдера окремо, бо в різних провайдерів він різний |
видача | відокремлена від оплати: paid і granted — різні стани. Надходження грошей і поява предме та — це два окремі факти, і якщо їх змішати, не видно, котрий із кроків зламався |
право володіння | зберігає своє джерело — покупку, промокод, нагороду, подарунок, — тож і через рік можна відповісти, звідки воно взялося |
витратне право володіння | накопичується: змінюється інкрементом із ключем ідемпотентності (idempotency key), а не перезаписом прочитаного значення |
володіння | задає предикат власника: право володіння належить гравцеві за тим самим механізмом, що й будь-який рядок із власником |
каталог | оголошують у коді, і за замовчуванням він потрапляє в панель у режимі володіння seed: код створює те, чого ще немає, а правки геймдизайнера переживають наступне завантаження |
секрети провайдерів | зберігаються на операторському рівні (operator plane) — ніколи в оголошенні й ніколи в репозиторії |
можливості провайдера | те ж оголошують: чи має провайдер узагалі придатний API і що він уміє. Так каталог не обіцяє сценарію, який магазин не зможе обслужити |
Повернення коштів може надійти будь-коли
Повернення коштів — це зовнішній перехід: він надходить без жодного запиту від гри. Що станеться з уже виданим, оголошують заздалегідь: варіантів відповіді три, і типового немає.
Кожна точка розширення називає тип, який вона передає хуку: перед покупкою — намір покупки (гравець, пропозиція, провайдер, ціна), після — саму покупку. Нетипізованого набору даних хук не отримує ніколи.
Помилки
- Поза аудиторією — відповідь not found, і повторювати марно. Поза розкладом — теж not found, але запит варто повторити, коли вікно відкриється.
- Провайдер недоступний і провайдер відхилив платіж — навмисно різні відповіді. Перша — unavailable, і запит можна повторити з наростаючою затримкою. Друга — конфлікт, який повтор не виправить.
- Недійсний чек — це відмова валідації. Чек, який уже використало інше замовлення чи інший гравець, — конфлікт: саме тому повторне використання чека нічого не дає.
- Ціна змінилася між читанням вітрини й покупкою — це precondition failure: перечитайте вітрину й вирішіть ще раз, щоб нова ціна не списалася мовчки.
- Брак ігрової валюти — це конфлікт, а не заборона: дозвіл на покупку у гравця є, просто не вистачає балансу. Після поповнення варто повторити.
- Постійне право володіння, яке вже є в гравця, — конфлікт.
- Регіон або вік не дозволяють покупку — відповідь forbidden, і повторювати марно.
- Минув термін замовлення — конфлікт: створіть нове замовлення.
- Вичерпано ліміт витрат — конфлікт або обмеження частоти, залежно від того, який саме ліміт спрацював. У відповіді вказано, коли ліміт оновиться.
Обмеження
Конкретні числа з’являться в розділі про ліміти платформи, а що відбувається на кожній межі, визначено вже зараз.
- Розмір каталогу: публікацію ще одного предмета відхиляють як конфлікт.
- Вітрини на проєкт: створення нової вітрини відхиляють.
- Пропозиції у вітрині: додавання нової відхиляють; вітрину ніколи не обрізають мовчки.
- Час життя замовлення, що чекає оплати: перехід у
expiredіз подією. - Частота спроб покупки: обмеження частоти з указаним терміном.
- Ліміт витрат за період: конфлікт, у якому вказано, коли ліміт оновиться.
- Зберігання замовлень: після оголошеного періоду замовлення вже не можна прочитати, але воно не зникає без пояснень.
- Права володіння на гравця: нову видачу відхиляють, а вже видане ніколи не витісняють.
- Точність ціни — не ліміт, а тип: ціле число в мінімальних одиницях.