GS2-Limit

回数制限機能

プレイヤーが行動できる回数を制限するための仕組みです。 「1日に5回までガチャを引ける」「1週間に1度だけ受け取れる報酬」など、ゲーム内で頻出する回数制限を一元的に管理できます。

graph LR
  Action["プレイヤーの行動"] --> CountUp["カウントアップ (maxValue 指定)"]
  CountUp -- 上限に達していない --> Success["処理を許可"]
  CountUp -- 上限に達している --> Failure["処理を拒否"]
  Reset["リセット周期"] --> Counter["カウンターを 0 に戻す"]

カウンター

プレイヤーの行動回数を表現するためのエンティティで、 カウンターの値を上昇する際に許容可能な最大値を指定してカウントアップを試みることで最大値を超える場合はカウントアップが失敗し、後続の処理を失敗させることができるメカニズムで回数制限を実現します。 このとき、カウンターに対して最大値が存在するのではなく、カウントアップアクションに最大値が設定できるのが特徴です。

たとえば、スタミナの回復処理を例に考えてみます。 多くのゲームでは1日にスタミナを回復できる回数には上限が設けられています。 そして、回復をすればするほど、回復に必要な金額が上昇していきます。

このような仕様は以下のように表現できます。

ティアー 回復に必要なコスト 実行できる回数
Tier.1 5 10
Tier.2 10 10
Tier.3 20 10
Tier.4 40 10

そして、これを 回数制限のカウントアップアクションと、回復に必要なコストとして捉えたのが以下です。

ティアー カウンター名 カウンターの上昇量 カウンターの最大値 回復に必要なコスト
Tier.1 RecoveryStaminaCounter 1 10 5
Tier.2 RecoveryStaminaCounter 1 20 10
Tier.3 RecoveryStaminaCounter 1 30 20
Tier.4 RecoveryStaminaCounter 1 40 40

全てのティアーで同じカウンターを使用し、回復に必要なコストが安いものほど、カウンターの最大値を低く設定しています。

こうすることで、真っ先に一番安い Tier.1 が購入できなくなり、Tier.2 を購入せざるを得なくなり Tier.2 もそのうち購入できなくなり、Tier.3 を購入せざるを得なくなるように設計ができます。

カウンターのリセット

カウンターにはリセット周期を設定可能です。 リセット周期には以下の種類が存在します。

resetType 説明
notReset リセットしない(手動でリセットしない限り永続的にカウントが残ります)
daily 毎日 resetHour 時にリセット
weekly 毎週 resetDayOfWeek 曜日の resetHour 時にリセット
monthly 毎月 resetDayOfMonth 日の resetHour 時にリセット
days 基準日(anchorTimestamp)から days 日ごとにリセット

anchorTimestamp を活用すると、イベント開始日からN日ごとのリセットといった柔軟なスケジューリングが可能です。

バフによる補正

GS2-Buff と連携すると、CountUp/CountUpByUserIdmaxValue をバフで補正して一時的に上限値を増減できます。 「期間限定で1日に引けるガチャ回数を倍にする」といった、イベントに合わせた緩和措置を実装する際に有用です。

スクリプトトリガー

ネームスペースに countUpScript を設定すると、カウントアップ処理の前後でカスタムスクリプトを実行できます。 トリガーは同期・非同期の実行方式を選択でき、非同期処理では GS2-Script や Amazon EventBridge を利用した外部連携も可能です。

設定できる主なイベントトリガーとスクリプト設定名は以下の通りです。

  • countUpScript(完了通知: countUpDone): カウントアップ処理の前後。

トランザクションアクション

GS2-Limit では以下のトランザクションアクションを提供しています。

種別 アクション 説明
検証 Gs2Limit:VerifyCounterByUserId カウンター値の検証(一致/未満/より大きい など)
消費 Gs2Limit:CountUpByUserId カウンター値の加算(カウントアップ)
入手 Gs2Limit:CountDownByUserId カウンター値の減算(カウントダウン)
入手 Gs2Limit:DeleteCounterByUserId カウンターの削除(リセット)

「カウンター値の減算(カウントダウン)」を入手アクションとして利用することで、特定のアイテムを入手した際や、ミッション達成の報酬として、制限回数を回復(実質的に、消費した回数を差し戻す)させるといった処理が可能になります。これにより、プレイヤーの継続的なプレイを促進するような報酬設計が容易になります。

マスターデータ運用

マスターデータを登録することでマイクロサービスで利用可能なデータや振る舞いを設定できます。

マスターデータの種類には以下があります。

  • LimitModel: リセット周期と上限値

以下はマスターデータの JSON 例です。

{
  "version": "2023-09-04",
  "limitModels": [
    {
      "name": "daily",
      "metadata": "デイリー制限",
      "resetType": "daily",
      "resetHour": 5
    },
    {
      "name": "weekly",
      "metadata": "ウィークリー制限",
      "resetType": "weekly",
      "resetDayOfWeek": "monday",
      "resetHour": 5
    }
  ]
}

マスターデータの登録はマネージメントコンソールから登録する他、GitHubからデータを反映したり、GS2-Deployを使ってCIから登録するようなワークフローを組むことが可能です。

実装例

カウンターの一覧を取得

    var items = await gs2.Limit.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).CountersAsync(
    ).ToListAsync();
    const auto Domain = Gs2->Limit->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto It = Domain->Counters( // limitName
    );
    TArray<Gs2::UE5::Limit::Model::FEzCounterPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

カウンターの状態を取得

    var item = await gs2.Limit.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Counter(
        limitName: "daily",
        counterName: "counter1"
    ).ModelAsync();
    const auto Domain = Gs2->Limit->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Counter(
        "daily", // limitName
        "counter1" // counterName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

カウンターの値を上昇

このAPIでカウンターの上昇処理を行うことは推奨していません。 GS2-Exchange / GS2-Showcase / GS2-Quest といった回数制限を設けたい対象の処理を実行するための対価にカウンター値の上昇を設定することを推奨します。

maxValue を超えるカウントアップを試みると OverflowException が発生します。 クライアント側でこの例外を捕捉して「本日の上限に達しました」といった UI を出すことで、回数制限を表現できます。

    try {
        var result = await gs2.Limit.Namespace(
            namespaceName: "namespace-0001"
        ).Me(
            gameSession: GameSession
        ).Counter(
            limitName: "daily",
            counterName: "counter1"
        ).CountUpAsync(
            countUpValue: 1,
            maxValue: 100
        );
    } catch (Gs2.Gs2Limit.Exception.CountUpOverflowException e) {
        // The maximum number of times limit has been reached.
    }
    const auto Domain = Gs2->Limit->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Counter(
        "daily", // limitName
        "counter1" // counterName
    );
    const auto Future = Domain->CountUp(
        1, // countUpValue
        100 // maxValue
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        auto e = Future->GetTask().Error();
        if (e->IsChildOf(Gs2::Limit::Error::FOverflowError::Class))
        {
            // The maximum number of times limit has been reached.
        }
        return false;
    }

    // obtain changed values / result values
    const auto Future2 = Future->GetTask().Result()->Model();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
    const auto Result = Future2->GetTask().Result();

カウンターの強制リセット

カウンターの強制リセットはゲームエンジン用の SDK では処理できません。 ゲームサーバーから DeleteCounterByUserId を呼び出すか、トランザクションの入手アクションとして Gs2Limit:DeleteCounterByUserId を実行することでカウンターをリセットできます。

詳細なリファレンス