Skip to main content

Use SDK to get challenges and claim rewards - Challenge - (Unity module)

Last updated on July 28, 2025

Unwrap the wrapper

In this tutorial, you will learn how to implement AccelByte Gaming Services (AGS) Challenge using the AGS Software Development Kit (SDK) to integrate challenge features. In Byte Wars, there is a game instance wrapper defined in the ChallengeEssentialsWrapper class, which already includes full functionality for challenge integration. For this tutorial, you will use the ChallengeEssentialsWrapper_Starter wrapper, a starter version of the ChallengeEssentialsWrapper class, to implement the feature from scratch.

What's in the starter pack

To follow this tutorial, a starter wrapper class named ChallengeEssentialsWrapper_Starter has been prepared. This class is available in the Resources section and consists of the following files:

  • C# file: Assets/Resources/Modules/ChallengeEssentials/Scripts/ChallengeEssentialsWrapper_Starter.cs

There is also a model class containing helper constant variables and a struct, defined in the file below:

  • C# file: Assets/Resources/Modules/ChallengeEssentials/Scripts/ChallengeEssentialsModels.cs

The ChallengeEssentialsWrapper_Starter class includes several predefined components:

  • Helper variables to reference the AGS SDK interfaces. These variables are assigned when the wrapper is initialized:

    private IClientChallenge challenge;
    private Items items;

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

The ChallengeEssentialsModels class includes the following helpers:

  • A helper class named ChallengeGoalData to store challenge goal information. You will use this class with the UI component to display the list of challenge goals.

    [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();
    }
    }
    }
  • A helper class named ChallengeGoalRewardData to store challenge reward information. You will use this class with the UI component to display the list of rewards.

    [Serializable]
    public struct ChallengeGoalRewardData
    {
    public ChallengeReward Reward;
    public ItemInfo ItemInfo;
    }
  • Helper enums to convert string data from backend response.

    [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
    };
  • Constants for logging and displaying messages.

    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";

Get challenge by period

Previously, you configured the challenge and its rotation/period in the Admin Portal. In this section, you will learn how to implement functionality to retrieve challenge information based on the selected period.

  1. Open the ChallengeEssentialsWrapper_Starter class and create the function below. This function sends a request to retrieve all available challenges and filters the one that matches the specified period.

    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. The result returned by the function above includes challenge information such as the challenge name and challenge code.

Get challenge goals

In this section, you will learn how to retrieve challenge goals, their progress, and associated rewards.

  1. Open the ChallengeEssentialsWrapper_Starter class. Let's prepare some functions to query reward item information. First, create a new helper variable to store reward item information in a cache to minimize endpoint calls.

    private Dictionary<string /*sku*/, ItemInfo /*info*/> cachedRewardItemInfos = new();
  2. Create the function below to send a request to query reward item information by SKU one by one. Each item info is then stored in the cache using the previously defined helper variable.

    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. Create the function below to query challenge reward item information. This function checks whether the reward item information is already cached. If not, it sends a request to retrieve the reward item details using QueryRewardItemsBySkusRecursively(). The response is then handled by 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. Next, create the OnQueryRewardItemsInformationComplete() callback function. This function gathers reward information such as SKU, name, icon, and quantity, then passes the result to the assigned delegate.

    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. Now that the functions to query reward item information are complete, let's create the function to retrieve the challenge goal list. Still in the same file, create the function below. This function sends a request to evaluate the player's challenge progress, retrieves goal progress, and then calls QueryRewardItemsInformation() to fetch reward item details. Finally, it invokes OnGetChallengeGoalListComplete() to return the result.

    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. Finally, create the OnGetChallengeGoalListComplete() callback function. This is the last step in the goal retrieval process. It sorts the goal list based on completion and claimed rewards, then returns the result using the assigned delegate.

    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));
    }

Claim challenge rewards

In this section, you will learn how to claim challenge rewards.

  1. Open the ChallengeEssentialsWrapper_Starter class and create the function below. This function sends a request to claim the rewards by passing a list of reward IDs. It then returns the result through the assigned delegate.

    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());
    });
    }

Resources