メインコンテンツまでスキップ

SDKを使用してチャレンジを取得し報酬を請求する - チャレンジ - (Unityモジュール)

Last updated on February 4, 2026

注釈:本資料はAI技術を用いて翻訳されています。

ラッパーを開く

このチュートリアルでは、AGS Software Development Kit (SDK) を使用してAccelByte Gaming Services (AGS) Challengeを実装し、チャレンジ機能を統合する方法を学びます。Byte Warsには、ChallengeEssentialsWrapperクラスで定義されたゲームインスタンスラッパーがあり、チャレンジ統合のための完全な機能がすでに含まれています。このチュートリアルでは、ChallengeEssentialsWrapper_Starterラッパー(ChallengeEssentialsWrapperクラスのスターターバージョン)を使用して、機能をゼロから実装します。

スターターパックの内容

このチュートリアルに従うために、ChallengeEssentialsWrapper_Starterという名前のスターターラッパークラスが用意されています。このクラスはリソースセクションで入手でき、以下のファイルで構成されています。

  • C#ファイル: Assets/Resources/Modules/Engagement/ChallengeEssentials/Scripts/ChallengeEssentialsWrapper_Starter.cs

また、ヘルパー定数変数と構造体を含むモデルクラスが以下のファイルで定義されています。

  • C#ファイル: Assets/Resources/Modules/Engagement/ChallengeEssentials/Scripts/ChallengeEssentialsModels.cs

ChallengeEssentialsWrapper_Starterクラスには、いくつかの事前定義されたコンポーネントが含まれています。

  • AGS SDKインターフェースを参照するためのヘルパー変数。これらの変数は、ラッパーが初期化されるときに割り当てられます。

    private IClientChallenge challenge;
    private Items items;

    private void Awake()
    {
    challenge = AccelByteSDK.GetClientRegistry().GetApi().GetChallenge();
    items = AccelByteSDK.GetClientRegistry().GetApi().GetItems();
    }

ChallengeEssentialsModelsクラスには、以下のヘルパーが含まれています。

  • チャレンジゴール情報を保存するためのChallengeGoalDataという名前のヘルパークラス。このクラスは、UIコンポーネントと共に使用して、チャレンジゴールのリストを表示します。

    [Serializable]
    public struct ChallengeGoalData
    {
    public ChallengeGoalMeta Meta;
    public GoalProgressionInfo Progress;
    public List<ChallengeGoalRewardData> Rewards;
    public string EndDateTime;

    public string EndTimeDuration
    {
    get
    {
    if (!DateTime.TryParse(
    EndDateTime,
    null,
    System.Globalization.DateTimeStyles.AdjustToUniversal,
    out DateTime parsedEndDateTime))
    {
    return string.Empty;
    }

    TimeSpan duration = parsedEndDateTime - DateTime.UtcNow;
    if (duration.TotalMinutes < 1)
    {
    return "< 1m";
    }

    string result = string.Empty;
    if (duration.Days > 0)
    {
    result += $"{duration.Days}d ";
    }
    if (durationours > 0)
    {
    result += $"{durationours}h ";
    }
    if (duration.Minutes > 0)
    {
    result += $"{duration.Minutes}m";
    }

    return result.Trim();
    }
    }
    }
  • チャレンジ報酬情報を保存するためのChallengeGoalRewardDataという名前のヘルパークラス。このクラスは、UIコンポーネントと共に使用して、報酬のリストを表示します。

    [Serializable]
    public struct ChallengeGoalRewardData
    {
    public ChallengeReward Reward;
    public ItemInfo ItemInfo;
    }
  • バックエンドレスポンスから文字列データを変換するためのヘルパーenum。

    [JsonConverter(typeof(StringEnumConverter)), Serializable]
    public enum ChallengeRotation
    {
    [Description("NONE"), EnumMember(Value = "NONE")]
    None,

    [Description("DAILY"), EnumMember(Value = "DAILY")]
    Daily,

    [Description("WEEKLY"), EnumMember(Value = "WEEKLY")]
    Weekly,

    [Description("MONTHLY"), EnumMember(Value = "MONTHLY")]
    Monthly,

    [Description("CUSTOM"), EnumMember(Value = "CUSTOM")]
    Custom
    }

    [JsonConverter(typeof(StringEnumConverter)), Serializable]
    public enum ChallengeGoalProgressStatus
    {
    [Description("NONE"), EnumMember(Value = "NONE")]
    None,

    [Description("ACTIVE"), EnumMember(Value = "ACTIVE")]
    Active,

    [Description("COMPLETED"), EnumMember(Value = "COMPLETED")]
    Completed,

    [Description("RETIRED"), EnumMember(Value = "RETIRED")]
    Retired,

    [Description("NOT_STARTED"), EnumMember(Value = "NOT_STARTED")]
    Not_Started
    };
  • ログ記録とメッセージ表示のための定数。

    public const string EmptyChallengeMessage = "No Challenge Found";
    public const string EmptyClaimableChallengeRewardMessage = "No Claimable Challenge Reward Found";
    public const string ClaimedChallengeRewardLabel = "Claimed";
    public const string ClaimableChallengeRewardLabel = "Claim";
    public const string ClaimingChallengeRewardLabel = "Claiming";
    public const string AllTimeChallengeTitleLabel = "All Time Challenges";
    public const string PeriodicChallengeTitleLabel = "{0} Challenges";

期間別にチャレンジを取得する

以前、Admin Portalでチャレンジとそのローテーション/期間を設定しました。このセクションでは、選択した期間に基づいてチャレンジ情報を取得する機能を実装する方法を学びます。

  1. ChallengeEssentialsWrapper_Starterクラスを開き、以下の関数を作成します。この関数は、利用可能なすべてのチャレンジを取得するリクエストを送信し、指定された期間に一致するものをフィルタリングします。

    public void GetChallengeByPeriod(
    ChallengeRotation period,
    ResultCallback<ChallengeResponseInfo> onComplete)
    {
    challenge.GetChallenges((Result<ChallengeResponse> result) =>
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to get challenge. Error {result.Error.Code}: {result.Error.Message}");
    onComplete?.Invoke(Result<ChallengeResponseInfo>.CreateError(result.Error));
    return;
    }

    foreach (ChallengeResponseInfo challengeInfo in result.Value.Data)
    {
    // Skip inactive challenge.
    if (challengeInfo.Status.ToLower() == ChallengeStatus.Retired.ToString().ToLower())
    {
    continue;
    }

    // Challenge codes in Byte Wars use the <engine>-<period> format, e.g., unity-alltime or unity-weekly.
    if (challengeInfo.Code.Contains("unity", System.StringComparison.OrdinalIgnoreCase) &&
    challengeInfo.Rotation.ToLower() == period.ToString().ToLower())
    {
    BytewarsLogger.Log($"Success to get challenge with {period} period. Challenge code: {challengeInfo.Code}");
    onComplete?.Invoke(Result<ChallengeResponseInfo>.CreateOk(challengeInfo));
    return;
    }
    }

    BytewarsLogger.LogWarning($"Failed to get challenge. No challenge found with {period} period.");
    onComplete?.Invoke(Result<ChallengeResponseInfo>.CreateError(ErrorCode.NotFound, EmptyChallengeMessage));
    });
    }
  2. 上記の関数によって返される結果には、チャレンジ名やチャレンジコードなどのチャレンジ情報が含まれます。

チャレンジゴールを取得する

このセクションでは、チャレンジゴール、その進捗状況、および関連する報酬を取得する方法を学びます。

  1. ChallengeEssentialsWrapper_Starterクラスを開きます。報酬アイテム情報をクエリするための関数を準備しましょう。まず、エンドポイント呼び出しを最小限に抑えるために、報酬アイテム情報をキャッシュに保存する新しいヘルパー変数を作成します。

    private Dictionary<string /*sku*/, ItemInfo /*info*/> cachedRewardItemInfos = new();
  2. 以下の関数を作成して、SKUごとに報酬アイテム情報をクエリするリクエストを1つずつ送信します。各アイテム情報は、以前に定義したヘルパー変数を使用してキャッシュに保存されます。

    private void QueryRewardItemsBySkusRecursively(
    List<string> itemSkusToQuery,
    List<ChallengeGoalData> goals,
    ResultCallback<List<ChallengeGoalData>> onComplete)
    {
    if (itemSkusToQuery.Count <= 0)
    {
    BytewarsLogger.Log("Successfully queried reward item info by SKUs.");
    OnQueryRewardItemsInformationComplete(Result.CreateOk(), goals, onComplete);
    return;
    }

    string currentSku = itemSkusToQuery[0];
    itemSkusToQuery.RemoveAt(0);

    items.GetItemBySku(
    currentSku,
    string.Empty,
    string.Empty,
    (Result<ItemInfo> result) =>
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning(
    $"Failed to query reward item by SKU '{currentSku}'. " +
    $"Error {result.Error.Code}: {result.Error.Message}");
    onComplete?.Invoke(Result<List<ChallengeGoalData>>.CreateError(result.Error));
    return;
    }

    // Store the info in the cache
    cachedRewardItemInfos.Add(result.Value.sku, result.Value);

    // Continue with the next SKU
    QueryRewardItemsBySkusRecursively(itemSkusToQuery, goals, onComplete);
    });
    }
  3. 以下の関数を作成して、チャレンジ報酬アイテム情報をクエリします。この関数は、報酬アイテム情報がすでにキャッシュされているかどうかを確認します。キャッシュされていない場合は、QueryRewardItemsBySkusRecursively()を使用して報酬アイテムの詳細を取得するリクエストを送信します。レスポンスはOnQueryRewardItemsInformationComplete()によって処理されます。

    private void QueryRewardItemsInformation(
    List<ChallengeGoalData> goals,
    ResultCallback<List<ChallengeGoalData>> onComplete)
    {
    // Collect reward item SKUs to query.
    List<string> rewardItemSkusToQuery = goals
    .SelectMany(x => x.Meta.Rewards)
    .Select(x => x.ItemId)
    .ToList();

    // Return success if all reward items are already cached.
    rewardItemSkusToQuery = rewardItemSkusToQuery.Except(cachedRewardItemInfos.Keys).ToList();
    if (rewardItemSkusToQuery.Count <= 0)
    {
    OnQueryRewardItemsInformationComplete(Result.CreateOk(), goals, onComplete);
    return;
    }

    // Query reward item information by SKUs recursively.
    cachedRewardItemInfos.Clear();
    QueryRewardItemsBySkusRecursively(rewardItemSkusToQuery, goals, onComplete);
    }
  4. 次に、OnQueryRewardItemsInformationComplete()コールバック関数を作成します。この関数は、SKU、名前、アイコン、数量などの報酬情報を収集し、結果を割り当てられたデリゲートに渡します。

    private void OnQueryRewardItemsInformationComplete(
    Result queryResult,
    List<ChallengeGoalData> goals,
    ResultCallback<List<ChallengeGoalData>> onComplete)
    {
    if (queryResult.IsError)
    {
    BytewarsLogger.LogWarning(
    $"Failed to query reward item info. " +
    $"Error {queryResult.Error.Code}: {queryResult.Error.Message}");
    onComplete?.Invoke(Result<List<ChallengeGoalData>>.CreateError(queryResult.Error));
    return;
    }

    // Construct goal reward data based on the queried information.
    foreach (ChallengeGoalData goal in goals)
    {
    foreach (ChallengeReward reward in goal.Meta.Rewards)
    {
    cachedRewardItemInfos.TryGetValue(reward.ItemId, out ItemInfo rewardInfo);
    goal.Rewards.Add(new()
    {
    Reward = reward,
    ItemInfo = rewardInfo
    });
    }
    }

    BytewarsLogger.Log("Successfully queried reward item info.");
    onComplete?.Invoke(Result<List<ChallengeGoalData>>.CreateOk(goals));
    }
  5. 報酬アイテム情報をクエリする関数が完成したので、チャレンジゴールリストを取得する関数を作成しましょう。同じファイル内で、以下の関数を作成します。この関数は、プレイヤーのチャレンジ進捗状況を評価するリクエストを送信し、ゴールの進捗状況を取得してから、QueryRewardItemsInformation()を呼び出して報酬アイテムの詳細を取得します。最後に、OnGetChallengeGoalListComplete()を呼び出して結果を返します。

    public void GetChallengeGoalList(
    ChallengeResponseInfo challengeInfo,
    ResultCallback<List<ChallengeGoalData>> onComplete,
    int rotationIndex = 0)
    {
    // Request evaluation to update challenge goal progress.
    challenge.EvaluateChallengeProgress((Result evaluateResult) =>
    {
    if (evaluateResult.IsError)
    {
    OnGetChallengeGoalListComplete(false, evaluateResult.Error, null, onComplete);
    return;
    }

    // Get the goal list and their progress.
    challenge.GetChallengeProgress(
    challengeInfo.Code,
    rotationIndex,
    (Result<GoalProgressionResponse> progressResult) =>
    {
    if (progressResult.IsError)
    {
    OnGetChallengeGoalListComplete(false, progressResult.Error, null, onComplete);
    return;
    }

    // Construct new goal objects and add them to the list.
    List<ChallengeGoalData> goals = new();
    foreach (GoalProgressionInfo progress in progressResult.Value.Data)
    {
    goals.Add(new()
    {
    Meta = progress.Goal,
    Progress = progress,
    Rewards = new(),
    EndDateTime =
    challengeInfo.Rotation.ToLower() == ChallengeRotation.None.ToString().ToLower() ?
    string.Empty : progressResult.Value.Meta.Period.EndTime
    });
    }

    // Query reward item information for all goals.
    QueryRewardItemsInformation(goals, (Result<List<ChallengeGoalData>> queryResult) =>
    {
    // Operation complete, return the result.
    OnGetChallengeGoalListComplete(queryResult.IsError, queryResult.Error, queryResult.Value, onComplete);
    });
    });
    });
    }
  6. 最後に、OnGetChallengeGoalListComplete()コールバック関数を作成します。これは、ゴール取得プロセスの最後のステップです。完了状況と請求済み報酬に基づいてゴールリストをソートし、割り当てられたデリゲートを使用して結果を返します。

    private void OnGetChallengeGoalListComplete(
    bool isError,
    Error error,
    List<ChallengeGoalData> goals,
    ResultCallback<List<ChallengeGoalData>> onComplete)
    {
    if (isError)
    {
    BytewarsLogger.LogWarning($"Failed to get challenge goal list. Error {error.Code}: {error.Message}");
    onComplete?.Invoke(Result<List<ChallengeGoalData>>.CreateError(error));
    return;
    }

    // Sort the result based on unclaimed rewards and completion status.
    goals.Sort((goal1, goal2) =>
    {
    // Goals with unclaimed rewards come first.
    bool goal1HasUnclaimed = (goal1.Progress.ToClaimRewards?.Length ?? 0) > 0;
    bool goal2HasUnclaimed = (goal2.Progress.ToClaimRewards?.Length ?? 0) > 0;
    if (goal1HasUnclaimed != goal2HasUnclaimed)
    {
    return goal1HasUnclaimed ? -1 : 1;
    }

    // Completed goals come before others.
    bool goal1Completed = goal1.Progress.Status.ToLower() == ChallengeGoalProgressStatus.Completed.ToString().ToLower();
    bool goal2Completed = goal2.Progress.Status.ToLower() == ChallengeGoalProgressStatus.Completed.ToString().ToLower();
    if (goal1Completed != goal2Completed)
    {
    return goal1Completed ? -1 : 1;
    }

    return 0;
    });

    BytewarsLogger.Log("Successfully retrieved challenge goal list.");
    onComplete?.Invoke(Result<List<ChallengeGoalData>>.CreateOk(goals));
    }

チャレンジ報酬を請求する

このセクションでは、チャレンジ報酬を請求する方法を学びます。

  1. ChallengeEssentialsWrapper_Starterクラスを開き、以下の関数を作成します。この関数は、報酬IDのリストを渡して報酬を請求するリクエストを送信します。その後、割り当てられたデリゲートを通じて結果を返します。

    public void ClaimChallengeGoalRewards(
    List<string> rewardIDs,
    ResultCallback onComplete)
    {
    challenge.ClaimReward(rewardIDs.ToArray(), (Result<UserReward[]> result) =>
    {
    if (result.IsError)
    {
    BytewarsLogger.LogWarning($"Failed to claim challenge rewards. Error {result.Error}: {result.Error.Message}");
    onComplete?.Invoke(Result.CreateError(result.Error));
    return;
    }

    if (result.Value.Length <= 0)
    {
    BytewarsLogger.LogWarning("Failed to claim challenge rewards. No claimable reward found.");
    onComplete?.Invoke(Result.CreateError(ErrorCode.NotFound, EmptyClaimableChallengeRewardMessage));
    return;
    }

    BytewarsLogger.Log("Success to claim challenge rewards.");
    onComplete?.Invoke(Result.CreateOk());
    });
    }

リソース