Перейти к основному содержимому
Сервисы вокруг игры

Эта страница ещё не переведена — показана английская версия.

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

Как объявлять через атрибуты в 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
на что указываетпри желании — сущность любого объявленного типа; тогда покупка предмета передаёт игроку владение этой сущностью

Какой может быть композиция.

Что это
что можно купитьтолько предмет каталога, а не произвольная сущность: у покупки должен быть тот, кто выдаёт право и отвечает за возврат
два уровня, третьего нетнабор (bundle) — это предмет каталога, собранный из других предметов. Витрина — это совокупность предложений (offer): предложение указывает на предмет и может переопределить его цену и состав набора
региональная ценазадаётся витриной, а не на самом предмете

Что объявляет витрина.

ОбъявлениеЧто это
offersсами предложения, каждое указывает на предмет
scheduleкогда витрина открывается и закрывается — по календарному времени, всегда в UTC
audienceпредикат, а не список игроков. Аудитория — это правило, которое продолжает выполняться, а не снимок на момент создания

Если игрок не входит в аудиторию витрины, для него этой витрины не существует.

Состояния заказа.

ИзВ
createdawaiting payment
awaiting paymentpaid · declined · expired
paidgranted
paid или grantedrefunded
ВсегдаЧто это
ценафиксируется в заказе в момент его создания, поэтому изменение цены после этого не меняет договорённость
awaiting paymentдлится до объявленного срока, и срок объявляется для каждого провайдера отдельно: у всех он разный
выдачаотделена от оплаты: paid и granted — разные состояния. Пришли деньги и появился предмет — это два разных факта, и если их смешать, не видно, какой из шагов сломался
право владенияхранит источник — покупка, промокод, награда, подарок, — так что и через год можно ответить, откуда оно взялось
расходуемое право владениянакапливается: меняется инкрементом с ключом идемпотентности, а не перезаписью прочитанного значения
владениезадаётся предикатом владельца: право владения принадлежит игроку по тому же механизму, что и любая строка с владельцем
каталогобъявляется в коде и по умолчанию попадает в панель в режиме владения seed: код создаёт то, чего ещё нет, а правки геймдизайнера переживают следующий push
секреты провайдеровхранятся на операторском уровне — никогда в объявлении и никогда в репозитории
возможности провайдераобъявляются: есть ли у провайдера вообще пригодный API и что он умеет. Так каталог не обещает сценарий, который магазин не сможет обслужить

Возврат может прийти в любой момент

Возврат — внешний переход: он приходит без всякого запроса со стороны игры. Что станет с выданным, объявляется явно: вариантов ответа три, и значения по умолчанию нет.

Каждая точка расширения называет тип, который она передаёт хуку. До покупки это намерение покупки — игрок, предложение, провайдер, цена, — а после неё сама покупка. Нетипизированный набор полей хук не получает никогда.

Ошибки

  • Игрок вне аудитории — ответ not found, повторять бессмысленно. Витрина вне расписания — тоже not found, но повторить стоит, когда окно откроется.
  • Провайдер недоступен и провайдер отклонил платёж — намеренно разные ответы. Первый — unavailable, его можно повторять с нарастающей задержкой. Второй — конфликт, и повтор его не исправит.
  • Неверный чек — отказ валидации. Если чек уже использован другим заказом или другим игроком, это конфликт — именно поэтому повторная отправка чека ничего не даёт.
  • Цена изменилась между чтением витрины и покупкой — precondition failure: перечитайте витрину и решите заново, вместо того чтобы молча заплатить новую цену.
  • Не хватает игровой валюты — это конфликт, а не forbidden: право на покупку у игрока есть, просто баланса недостаточно. Повторить стоит после пополнения.
  • Постоянное право владения уже есть — конфликт.
  • Регион или возраст не позволяют покупку — ответ forbidden, повторять бессмысленно.
  • Срок заказа истёк — конфликт: создайте новый заказ.
  • Лимит трат исчерпан — конфликт или ограничение частоты, смотря какой это лимит. В ответе сказано, когда лимит обновится.

Ограничения

Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждом пределе зафиксировано уже сейчас.

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