Эта страница ещё не переведена — показана английская версия.
Catalog & Commerce
Предметы, цены, кошельки, витрины, покупки и права владения. Настоящие интеграции с магазинами там, где платформы это позволяют (Stripe, App Store, Google Play, Steam, Xbox). Витрины по расписанию и для выбранной аудитории. Сценарий покупки, в котором любой шаг можно перехватить хуком.
Когда это нужно
- Вы что-то продаёте: за реальные деньги через Stripe, App Store, Google Play, Steam или Xbox либо за валюту из кошелька.
- Витрина (storefront) должна собираться под конкретного игрока: расписание, аудиторию и цену считает сервер, а клиент никогда не решает, что игроку доступно.
- Правила цен должны жить в одном хуке, который можно протестировать: скидки, перерасчёт цены и запрет покупки срабатывают до списания денег.
- Чек нельзя использовать повторно, а возврат должен отзывать право владения (entitlement) через те же события, через которые его выдали.
- Не нужен, если вы ничего не продаёте. Награды всё равно приходят через единственный
Grantкоммерции (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 |
на что указывает | при желании — сущность любого объявленного типа; тогда покупка предмета передаёт игроку владение этой сущностью |
Какой может быть композиция.
| Что это | |
|---|---|
что можно купить | только предмет каталога, а не произвольная сущность: у покупки должен быть тот, кто выдаёт право и отвечает за возврат |
два уровня, третьего нет | набор (bundle) — это предмет каталога, собранный из других предметов. Витрина — это совокупность предложений (offer): предложение указывает на предмет и может переопределить его цену и состав набора |
региональная цена | задаётся витриной, а не на самом предмете |
Что объявляет витрина.
| Объявление | Что это |
|---|---|
offers | сами предложения, каждое указывает на предмет |
schedule | когда витрина открывается и закрывается — по календарному времени, всегда в UTC |
audience | предикат, а не список игроков. Аудитория — это правило, которое продолжает выполняться, а не снимок на момент создания |
Если игрок не входит в аудиторию витрины, для него этой витрины не существует.
Состояния заказа.
| Из | В |
|---|---|
created | awaiting payment |
awaiting payment | paid · declined · expired |
paid | granted |
paid или granted | refunded |
| Всегда | Что это |
|---|---|
цена | фиксируется в заказе в момент его создания, поэтому изменение цены после этого не меняет договорённость |
awaiting payment | длится до объявленного срока, и срок объявляется для каждого провайдера отдельно: у всех он разный |
выдача | отделена от оплаты: paid и granted — разные состояния. Пришли деньги и появился предмет — это два разных факта, и если их смешать, не видно, какой из шагов сломался |
право владения | хранит источник — покупка, промокод, награда, подарок, — так что и через год можно ответить, откуда оно взялось |
расходуемое право владения | накапливается: меняется инкрементом с ключом идемпотентности, а не перезаписью прочитанного значения |
владение | задаётся предикатом владельца: право владения принадлежит игроку по тому же механизму, что и любая строка с владельцем |
каталог | объявляется в коде и по умолчанию попадает в панель в режиме владения seed: код создаёт то, чего ещё нет, а правки геймдизайнера переживают следующий push |
секреты провайдеров | хранятся на операторском уровне — никогда в объявлении и никогда в репозитории |
возможности провайдера | объявляются: есть ли у провайдера вообще пригодный API и что он умеет. Так каталог не обещает сценарий, который магазин не сможет обслужить |
Возврат может прийти в любой момент
Возврат — внешний переход: он приходит без всякого запроса со стороны игры. Что станет с выданным, объявляется явно: вариантов ответа три, и значения по умолчанию нет.
Каждая точка расширения называет тип, который она передаёт хуку. До покупки это намерение покупки — игрок, предложение, провайдер, цена, — а после неё сама покупка. Нетипизированный набор полей хук не получает никогда.
Ошибки
- Игрок вне аудитории — ответ not found, повторять бессмысленно. Витрина вне расписания — тоже not found, но повторить стоит, когда окно откроется.
- Провайдер недоступен и провайдер отклонил платёж — намеренно разные ответы. Первый — unavailable, его можно повторять с нарастающей задержкой. Второй — конфликт, и повтор его не исправит.
- Неверный чек — отказ валидации. Если чек уже использован другим заказом или другим игроком, это конфликт — именно поэтому повторная отправка чека ничего не даёт.
- Цена изменилась между чтением витрины и покупкой — precondition failure: перечитайте витрину и решите заново, вместо того чтобы молча заплатить новую цену.
- Не хватает игровой валюты — это конфликт, а не forbidden: право на покупку у игрока есть, просто баланса недостаточно. Повторить стоит после пополнения.
- Постоянное право владения уже есть — конфликт.
- Регион или возраст не позволяют покупку — ответ forbidden, повторять бессмысленно.
- Срок заказа истёк — конфликт: создайте новый заказ.
- Лимит трат исчерпан — конфликт или ограничение частоты, смотря какой это лимит. В ответе сказано, когда лимит обновится.
Ограничения
Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждом пределе зафиксировано уже сейчас.
- Размер каталога: публикация ещё одного предмета отклоняется как конфликт.
- Витрины на проект: создание новой отклоняется.
- Предложения на витрине: добавление отклоняется, и витрина никогда не обрезается молча.
- Время жизни заказа, ожидающего оплаты: заказ переходит в
expired, и приходит событие. - Частота попыток покупки: ограничение частоты с указанием срока.
- Лимит трат за период: конфликт, в котором сказано, когда лимит обновится.
- Хранение заказов: после объявленного срока заказ больше нельзя прочитать — он не исчезает без объяснений.
- Права владения на игрока: новая выдача отклоняется, а уже выданное никогда не вытесняется.
- Точность цены — не ограничение, а тип: целое число в минимальных единицах.