using Rukha93.ModularAnimeCharacter.Customization.UI; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Rukha93.ModularAnimeCharacter.Customization { public class CustomizationDemo : MonoBehaviour { public class EquipedItem { public string path; public List instantiatedObjects; public CustomizationItemAsset assetReference; //for material customization public Renderer[] renderers; } [SerializeField] private UICustomizationDemo m_UI; private IAssetLoader m_AssetLoader; private List m_Categories = new List { "body", "head", "hairstyle", "top", "bottom", "shoes", "outfit" }; private Animator m_Character; private SkinnedMeshRenderer m_ReferenceMesh; private Dictionary> m_CustomizationOptions; // private Dictionary m_BodyParts; private Dictionary m_Equiped; private Dictionary m_MaterialProperties; private Coroutine m_LoadingCoroutine; private void Awake() { //init variables m_AssetLoader = GetComponentInChildren(); m_CustomizationOptions = new Dictionary>(); m_Equiped = new Dictionary(); m_BodyParts = new Dictionary(); m_MaterialProperties = new Dictionary(); //ui callbacks m_UI.OnClickCategory += OnSelectCategory; m_UI.OnChangeItem += OnSwapItem; m_UI.OnChangeColor += OnChangeColor; } private void Start() { //init categories UI m_UI.SetCategories(m_Categories.ToArray()); for (int i = 0; i < m_Categories.Count; i++) m_UI.SetCategoryValue(i, ""); m_LoadingCoroutine = StartCoroutine(Co_LoadAndInitBody("f")); } private void InitBody(string path, GameObject prefab) { //instantiate the body prefab and store the animator m_Character = Instantiate(prefab, this.transform).GetComponent(); //get a random body mesh to be used as reference var meshes = GetComponentsInChildren(); m_ReferenceMesh = meshes[meshes.Length / 2]; //initialize all tagged body parts //they be used to disable meshes that are hidden by clothes var bodyparts = m_Character.GetComponentsInChildren(); foreach (var part in bodyparts) m_BodyParts[part.type] = part; var equip = new EquipedItem() { path = path, assetReference = null, instantiatedObjects = new List() { m_Character.gameObject } }; InitRenderersForItem(equip); m_Equiped["body"] = equip; //update ui m_UI.SetCategoryValue(m_Categories.IndexOf("body"), path); if (m_UI.IsCustomizationOpen && m_UI.CurrentCategory == "body") m_UI.SetCustomizationMaterials(equip.renderers); } private IEnumerator Co_LoadAndInitBody(string bodyType) { //destroy old character if (m_Character != null) { Destroy(m_Character.gameObject); //clear old items m_Equiped.Clear(); m_CustomizationOptions.Clear(); m_BodyParts.Clear(); } //init the customization options for the selected body type List coroutines = new List(); for (int i = 0; i < m_Categories.Count; i++) { int index = i; string path = m_Categories[i].Equals("body") ? "body" : GetAssetPath(bodyType, m_Categories[i]); coroutines.Add(StartCoroutine(m_AssetLoader.LoadAssetList(path, res => m_CustomizationOptions[m_Categories[index]] = new List(res)))); } for (int i = 0; i < m_Categories.Count; i++) { yield return coroutines[i]; //add an empty item for all categories that can be empty if (m_Categories[i].Equals("body")) continue; if (m_Categories[i].Equals("head")) continue; m_CustomizationOptions[m_Categories[i]].Insert(0, ""); } //initialize the body var bodyPath = GetAssetPath(bodyType, "body"); yield return m_AssetLoader.LoadAsset(bodyPath, res => InitBody(bodyPath, res)); //initialize the head with the first available string assetPath = m_CustomizationOptions["head"][0]; yield return m_AssetLoader.LoadAsset(assetPath, res => Equip("head", assetPath, res)); m_LoadingCoroutine = null; } #region EQUIPMENT public void Equip(string cat, string path, CustomizationItemAsset item) { //if outfit, remove all othet pieces if(cat.Equals("outfit")) { Unequip("top", false); Unequip("bottom", false); } else if (cat.Equals("top") || cat.Equals("bottom")) { Unequip("outfit", false); } //unequip previous item Unequip(cat, false); EquipedItem equip = new EquipedItem() { path = path, assetReference = item, instantiatedObjects = new List() }; m_Equiped[cat] = equip; //instantiate new meshes, init properties, parent to character GameObject go = null; SkinnedMeshRenderer skinnedMesh = null; foreach(var mesh in item.meshes) { //instantiate new gameobject go = new GameObject(mesh.name); go.transform.SetParent(m_Character.transform, false); m_Equiped[cat].instantiatedObjects.Add(go); //add the renderer skinnedMesh = go.AddComponent(); skinnedMesh.rootBone = m_ReferenceMesh.rootBone; skinnedMesh.bones = m_ReferenceMesh.bones; skinnedMesh.bounds = m_ReferenceMesh.bounds; skinnedMesh.sharedMesh = mesh.sharedMesh; skinnedMesh.sharedMaterials = mesh.sharedMaterials; } //instantiate objects, parent to target bones foreach(var obj in item.objects) { go = Instantiate(obj.prefab, m_Character.GetBoneTransform(obj.targetBone)); equip.instantiatedObjects.Add(go); } //update bodyparts UpdateBodyRenderers(); //map renderers InitRenderersForItem(equip); //update ui m_UI.SetCategoryValue(m_Categories.IndexOf(cat), path); if (m_UI.IsCustomizationOpen && m_UI.CurrentCategory == cat) m_UI.SetCustomizationMaterials(equip.renderers); //send message to the character //used to update the facial blendshape controller and colliders for hair m_Character.SendMessage("OnChangeEquip", new object[] { cat, equip.instantiatedObjects }, SendMessageOptions.DontRequireReceiver); } private IEnumerator Co_LoadAndEquip(string cat, string path) { yield return m_AssetLoader.LoadAsset(path, res => Equip(cat, path, res)); m_LoadingCoroutine = null; } public void Unequip(string category, bool updateRenderers = true) { if (m_Equiped.ContainsKey(category) == false) return; var item = m_Equiped[category]; m_Equiped.Remove(category); //destroy instances foreach (var go in item.instantiatedObjects) Destroy(go); //update body parts if (updateRenderers) UpdateBodyRenderers(); //update UI m_UI.SetCategoryValue(m_Categories.IndexOf(category), ""); if (m_UI.IsCustomizationOpen) m_UI.SetCustomizationMaterials(null); } public void UpdateBodyRenderers() { List disabled = new List(); //get all parts that are hidden by equips foreach (var equip in m_Equiped.Values) { if (equip.assetReference == null) continue; foreach (var part in equip.assetReference.bodyParts) if (!disabled.Contains(part)) disabled.Add(part); } //set active value of each part foreach (var part in m_BodyParts) part.Value.gameObject.SetActive(!disabled.Contains(part.Key)); //todo: maybe move this offset to the CharacterController's center offset or skin width var localPos = m_Character.transform.localPosition; localPos.y = m_Equiped.ContainsKey("shoes") ? 0.02f : 0; m_Character.transform.localPosition = localPos; } private void SyncMaterialChange(Material sharedMaterial, MaterialPropertyBlock newProperties) { //apply the new properties to all renderers sharing the same material foreach (var equip in m_Equiped.Values) { foreach (var renderer in equip.renderers) { for (int i = 0; i < renderer.sharedMaterials.Length; i++) { if (renderer.sharedMaterials[i] != sharedMaterial) continue; renderer.SetPropertyBlock(newProperties, i); } } } } private void SyncNewItemMaterials(Renderer renderer) { //update the new renderers material with the stored properties for (int i = 0; i < renderer.sharedMaterials.Length; i++) { if (m_MaterialProperties.ContainsKey(renderer.sharedMaterials[i]) == false) continue; renderer.SetPropertyBlock(m_MaterialProperties[renderer.sharedMaterials[i]], i); } } #endregion private void InitRenderersForItem(EquipedItem item) { List renderers = new List(); List props = new List(); //get all materials in the instantiated items foreach (var obj in item.instantiatedObjects) renderers.AddRange(obj.GetComponentsInChildren()); item.renderers = renderers.ToArray(); //update the material properties for the new item foreach (var renderer in item.renderers) SyncNewItemMaterials(renderer); } #region HELPERS public string GetAssetPath(string bodyType, string asset) { return $"{bodyType}/{asset}".ToLower(); } public string GetAssetPath(string bodyType, string category, string asset) { return $"{bodyType}/{category}/{asset}".ToLower(); } #endregion #region UI CALLBACKS private void OnSelectCategory(string cat) { if (string.Equals(m_UI.CurrentCategory, cat)) { m_UI.ShowCustomization(false); return; } //init items m_UI.SetCustomizationOptions(cat, m_CustomizationOptions[cat].ToArray(), m_Equiped.ContainsKey(cat) ? m_Equiped[cat].path : ""); //set material options if (m_Equiped.ContainsKey(cat)) m_UI.SetCustomizationMaterials(m_Equiped[cat].renderers); else m_UI.SetCustomizationMaterials(null); //show UI m_UI.ShowCustomization(true); } private void OnSwapItem(string cat, string asset) { //if empty, just unequip the current one if any if (string.IsNullOrEmpty(asset)) { Unequip(cat); return; } //stop loading previous item if (m_LoadingCoroutine != null) StopCoroutine(m_LoadingCoroutine); //load new item if (cat.Equals("body")) m_LoadingCoroutine = StartCoroutine(Co_LoadAndInitBody(asset.StartsWith("m") ? "m" : "f")); else m_LoadingCoroutine = StartCoroutine(Co_LoadAndEquip(cat, asset)); } private void OnChangeColor(Renderer renderer, int materialIndex, string property, Color color) { //update the renderer material property MaterialPropertyBlock block = new MaterialPropertyBlock(); renderer.GetPropertyBlock(block, materialIndex); block.SetColor(property, color); renderer.SetPropertyBlock(block, materialIndex); //store the properties for this material in case a new equip needs it var sharedMaterial = renderer.sharedMaterials[materialIndex]; m_MaterialProperties[sharedMaterial] = block; //update the property block of each renderer sharing the same material SyncMaterialChange(sharedMaterial, block); } #endregion } }