From 0810f42cf4cf660361b8b39bcdf2f28090db7c45 Mon Sep 17 00:00:00 2001 From: Robin <1121080+rYuuk@users.noreply.github.com> Date: Fri, 9 Jun 2023 11:02:53 +0200 Subject: [PATCH 1/3] [SDK-142] Add avatar template requests for default avatars (#28) ## [SDK-142](https://ready-player-me.atlassian.net/browse/SDK-142) ## Changes #### Added - Avatar template API and requests #### Updated - Replace hardcoded id with avatar templates in default avatar selection - Fixed blocked loading screen on error --- Runtime/AvatarManager.cs | 30 +++++++ Runtime/WebRequests/AvatarAPIRequests.cs | 79 +++++++++++++++++-- Samples~/Prefabs/DefaultAvatarButton.prefab | 6 +- Samples~/Scripts/Data/AvatarCreatorData.cs | 1 + Samples~/Scripts/UI/AssetTypeUICreator.cs | 6 +- Samples~/Scripts/UI/PanelSwitcher.cs | 13 +-- .../AvatarCreatorSelection.cs | 16 +++- .../UI/SelectionScreens/AvatarSelection.cs | 3 +- .../SelectionScreens/CameraPhotoSelection.cs | 3 +- .../DefaultAvatarSelection.cs | 74 ++++++----------- 10 files changed, 161 insertions(+), 70 deletions(-) diff --git a/Runtime/AvatarManager.cs b/Runtime/AvatarManager.cs index caf632f..3928abf 100644 --- a/Runtime/AvatarManager.cs +++ b/Runtime/AvatarManager.cs @@ -43,6 +43,36 @@ public AvatarManager(BodyType bodyType, OutfitGender gender, AvatarConfig avatar inCreatorAvatarLoader = new InCreatorAvatarLoader(); avatarAPIRequests = new AvatarAPIRequests(ctxSource.Token); } + + /// + /// Create a new avatar from a provided template. + /// + /// Properties which describes avatar + /// Avatar gameObject + public async Task CreateFromTemplateAvatar(AvatarProperties avatarProperties) + { + try + { + avatarProperties = await avatarAPIRequests.CreateFromTemplateAvatar( + avatarProperties.Id, + avatarProperties.Partner, + bodyType + ); + avatarId = avatarProperties.Id; + } + catch (Exception e) + { + OnError?.Invoke(e.Message); + return avatarProperties; + } + + if (ctxSource.IsCancellationRequested) + { + return avatarProperties; + } + + return avatarProperties; + } /// /// Create a new avatar. diff --git a/Runtime/WebRequests/AvatarAPIRequests.cs b/Runtime/WebRequests/AvatarAPIRequests.cs index 6465f33..a088c63 100644 --- a/Runtime/WebRequests/AvatarAPIRequests.cs +++ b/Runtime/WebRequests/AvatarAPIRequests.cs @@ -1,11 +1,13 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using ReadyPlayerMe.AvatarLoader; using ReadyPlayerMe.Core; +using UnityEngine; +using UnityEngine.Networking; namespace ReadyPlayerMe.AvatarCreator { @@ -16,6 +18,15 @@ public class AvatarAPIRequests private const string COLOR_PARAMETERS = "colors?type=skin,beard,hair,eyebrow"; private const string FETCH_AVATAR_PARAMETERS = "?select=id,partner&userId="; private const string DRAFT_PARAMETER = "draft"; + private const string TEMPLATE = "templates"; + private const string FULL_BODY = "fullbody"; + private const string HALF_BODY = "halfbody"; + private const string MALE = "male"; + private const string FEMALE = "female"; + private const string IMAGE_URL = "imageUrl"; + private const string PARTNER = "partner"; + private const string DATA = "data"; + private const string ID = "id"; private readonly AuthorizedRequest authorizedRequest; private readonly CancellationToken ctx; @@ -26,7 +37,7 @@ public AvatarAPIRequests(CancellationToken ctx = default) authorizedRequest = new AuthorizedRequest(); } - public async Task> FetchUserAvatars(string userId) + public async Task> GetUserAvatars(string userId) { var response = await authorizedRequest.SendRequest( new RequestData @@ -39,8 +50,62 @@ public async Task> FetchUserAvatars(string userId) response.ThrowIfError(); var json = JObject.Parse(response.Text); - var data = json["data"]!; - return data.ToDictionary(element => element["id"]!.ToString(), element => element["partner"]!.ToString()); + var data = json[DATA]!; + return data.ToDictionary(element => element[ID]!.ToString(), element => element[PARTNER]!.ToString()); + } + + public async Task> GetTemplates(OutfitGender gender) + { + var response = await authorizedRequest.SendRequest( + new RequestData + { + Url = $"{Endpoints.AVATAR_API_V2}/{TEMPLATE}?gender=" + (gender == OutfitGender.Masculine ? MALE : FEMALE), + Method = HttpMethod.GET, + }, + ctx: ctx + ); + response.ThrowIfError(); + + var json = JObject.Parse(response.Text); + var data = json[DATA]!; + return data.ToDictionary(element => element[ID]!.ToString(), element => element[IMAGE_URL]!.ToString()); + } + + public async Task GetTemplateAvatarImage(string url) + { + var downloadHandler = new DownloadHandlerTexture(); + var webRequestDispatcher = new WebRequestDispatcher(); + var response = await webRequestDispatcher.SendRequest(url, HttpMethod.GET, downloadHandler: downloadHandler, ctx: ctx); + + response.ThrowIfError(); + return response.Texture; + } + + public async Task CreateFromTemplateAvatar(string avatarId, string partner, BodyType bodyType) + { + var payloadData = new Dictionary + { + { nameof(partner), partner }, + { nameof(bodyType), bodyType == BodyType.FullBody ? FULL_BODY : HALF_BODY }, + }; + + var payload = AuthDataConverter.CreatePayload(payloadData); + + var response = await authorizedRequest.SendRequest( + new RequestData + { + Url = $"{Endpoints.AVATAR_API_V2}/templates/{avatarId}", + Method = HttpMethod.POST, + Payload = payload + }, + ctx: ctx + ); + + response.ThrowIfError(); + + var json = JObject.Parse(response.Text); + var data = json[DATA]!.ToString(); + return JsonConvert.DeserializeObject(data); } public async Task GetAllAvatarColors(string avatarId) @@ -72,7 +137,7 @@ public async Task GetAvatarMetadata(string avatarId) response.ThrowIfError(); var json = JObject.Parse(response.Text); - var data = json["data"]!.ToString(); + var data = json[DATA]!.ToString(); return JsonConvert.DeserializeObject(data); } @@ -90,7 +155,7 @@ public async Task CreateNewAvatar(AvatarProperties avatarPrope response.ThrowIfError(); var metadata = JObject.Parse(response.Text); - var data = metadata["data"]!.ToString(); + var data = metadata[DATA]!.ToString(); return JsonConvert.DeserializeObject(data); } diff --git a/Samples~/Prefabs/DefaultAvatarButton.prefab b/Samples~/Prefabs/DefaultAvatarButton.prefab index de79ef8..d18ac6d 100644 --- a/Samples~/Prefabs/DefaultAvatarButton.prefab +++ b/Samples~/Prefabs/DefaultAvatarButton.prefab @@ -35,8 +35,8 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} - m_AnchoredPosition: {x: 0, y: -50} - m_SizeDelta: {x: 400, y: 400} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 200, y: 200} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &3862883088835331388 CanvasRenderer: @@ -108,7 +108,7 @@ RectTransform: - {fileID: 546109656371019473} - {fileID: 8294350063510174813} m_Father: {fileID: 0} - m_RootOrder: 0 + m_RootOrder: -1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} diff --git a/Samples~/Scripts/Data/AvatarCreatorData.cs b/Samples~/Scripts/Data/AvatarCreatorData.cs index c5bc2b9..801b4a1 100644 --- a/Samples~/Scripts/Data/AvatarCreatorData.cs +++ b/Samples~/Scripts/Data/AvatarCreatorData.cs @@ -7,6 +7,7 @@ namespace ReadyPlayerMe public class AvatarCreatorData : ScriptableObject { public AvatarProperties AvatarProperties; + public bool IsExistingAvatar; public void Awake() { diff --git a/Samples~/Scripts/UI/AssetTypeUICreator.cs b/Samples~/Scripts/UI/AssetTypeUICreator.cs index bac903e..ea91029 100644 --- a/Samples~/Scripts/UI/AssetTypeUICreator.cs +++ b/Samples~/Scripts/UI/AssetTypeUICreator.cs @@ -47,7 +47,6 @@ private void Awake() cameraZoom = FindObjectOfType(); } - public void CreateUI(BodyType bodyType, IEnumerable assetTypes) { this.bodyType = bodyType; @@ -93,6 +92,11 @@ public void ResetUI() PanelSwitcher.Clear(); DefaultZoom(); + if (assetTypeButtonsMap == null) + { + return; + } + foreach (var assetTypeButton in assetTypeButtonsMap) { Destroy(assetTypeButton.Value.gameObject); diff --git a/Samples~/Scripts/UI/PanelSwitcher.cs b/Samples~/Scripts/UI/PanelSwitcher.cs index 9e13e50..dd5ee5d 100644 --- a/Samples~/Scripts/UI/PanelSwitcher.cs +++ b/Samples~/Scripts/UI/PanelSwitcher.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using ReadyPlayerMe.AvatarCreator; using UnityEngine; @@ -16,14 +15,15 @@ public static class PanelSwitcher public static void AddPanel(AssetType assetType, GameObject widget) { AssetTypePanelMap ??= new Dictionary(); - if(!AssetTypePanelMap.ContainsKey(assetType)) - { - AssetTypePanelMap.Add(assetType, widget); - } + AssetTypePanelMap.TryAdd(assetType, widget); } public static void Clear() { + if (AssetTypePanelMap == null) + { + return; + } foreach (var assetTypePanels in AssetTypePanelMap) { Object.Destroy(assetTypePanels.Value); @@ -85,11 +85,12 @@ private static void DisableColorPanels() SetActivePanel(AssetType.HairColor, false); SetActivePanel(AssetType.EyebrowColor, false); } + private static void SetActivePanel(AssetType assetType, bool enable) { if (AssetTypePanelMap.ContainsKey(assetType)) { - AssetTypePanelMap[assetType].SetActive(enable); + AssetTypePanelMap[assetType].SetActive(enable); } } } diff --git a/Samples~/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs b/Samples~/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs index 7d60e60..612dd7d 100644 --- a/Samples~/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs +++ b/Samples~/Scripts/UI/SelectionScreens/AvatarCreatorSelection.cs @@ -109,7 +109,11 @@ private async Task LoadAssets() partnerAssetManager.OnError += OnErrorCallback; var assetIconDownloadTasks = await partnerAssetManager.GetAllAssets(); - + if (assetIconDownloadTasks == null) + { + return; + } + CreateUI(AvatarCreatorData.AvatarProperties.BodyType, assetIconDownloadTasks); partnerAssetManager.DownloadAssetsIcon(assetButtonCreator.SetAssetIcons); DebugPanel.AddLogWithDuration("Got all partner assets", Time.time - startTime); @@ -130,7 +134,15 @@ private async Task LoadAvatar() } else { - avatar = await avatarManager.GetAvatar(AvatarCreatorData.AvatarProperties.Id); + if (!AvatarCreatorData.IsExistingAvatar) + { + AvatarCreatorData.AvatarProperties = await avatarManager.CreateFromTemplateAvatar(AvatarCreatorData.AvatarProperties); + avatar = await avatarManager.GetPreviewAvatar(AvatarCreatorData.AvatarProperties.Id); + } + else + { + avatar = await avatarManager.GetAvatar(AvatarCreatorData.AvatarProperties.Id); + } } if (avatar == null) diff --git a/Samples~/Scripts/UI/SelectionScreens/AvatarSelection.cs b/Samples~/Scripts/UI/SelectionScreens/AvatarSelection.cs index 32f56c9..7374b72 100644 --- a/Samples~/Scripts/UI/SelectionScreens/AvatarSelection.cs +++ b/Samples~/Scripts/UI/SelectionScreens/AvatarSelection.cs @@ -47,7 +47,7 @@ private async void CreateAvatarButtons() LoadingManager.EnableLoading(); avatarAPIRequests = new AvatarAPIRequests(); - avatarPartnerMap = await avatarAPIRequests.FetchUserAvatars(AuthManager.UserSession.Id); + avatarPartnerMap = await avatarAPIRequests.GetUserAvatars(AuthManager.UserSession.Id); avatarButtonsMap = new Dictionary(); foreach (var avatar in avatarPartnerMap) @@ -103,6 +103,7 @@ private async void OnCustomize(string avatarId) { AvatarCreatorData.AvatarProperties.Id = avatarId; AvatarCreatorData.AvatarProperties = await avatarAPIRequests.GetAvatarMetadata(avatarId); + AvatarCreatorData.IsExistingAvatar = true; StateMachine.SetState(StateType.Editor); } diff --git a/Samples~/Scripts/UI/SelectionScreens/CameraPhotoSelection.cs b/Samples~/Scripts/UI/SelectionScreens/CameraPhotoSelection.cs index ef9a3f9..0f3232c 100644 --- a/Samples~/Scripts/UI/SelectionScreens/CameraPhotoSelection.cs +++ b/Samples~/Scripts/UI/SelectionScreens/CameraPhotoSelection.cs @@ -75,7 +75,8 @@ private void OnCameraButton() AvatarCreatorData.AvatarProperties.Id = string.Empty; AvatarCreatorData.AvatarProperties.Base64Image = Convert.ToBase64String(bytes); - + AvatarCreatorData.IsExistingAvatar = false; + StateMachine.SetState(StateType.Editor); } } diff --git a/Samples~/Scripts/UI/SelectionScreens/DefaultAvatarSelection.cs b/Samples~/Scripts/UI/SelectionScreens/DefaultAvatarSelection.cs index 9a97573..122ff13 100644 --- a/Samples~/Scripts/UI/SelectionScreens/DefaultAvatarSelection.cs +++ b/Samples~/Scripts/UI/SelectionScreens/DefaultAvatarSelection.cs @@ -13,39 +13,7 @@ namespace ReadyPlayerMe public class DefaultAvatarSelection : State { private const string LOADING_MESSAGE = "Fetching default avatars"; - - private readonly string[] maleAvatarIds = - { - "64229ee84a25835c6ae8a5a4", - "6422a00651c9394af01e94ed", - "6422a0362ec187fafe20d48e", - "6422a08b2ec187fafe20d4ad", - "6422a0be7fc17f5f678cd69b", - "6422a0eb8933ad00c8629d57", - "6422a1242ec187fafe20d4e1", - "6422a14e4a25835c6ae8a682", - "6422a17cc9e8aa39b5d0e6a6", - "6422a1b34e26f24ed304729e", - "6422a1f17fc17f5f678cd722", - "6422a23d8933ad00c8629dfa" - }; - private readonly string[] femaleAvatarIds = - { - "64229ebbc9e8aa39b5d0e598", - "64229f0e4a25835c6ae8a5ba", - "6422a01fc9e8aa39b5d0e620", - "6422a053a9cf14ab7e44d413", - "6422a0a62ec187fafe20d4b3", - "6422a0d47fc17f5f678cd6a2", - "6422a10a51c9394af01e9543", - "6422a13a8933ad00c8629d8c", - "6422a1684e26f24ed3047276", - "6422a1962ec187fafe20d51d", - "6422a1d87fc17f5f678cd716", - "6422a2254a25835c6ae8a6db", - }; - private readonly Dictionary avatarRenderMap = new Dictionary(); [SerializeField] private Transform parent; @@ -53,26 +21,34 @@ public class DefaultAvatarSelection : State public override StateType StateType => StateType.DefaultAvatarSelection; public override StateType NextState => StateType.Editor; - + + private AvatarAPIRequests avatarAPIRequests; private CancellationTokenSource ctxSource; public override async void ActivateState() { LoadingManager.EnableLoading(LOADING_MESSAGE); + if (!AuthManager.IsSignedIn) + { + await AuthManager.LoginAsAnonymous(); + } + + avatarAPIRequests = new AvatarAPIRequests(); + var templateAvatars = await avatarAPIRequests.GetTemplates(AvatarCreatorData.AvatarProperties.Gender); ctxSource = new CancellationTokenSource(); - var avatarIds = AvatarCreatorData.AvatarProperties.Gender == OutfitGender.Feminine ? femaleAvatarIds : maleAvatarIds; + var downloadRenderTasks = new List(); - foreach (var avatarId in avatarIds) + foreach (var template in templateAvatars) { - if (!avatarRenderMap.ContainsKey(avatarId)) + if (!avatarRenderMap.ContainsKey(template.Key)) { - downloadRenderTasks.Add(CreateAvatarRender(avatarId)); + downloadRenderTasks.Add(CreateAvatarRender(template.Key, template.Value)); } else { - avatarRenderMap[avatarId].SetActive(true); + avatarRenderMap[template.Key].SetActive(true); } } @@ -92,33 +68,33 @@ public override void DeactivateState() } } - private async Task CreateAvatarRender(string avatarId) + private async Task CreateAvatarRender(string id, string url) { - Texture2D renderImage; + Texture renderImage; try { - renderImage = await AvatarRenderHelper.GetPortrait(avatarId); + renderImage = await avatarAPIRequests.GetTemplateAvatarImage(url); } catch (Exception e) { Debug.Log(e); return; } - + var button = Instantiate(buttonPrefab, parent); var rawImage = button.GetComponentInChildren(); - button.GetComponent