Adding a Modular Armor system to RPG Equipment/Inventory

Hello everyone,
I know this is a subject beyond the scope of what is provided in the tutorials, but since the blog post about this subject is incomplete I would like to move ahead with it and help others stitch together their own modular armor/character creator system though I am having trouble doing so on my own. For starters, I would like to show what I have accomplished so far, so we won’t be working from ground zero.

First, I would like to say that I will be using the Synty Modular Fantasy Hero Character pack found here.

Second, I would like to show that I will be using the armor editor tool & code (who will be doing most of the heavy lifting) provided by BattleDrakeStudios, which is free to download/use for any modular character, not just Synty, and they can be found here in the video description. (Watch parts 1 & 2) This tool allows the Game Designer to modify the default settings of any modular character parts by allowing them to change the color and set specific sections of armor while also allowing the creation of inventory icons for UI purposes.

The issue is trying to link BattleDrakeStudios code to the inventory system we have already made since the Armor creator returns a scriptable object I assumed to just make a receiver in StatsEquipableItem.cs

public class StatsEquipableItem : EquipableItem, IModifierProvider
{

    [SerializeField]
    Modifier[] additiveModifiers;
    [SerializeField]
    Modifier[] percentageModifiers;
    
    public ModularArmor modularArmor;
    // continue as followed...
}

allowing us to add a ModularArmor class object to our items in the inspector as seen here

Then I added modifications to the Equipment.cs script in the AddItem() Method to equip the ModularArmor (most of which was copy/pasted from the BattleDrakeStudios Character Manager Script)

  public void AddItem(EquipLocation slot, EquipableItem item)
    {
        Debug.Assert(item.CanEquip(slot, this));

        equippedItems[slot] = item;
        
       //check for StatsEquipableItem and equip it
        if (item is StatsEquipableItem equipableItem)
        {
            EquipItem(equipableItem);
        }

        if (equipmentUpdated != null)
        {
            equipmentUpdated();
        }
    }
 public void EquipItem(StatsEquipableItem itemToEquip)
    {
        if (itemToEquip.modularArmor != null)
        {
            foreach (var part in itemToEquip.modularArmor.armorParts)
            {
                if (part.partID > -1)
                {
                    characterManager.ActivatePart(part.bodyType, part.partID);
                    ColorPropertyLinker[] armorColors = itemToEquip.modularArmor.armorColors;
                    for (int i = 0; i < armorColors.Length; i++)
                    {
                        characterManager.SetPartColor(part.bodyType, part.partID, armorColors[i].property, armorColors[i].color);
                    }
                }
                else
                {
                    characterManager.DeactivatePart(part.bodyType);
                }
            }
        }
        else return;

    }

Then to remove said Modular armor once it had left the equipment slot

public void RemoveItem(EquipLocation slot)
    {
        //Remove equipped Armor
        RemoveEquippedItem(GetItemInSlot(slot));
        equippedItems.Remove(slot);
       
        if (equipmentUpdated != null)
        {
            equipmentUpdated();
        }
    }
public void RemoveEquippedItem(EquipableItem item)
    {
        if(item is StatsEquipableItem itemToUnequip)
        {
            foreach (var part in itemToUnequip.modularArmor.armorParts)
            {
                 characterManager.DeactivatePart(part.bodyType);
            }
        }
        
    }

The result was successful for the most part as seen here

but once the armor was removed

and when scene was changed

As you can see there are a few things amiss that I have struggled with. 1) Disabling character model parts (head, ears, eyebrows, etc.) to keep them from clipping through the equipment. 2) reactivating parts when removing/switching armor pieces. 3) keeping the equipment inventory saved when passing into other scenes (which I know involves the savable entity, but I am cautious about modifying it)

So, that is what I have made thus far, and I feel like I’m on the right track just unsure how to fix the above issues. I would also like to be able for players to customize their characters (gender, race, eyebrows, hair, facial hair, etc.) at some point and I know that the two subjects are linked. Any help would be appreciated though this may be a subject that will be explored in the soon-to-be-released 3rd person action RPG tutorial, I will share my knowledge to the best of my ability with everyone on the site as a complete tutorial on the blog if allowed.

Thank you for your time.

I took a slightly different approach to this, brewing my own version…

I started with a SyntyStatics class which lists all of the possible body parts.

public static class SyntyStatics
{
    public const string HairColor = "_Color_Hair";
    public const string SkinColor = "_Color_Skin";
    public const string PrimaryColor = "_Color_Primary";
    public const string SecondaryColor = "_Color_Secondary";
    public const string LeatherPrimaryColor = "_Color_Leather_Primary";
    public const string LeatherSecondaryColor = "_Color_Leather_Secondary";
    public const string MetalPrimaryColor = "_Color_Metal_Primary";
    public const string MetalSecondaryColor = "_Color_Metal_Secondary";
    public const string MetalDarkColor = "_Color_Metal_Dark";
    public static readonly string HeadCoverings_Base_Hair = "HeadCoverings_Base_Hair";
    public static readonly string HeadCoverings_No_FacialHair = "HeadCoverings_No_FacialHair";
    public static readonly string HeadCoverings_No_Hair = "HeadCoverings_No_Hair";
    public static readonly string All_01_Hair = "All_01_Hair";
    public static readonly string Helmet = "Helmet";
    public static readonly string Back_Attachment = "All_04_Back_Attachment";
    public static readonly string Shoulder_Attachment_Right = "All_05_Shoulder_Attachment_Right";
    public static readonly string Shoulder_Attachment_Left = "All_06_Shoulder_Attachment_Left";
    public static readonly string Elbow_Attachment_Right = "All_07_Elbow_Attachment_Right";
    public static readonly string Elbow_Attachment_Left = "All_08_Elbow_Attachment_Left";
    public static readonly string Hips_Attachment = "All_09_HipsAttachment";
    public static readonly string Knee_Attachment_Right = "All_10_Knee_Attachement_Right";
    public static readonly string Knee_Attachment_Left = "All_11_Knee_Attachement_Left";
    public static readonly string Elf_Ear = "Elf_Ear";

    public static readonly string[] AllGenderBodyParts = new string[]
                                                         {
                                                             "HeadCoverings_Base_Hair",
                                                             "HeadCoverings_No_FacialHair",
                                                             "HeadCoverings_No_Hair",
                                                             "All_01_Hair",
                                                             "Helmet",
                                                             "All_04_Back_Attachment",
                                                             "All_05_Shoulder_Attachment_Right",
                                                             "All_06_Shoulder_Attachment_Left",
                                                             "All_07_Elbow_Attachment_Right",
                                                             "All_08_Elbow_Attachment_Left",
                                                             "All_09_Hips_Attachment",
                                                             "All_10_Knee_Attachement_Right",
                                                             "All_11_Knee_Attachement_Left",
                                                             "Elf_Ear"
                                                         };

    public static readonly string Female_Head_All_Elements = "Female_Head_All_Elements";
    public static readonly string Female_Head_NoElements = "Female_Head_No_Elements";
    public static readonly string Female_Eyebrows = "Female_01_Eyebrows";
    public static readonly string Female_Torso = "Female_03_Torso";
    public static readonly string Female_Arm_Upper_Right = "Female_04_Arm_Upper_Right";
    public static readonly string Female_Arm_Upper_Left = "Female_05_Arm_Upper_Left";
    public static readonly string Female_Arm_Lower_Right = "Female_06_Arm_Lower_Right";
    public static readonly string Female_Arm_Lower_Left = "Female_07_Arm_Lower_Left";
    public static readonly string Female_Hand_Right = "Female_08_Hand_Right";
    public static readonly string Female_Hand_Left = "Female_09_Hand_Left";
    public static readonly string Female_Hips = "Female_10_Hips";
    public static readonly string Female_Leg_Right = "Female_11_Leg_Right";
    public static readonly string Female_Leg_Left = "Female_12_Leg_Left";

    public static readonly string[] FemaleBodyCategories = new string[]
                                                           {
                                                               "Female_Head_All_Elements",
                                                               "Female_Head_No_Elements",
                                                               "Female_01_Eyebrows",
                                                               "Female_03_Torso",
                                                               "Female_04_Arm_Upper_Right",
                                                               "Female_05_Arm_Upper_Left",
                                                               "Female_06_Arm_Lower_Right",
                                                               "Female_07_Arm_Lower_Left",
                                                               "Female_08_Hand_Right",
                                                               "Female_09_Hand_Left",
                                                               "Female_10_Hips",
                                                               "Female_11_Leg_Right",
                                                               "Female_12_Leg_Left",
                                                           };

    public static readonly string Male_Head_All_Elements = "Male_Head_All_Elements";
    public static readonly string Male_Head_No_Elements = "Male_Head_No_Elements";
    public static readonly string Male_Eyebrows = "Male_01_Eyebrows";
    public static readonly string Male_FacialHair = "Male_02_FacialHair";
    public static readonly string Male_Torso = "Male_03_Torso";
    public static readonly string Male_Arm_Upper_Right = "Male_04_Arm_Upper_Right";
    public static readonly string Male_Arm_Upper_Left = "Male_05_Arm_Upper_Left";
    public static readonly string Male_Arm_Lower_Right = "Male_06_Arm_Lower_Right";
    public static readonly string Male_Arm_Lower_Left = "Male_07_Arm_Lower_Left";
    public static readonly string Male_Hand_Right = "Male_08_Hand_Right";
    public static readonly string Male_Hand_Left = "Male_09_Hand_Left";
    public static readonly string Male_Hips = "Male_10_Hips";
    public static readonly string Male_Leg_Right = "Male_11_Leg_Right";
    public static readonly string Male_Leg_Left = "Male_12_Leg_Left";

    public static readonly string[] MaleBodyCategories = new string[]
                                                         {
                                                             "Male_Head_All_Elements",
                                                             "Male_Head_No_Elements",
                                                             "Male_01_Eyebrows",
                                                             "Male_02_FacialHair",
                                                             "Male_03_Torso",
                                                             "Male_04_Arm_Upper_Right",
                                                             "Male_05_Arm_Upper_Left",
                                                             "Male_06_Arm_Lower_Right",
                                                             "Male_07_Arm_Lower_Left",
                                                             "Male_08_Hand_Right",
                                                             "Male_09_Hand_Left",
                                                             "Male_10_Hips",
                                                             "Male_11_Leg_Right",
                                                             "Male_12_Leg_Left",
                                                         };

    public static string[] GearColors = new string[]
                                        {
                                            PrimaryColor,
                                            SecondaryColor,
                                            LeatherPrimaryColor,
                                            LeatherSecondaryColor,
                                            MetalPrimaryColor,
                                            MetalSecondaryColor,
                                            MetalDarkColor
                                        };
}

Next, i made a SyntyEquipableItem Scriptable Object

using System.Collections.Generic;
using System.Linq;
using GameDevTV.Inventories;
using UnityEditor;
using UnityEngine;

namespace TkrainDesigns.Inventories
{
    [CreateAssetMenu(menuName = "Equipment/SyntyEquipableItem", fileName = "SyntyEquipableItem", order = 0)]
    public class SyntyEquipableItem : StatsEquipableItem
    {
        [System.Serializable]
        public class ItemPair
        {
            public string category = "";
            public int index;
        }

        [System.Serializable]
        public class ItemColor
        {
            public string category = "";
            public Color color = Color.magenta;
        }

        [Header("The name of the object in the Modular Characters Prefab representing this item.")] [SerializeField]
        List<ItemPair> objectsToActivate = new List<ItemPair>();

        [SerializeField] List<ItemColor> colorChanges = new List<ItemColor>();

        [Header("Slot Categories to deactivate when this item is activated.")] [SerializeField]
        List<string> slotsToDeactivate = new List<string>();

        public List<string> SlotsToDeactivate => slotsToDeactivate;
        public List<ItemPair> ObjectsToActivate => objectsToActivate;
        public List<ItemColor> ColorChangers => colorChanges;
    }
}

Finally, I created a CharacterGenerator class. This class is responsible for drawing the character by pulling data from the Equipment. It starts by activating the default character, then for each SyntyEquipableItem it encounters, it activates the appropriate parts:

using System.Collections.Generic;
using System.Linq;
using GameDevTV.Inventories;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace TkrainDesigns.Inventories
{
    [DisallowMultipleComponent]
    [RequireComponent(typeof(Equipment))]
    public class CharacterGenerator : MonoBehaviour, ISaveable, IJsonSaveable
    {
    #region statics

    #endregion

    #region Fields

        /// <summary>
        /// A dictionary containing all of the modular parts, organized by category.
        /// Use this catalogue, not the static one when actually customizing the character.
        /// </summary>
        Dictionary<string, List<GameObject>> characterGameObjects;

        /// <summary>
        /// A dictionary containing all of the modular parts, organized by category.
        /// Use this catalogue, not the static one when actually customizing the character.
        /// </summary>
        Dictionary<string, List<GameObject>> CharacterGameObjects
        {
            get
            {
                InitGameObjects(); //This will build the dictionary if it hasn't yet been initialized.
                return characterGameObjects;
            }
        }

        [SerializeField] private bool playerClone = false;
        [SerializeField] SyntyStatics.Gender gender = SyntyStatics.Gender.Male;
        [SerializeField] SyntyStatics.Race race = SyntyStatics.Race.Human;
        [Range(-1, 37)] [SerializeField] int hair = 0;
        [Range(-1, 21)] [SerializeField] int head = 0;
        [Range(-1, 6)] [SerializeField] int eyebrow = 0;
        [Range(-1, 17)] [SerializeField] int facialHair = 0;
        [Range(-1, 27)] [SerializeField] int defaultTorso = 1;
        [Range(-1, 20)] [SerializeField] int defaultUpperArm = 0;
        [Range(-1, 17)] [SerializeField] int defaultLowerArm = 0;
        [Range(-1, 16)] [SerializeField] int defaultHand = 0;
        [Range(-1, 27)] [SerializeField] int defaultHips = 0;
        [Range(-1, 18)] [SerializeField] int defaultLeg = 0;
        public bool isMale => gender == SyntyStatics.Gender.Male;
        [SerializeField] private bool pickup = false;
        public bool isPickup => pickup;

        int hairColor = 0;
        int skinColor = 0;

        Equipment equipment;

    #endregion

    #region Initialization

        void Awake()
        {
            playerClone = (CompareTag("PlayerPreview"));
            equipment = playerClone
                ? GameObject.FindWithTag("Player").GetComponent<Equipment>()
                : GetComponent<Equipment>();
            equipment.equipmentUpdated += LoadArmor;
            LoadDefaultCharacter();
        }

        private void Start()
        {
            LoadDefaultCharacter();
            if (pickup)
            {
                Pickup pickup = GetComponent<Pickup>();
                InventoryItem item = pickup.GetItem();
                if (item is EquipableItem equipable)
                    equipment.AddItem(equipable.GetAllowedEquipLocation(), equipable);
            }
        }

        private void GetConfiguration()
        {
            CharacterGenerator otherGenerator = equipment.GetComponent<CharacterGenerator>();
            gender = otherGenerator.gender;
            race = otherGenerator.race;
            hair = otherGenerator.hair;
            head = otherGenerator.head;
            eyebrow = otherGenerator.eyebrow;
            facialHair = otherGenerator.facialHair;
            defaultTorso = otherGenerator.defaultTorso;
            defaultHand = otherGenerator.defaultHand;
            defaultHips = otherGenerator.defaultHips;
            defaultLeg = otherGenerator.defaultLeg;
            defaultLowerArm = otherGenerator.defaultLowerArm;
            defaultUpperArm = otherGenerator.defaultUpperArm;
        }

        public void InitGameObjects()
        {
            if (characterGameObjects != null) return;
            characterGameObjects = new Dictionary<string, List<GameObject>>();
            BuildCharacterGameObjectFromCatalogue(SyntyStatics.AllGenderBodyParts);
            BuildCharacterGameObjectFromCatalogue(SyntyStatics.MaleBodyCategories);
            BuildCharacterGameObjectFromCatalogue(SyntyStatics.FemaleBodyCategories);
        }

        void BuildCharacterGameObjectFromCatalogue(string[] catalogue)
        {
            foreach (string category in catalogue)
            {
                List<GameObject> list = new List<GameObject>();
                Transform t = GetComponentsInChildren<Transform>().FirstOrDefault(x => x.gameObject.name == category);
                if (t)
                {
                    for (int i = 0; i < t.childCount; i++)
                    {
                        Transform tr = t.GetChild(i);
                        if (tr == t) continue;
                        {
                            list.Add(tr.gameObject);
                            tr.gameObject.SetActive(false);
                        }
                    }

                    characterGameObjects[category] = list;
                }
                else
                {
                    Debug.Log($"BuildFromCatalogue - {name} - has no {category} category!");
                }

                if (characterGameObjects.ContainsKey(category))
                {
                    //Debug.Log($"Category {category}, objects {characterGameObjects[category].Count()}");
                }
            }
        }

    #endregion

    #region Character Generation

        /// <summary>
        /// Should only be called when creating the character or from within RestoreState()
        /// </summary>
        /// <param name="female"></param>
        public void SetGender(bool female)
        {
            gender = female ? SyntyStatics.Gender.Female : SyntyStatics.Gender.Male;
            LoadDefaultCharacter();
        }

        /// <summary>
        /// Should only be called when creating the character or from within RestoreState()
        /// </summary>
        /// <param name="index"></param>
        public void SetHairColor(int index)
        {
            if (index >= 0 && index < SyntyStatics.hairColors.Length)
            {
                hairColor = index;
            }

            SetColorInCategory(SyntyStatics.All_01_Hair, SyntyStatics.HairColor, SyntyStatics.hairColors[hairColor]);
            SetColorInCategory(SyntyStatics.Male_FacialHair, SyntyStatics.HairColor,
                SyntyStatics.hairColors[hairColor]);
            SetColorInCategory(SyntyStatics.Female_Eyebrows, SyntyStatics.HairColor,
                SyntyStatics.hairColors[hairColor]);
            SetColorInCategory(SyntyStatics.Male_Eyebrows, SyntyStatics.HairColor, SyntyStatics.hairColors[hairColor]);
        }

        /// <summary>
        /// Should only be called when creating the character.
        /// </summary>
        /// <param name="index"></param>
        public void CycleHairColor(int index)
        {
            hairColor += index;
            if (hairColor < 0) hairColor = SyntyStatics.hairColors.Length - 1;
            hairColor = hairColor % SyntyStatics.hairColors.Length;
            SetHairColor(hairColor);
        }

        /// <summary>
        /// Should only be called when creating the character.
        /// </summary>
        /// <param name="index"></param>
        public void CycleSkinColor(int index)
        {
            skinColor += index;
            if (skinColor < 0) skinColor += SyntyStatics.skinColors.Length - 1;
            skinColor = skinColor % SyntyStatics.skinColors.Length;
            SetSkinColor(skinColor);
        }

        /// <summary>
        /// Should only be called when creating the character.
        /// </summary>
        /// <param name="index"></param>
        public void CycleHairStyle(int index)
        {
            hair += index;
            if (hair < -1) hair = characterGameObjects[SyntyStatics.All_01_Hair].Count - 1;
            hair %= CharacterGameObjects[SyntyStatics.All_01_Hair].Count;
            ActivateHair(hair);
        }

        /// <summary>
        /// Should only be called when creating the character.
        /// </summary>
        /// <param name="index"></param>
        public void CycleFacialHair(int index)
        {
            facialHair += index;
            int maxHair = CharacterGameObjects[SyntyStatics.Male_FacialHair].Count;
            if (facialHair < -1) facialHair = maxHair - 1;
            if (facialHair >= maxHair) facialHair = -1;
            ActivateFacialHair(facialHair);
        }

        /// <summary>
        /// Should only be called when creating the character.
        /// </summary>
        /// <param name="index"></param>
        public void CycleHead(int index)
        {
            head += index;
            if (head < 0) head += CharacterGameObjects[SyntyStatics.Female_Head_All_Elements].Count - 1;
            head %= CharacterGameObjects[SyntyStatics.Female_Head_All_Elements].Count;
            ActivateHead(head);
        }

        /// <summary>
        /// Should only be called when creating the character.
        /// </summary>
        /// <param name="index"></param>
        public void CycleEyebrows(int index)
        {
            eyebrow += index;
            if (eyebrow < 0) eyebrow += CharacterGameObjects[SyntyStatics.Female_Eyebrows].Count - 1;
            eyebrow %= CharacterGameObjects[SyntyStatics.Female_Eyebrows].Count;
            ActivateEyebrows(eyebrow);
        }

        /// <summary>
        /// Should only be called when creating the character.
        /// </summary>
        /// <param name="category"></param>
        /// <param name="shaderVariable"></param>
        /// <param name="colorToSet"></param>
        void SetColorInCategory(string category, string shaderVariable, Color colorToSet)
        {
            if (!CharacterGameObjects.ContainsKey(category)) return;
            foreach (GameObject go in CharacterGameObjects[category])
            {
                Renderer rend = go.GetComponent<Renderer>();
                rend.material.SetColor(shaderVariable, colorToSet);
            }
        }

        /// <summary>
        /// Should only be called when creating the character or from RestoreState
        /// </summary>
        /// <param name="index"></param>
        public void SetSkinColor(int index)
        {
            if (index >= 0 && index < SyntyStatics.skinColors.Length)
            {
                skinColor = index;
            }

            foreach (var pair in CharacterGameObjects)
            {
                SetColorInCategory(pair.Key, "_Color_Skin", SyntyStatics.skinColors[skinColor]);
            }
        }

    #endregion

    #region CharacterActivation

        /// <summary>
        /// This sets the character to the default state, assuming no items in the EquipmentManager. 
        /// </summary>
        public void LoadDefaultCharacter()
        {
            foreach (var pair in CharacterGameObjects)
            {
                foreach (var item in pair.Value)
                {
                    item.SetActive(false);
                }
            }

            if (pickup) return;
            ActivateHair(hair);
            ActivateHead(head);
            ActivateEyebrows(eyebrow);
            ActivateFacialHair(facialHair);
            ActivateTorso(defaultTorso);
            ActivateUpperArm(defaultUpperArm);
            ActivateLowerArm(defaultLowerArm);
            ActivateHand(defaultHand);
            ActivateHips(defaultHips);
            ActivateLeg(defaultLeg);
        }


        public void LoadArmor()
        {
            if (equipment == null) equipment = GetComponent<Equipment>();
            LoadDefaultCharacter();
            foreach (var pair in equipment.EquippedItems)
            {
                if (pair.Value is SyntyEquipableItem item)
                    // Debug.Log(pair.Key.GetDisplayName());
                {
                    foreach (string category in item.SlotsToDeactivate)
                    {
                        DeactivateCategory(category);
                    }

                    var colorChanger = item.ColorChangers;
                    foreach (SyntyEquipableItem.ItemPair itemPair in item.ObjectsToActivate)
                    {
                        //Debug.Log($"{itemPair.category}-{itemPair.index}");
                        switch (itemPair.category)
                        {
                            case "Leg":
                                ActivateLeg(itemPair.index, colorChanger);
                                break;
                            case "Hips":
                                ActivateHips(itemPair.index, colorChanger);
                                break;
                            case "Torso":
                                ActivateTorso(itemPair.index, colorChanger);
                                break;
                            case "UpperArm":
                                ActivateUpperArm(itemPair.index, colorChanger);
                                break;
                            case "LowerArm":
                                ActivateLowerArm(itemPair.index, colorChanger);
                                break;
                            case "Hand":
                                ActivateHand(itemPair.index, colorChanger);
                                break;
                            default:
                                ActivatePart(itemPair.category, itemPair.index, colorChanger);
                                break;
                        }
                    }
                }
            }
        }


        void ActivateLeg(int selector, List<SyntyEquipableItem.ItemColor> colorChanges = null)
        {
            ActivatePart(gender == SyntyStatics.Gender.Male ? SyntyStatics.Male_Leg_Left : SyntyStatics.Female_Leg_Left,
                selector, colorChanges);
            ActivatePart(
                gender == SyntyStatics.Gender.Male ? SyntyStatics.Male_Leg_Right : SyntyStatics.Female_Leg_Right,
                selector, colorChanges);
            DeactivateCategory(isMale ? SyntyStatics.Female_Leg_Left : SyntyStatics.Male_Leg_Left);
            DeactivateCategory(isMale ? SyntyStatics.Female_Leg_Right : SyntyStatics.Male_Leg_Right);
        }

        void ActivateHips(int selector, List<SyntyEquipableItem.ItemColor> colorChanges = null)
        {
            ActivatePart(gender == SyntyStatics.Gender.Male ? SyntyStatics.Male_Hips : SyntyStatics.Female_Hips,
                selector, colorChanges);
            DeactivateCategory(isMale ? SyntyStatics.Female_Hips : SyntyStatics.Male_Hips);
        }

        void ActivateHand(int selector, List<SyntyEquipableItem.ItemColor> colorChanges = null)
        {
            ActivatePart(
                gender == SyntyStatics.Gender.Male ? SyntyStatics.Male_Hand_Right : SyntyStatics.Female_Hand_Right,
                selector, colorChanges);
            ActivatePart(
                gender == SyntyStatics.Gender.Male ? SyntyStatics.Male_Hand_Left : SyntyStatics.Female_Hand_Left,
                selector, colorChanges);
            DeactivateCategory(isMale ? SyntyStatics.Female_Hand_Right : SyntyStatics.Male_Hand_Right);
            DeactivateCategory(isMale ? SyntyStatics.Female_Hand_Left : SyntyStatics.Male_Hand_Left);
        }

        void ActivateLowerArm(int selector, List<SyntyEquipableItem.ItemColor> colorChanges = null)
        {
            ActivatePart(
                gender == SyntyStatics.Gender.Male
                    ? SyntyStatics.Male_Arm_Lower_Right
                    : SyntyStatics.Female_Arm_Lower_Right, selector,
                colorChanges);
            ActivatePart(
                gender == SyntyStatics.Gender.Male
                    ? SyntyStatics.Male_Arm_Lower_Left
                    : SyntyStatics.Female_Arm_Lower_Left, selector,
                colorChanges);
            DeactivateCategory(isMale ? SyntyStatics.Female_Arm_Lower_Right : SyntyStatics.Male_Arm_Lower_Right);
            DeactivateCategory(isMale ? SyntyStatics.Female_Arm_Lower_Left : SyntyStatics.Male_Arm_Lower_Left);
        }

        void ActivateUpperArm(int selector, List<SyntyEquipableItem.ItemColor> colorChanges = null)
        {
            ActivatePart(isMale ? SyntyStatics.Male_Arm_Upper_Right : SyntyStatics.Female_Arm_Upper_Right, selector,
                colorChanges);
            ActivatePart(isMale ? SyntyStatics.Male_Arm_Upper_Left : SyntyStatics.Female_Arm_Upper_Left, selector,
                colorChanges);
            DeactivateCategory(isMale ? SyntyStatics.Female_Arm_Upper_Right : SyntyStatics.Male_Arm_Upper_Right);
            DeactivateCategory(isMale ? SyntyStatics.Female_Arm_Upper_Left : SyntyStatics.Male_Arm_Upper_Left);
        }

        void ActivateTorso(int selector, List<SyntyEquipableItem.ItemColor> colorChanges = null)
        {
            ActivatePart(isMale ? SyntyStatics.Male_Torso : SyntyStatics.Female_Torso, selector, colorChanges);
            DeactivateCategory(isMale ? SyntyStatics.Female_Torso : SyntyStatics.Male_Torso);
        }

        void ActivateFacialHair(int selector)
        {
            if (!isMale)
            {
                DeactivateCategory(SyntyStatics.Male_FacialHair);
                return;
            }

            ActivatePart(SyntyStatics.Male_FacialHair, selector);
        }

        void ActivateEyebrows(int selector)
        {
            ActivatePart(isMale ? SyntyStatics.Male_Eyebrows : SyntyStatics.Female_Eyebrows, selector);
            DeactivateCategory(isMale ? SyntyStatics.Female_Eyebrows : SyntyStatics.Male_Eyebrows);
        }

        void ActivateHead(int selector)
        {
            ActivatePart(isMale ? SyntyStatics.Male_Head_All_Elements : SyntyStatics.Female_Head_All_Elements,
                selector);
            DeactivateCategory(isMale ? SyntyStatics.Female_Head_All_Elements : SyntyStatics.Male_Head_All_Elements);
        }


        void ActivateHair(int selector)
        {
            ActivatePart(SyntyStatics.All_01_Hair, selector);
        }


        private Dictionary<GameObject, Material> materialDict = new Dictionary<GameObject, Material>();

        void ActivatePart(string identifier, int selector, List<SyntyEquipableItem.ItemColor> colorChanges = null)
        {
            if (selector < 0)
            {
                DeactivateCategory(identifier);
                return;
            }

            if (!CharacterGameObjects.ContainsKey(identifier))
            {
                Debug.Log($"{name} - {identifier} not found in dictionary");
                return;
            }

            if ((CharacterGameObjects[identifier].Count < selector))
            {
                Debug.Log($"Index {selector}out of range for {identifier}");
                return;
            }

            DeactivateCategory(identifier);
            GameObject go = CharacterGameObjects[identifier][selector];
            go.SetActive(true);
            if (colorChanges == null) return;
            foreach (var pair in colorChanges)
            {
                SetColor(go, pair.category, pair.color);
            }
        }

        void DeactivateCategory(string identifier)
        {
            if (!CharacterGameObjects.ContainsKey(identifier))
            {
                Debug.LogError($"Category {identifier} not found in database!");
                return;
            }

            foreach (GameObject g in CharacterGameObjects[identifier])
            {
                g.SetActive(false);
            }
        }

    #endregion

    #region StaticDictionary

        /// <summary>
        /// This static dictionary is for a hook for the custom editors for EquipableItem and other Editor windows.
        /// Outside of this, it should not be used as a reference to get/set items on the character because it is
        /// terribly inefficient for this purpose.
        /// </summary>
        static Dictionary<string, List<string>> characterParts;

        public static Dictionary<string, List<string>> CharacterParts
        {
            get
            {
                InitCharacterParts();
                return characterParts;
            }
        }

        public static void InitCharacterParts()
        {
            if (characterParts != null) return;
            GameObject character = Resources.Load<GameObject>("PolyFantasyHeroBase");
            if (character == null) Debug.Log("Unable to find Character!");
            characterParts = new Dictionary<string, List<string>>();
            BuildCategory(SyntyStatics.AllGenderBodyParts, character);
            BuildCategory(SyntyStatics.FemaleBodyCategories, character);
            BuildCategory(SyntyStatics.MaleBodyCategories, character);
            character = null;
        }

        static void BuildCategory(IEnumerable<string> parts, GameObject source)
        {
            foreach (string category in parts)
            {
                List<string> items = new List<string>();
                if (source == null)
                {
                    Debug.Log("Source Not Loaded?");
                }
                else
                {
                    Debug.Log($"Source is {source.name}");
                }

                Debug.Log($"Testing {category}");
                Transform t = source.GetComponentsInChildren<Transform>().First(x => x.gameObject.name == category);
                if (t == null)
                {
                    Debug.Log($"Unable to locate {category}");
                }
                else
                {
                    Debug.Log($"Category {t.name}");
                }

                foreach (Transform tr in t.gameObject.GetComponentsInChildren<Transform>())
                {
                    if (tr == t) continue;
                    GameObject go = tr.gameObject;
                    Debug.Log($"Adding {go.name}");
                    items.Add(go.name);
                }

                characterParts[category] = items;
                Debug.Log(characterParts[category].Count);
            }
        }

    #endregion

    #region ISaveable

        public struct ModularData
        {
            public bool isMale;
            public int hair;
            public int facialHair;
            public int head;
            public int eyebrow;
            public int skinColor;
            public int hairColor;

            public ModularData(bool _isMale, int _hair, int _facialHair, int _head, int _eyebrow, int _skinColor,
                               int _hairColor)
            {
                isMale = _isMale;
                hair = _hair;
                facialHair = _facialHair;
                head = _head;
                eyebrow = _eyebrow;
                skinColor = _skinColor;
                hairColor = _hairColor;
            }
        }

        public JToken CaptureState()
        {
            return JToken.FromObject(new ModularData(isMale, hair, facialHair, head, eyebrow, skinColor, hairColor));
        }

        public JToken CaptureAsJToken()
        {
            return JToken.FromObject(new ModularData(isMale, hair, facialHair, head, eyebrow, skinColor, hairColor));
        }

        public void RestoreState(JToken state)
        {
            equipment.equipmentUpdated -= LoadArmor; //prevent issues
            ModularData data = state.ToObject<ModularData>();
            gender = data.isMale ? SyntyStatics.Gender.Male : SyntyStatics.Gender.Female;
            hair = data.hair;
            facialHair = data.facialHair;
            head = data.head;
            eyebrow = data.eyebrow;
            skinColor = data.skinColor;
            hairColor = data.hairColor;
            SetHairColor(hairColor);
            SetSkinColor(skinColor);
            equipment.equipmentUpdated += LoadArmor;
            Invoke(nameof(LoadArmor), .1f);
        }

        public void RestoreFromJToken(JToken state)
        {
            equipment.equipmentUpdated -= LoadArmor; //prevent issues
            ModularData data = state.ToObject<ModularData>();
            gender = data.isMale ? SyntyStatics.Gender.Male : SyntyStatics.Gender.Female;
            hair = data.hair;
            facialHair = data.facialHair;
            head = data.head;
            eyebrow = data.eyebrow;
            skinColor = data.skinColor;
            hairColor = data.hairColor;
            SetHairColor(hairColor);
            SetSkinColor(skinColor);
            equipment.equipmentUpdated += LoadArmor;
            Invoke(nameof(LoadArmor), .1f);
        }

    #endregion

        /* This section is used by the EquipmentBuilder scene only */

    #region EquipmentBuilder

        public int SetParameter(string parameterString, int value, int i)
        {
            if (!CharacterGameObjects.ContainsKey(parameterString))
                return TryAlternateParameter(parameterString, value, i);
            int available = CharacterGameObjects[parameterString].Count;
            value += i;
            if (value >= available) value = -1;
            else if (value < -1) value = available - 1;
            ActivatePart(parameterString, value);
            return value;
        }

        int TryAlternateParameter(string parameterString, int value, int i)
        {
            switch (parameterString)
            {
                case "Torso":
                    value = CycleValue(SyntyStatics.Male_Torso, value, i);
                    ActivateTorso(value);
                    break;
                case "UpperArm":
                    value = CycleValue(SyntyStatics.Male_Arm_Upper_Left, value, i);
                    ActivateUpperArm(value);
                    break;
                case "LowerArm":
                    value = CycleValue(SyntyStatics.Male_Arm_Lower_Left, value, i);
                    ActivateLowerArm(value);
                    break;
                case "Hand":
                    value = CycleValue(SyntyStatics.Male_Hand_Left, value, i);
                    ActivateHand(value);
                    break;
                case "Hips":
                    value = CycleValue(SyntyStatics.Male_Hips, value, i);
                    ActivateHips(value);
                    break;
                case "Leg":
                    value = CycleValue(SyntyStatics.Male_Leg_Left, value, i);
                    ActivateLeg(value);
                    break;
                default:
                    value = -999;
                    break;
            }

            return value;
        }

        int CycleValue(string parameterString, int value, int i)
        {
            int available = CharacterGameObjects[parameterString].Count;
            value += i + available;
            value %= available;
            return value;
        }

        void SetColor(GameObject item, string parameterString, Color colorToSet)
        {
            //Color colorToSet = Color.white;
            //colorToSet = SyntyStatics.GetColor(parameterString, value);

            {
                Material mat;
                if (materialDict.ContainsKey(item) && materialDict[item] != null)
                {
                    mat = materialDict[item];
                }
                else
                {
                    mat = Instantiate(item.GetComponent<Renderer>().sharedMaterial);
                    item.GetComponent<Renderer>().material = mat;
                    materialDict[item] = mat;
                }

                mat.SetColor(parameterString, colorToSet);
                //item.GetComponent<Renderer>().material.SetColor(parameterString, colorToSet);
            }
        }


        public int CycleColor(string parameterString, int value, int modifier)
        {
            int cycleValue = 0;
            cycleValue = SyntyStatics.GetColorCount(parameterString);

            value += modifier + cycleValue;
            value %= cycleValue;

            foreach (var itemList in CharacterGameObjects.Values)
            {
                foreach (GameObject item in itemList)
                {
                    SetColor(item, parameterString, Color.white);
                }
            }

            return value;
        }

    #endregion
    }
}

That last section is for a character editor scene I was using in one of the games…

2 Likes

Okay, I’ve done my best to apply the scripts you’ve provided. I’ve just hit a few snags that could use a little clearing up.

For the SyntyStatics.cs I added:

public enum Gender { Male, Female}
public enum Race { Human, Elf }

//Hair Color Cache
public static Color blondeColor = new Color(0.3098039f, 0.254902f, 0.1764706f);
//For Testing purposes
public static Color blueColor = new Color(0.0509804f, 0.6745098f, 0.9843138f);

//Skin Color Cache
public static Color whiteSkin = new Color(1f, 0.8000001f, 0.682353f);  
public static Color brownSkin = new Color(0.8196079f, 0.6352941f, 0.4588236f);
public static Color blackSkin = new Color(0.5647059f, 0.4078432f, 0.3137255f); 
public static Color elfSkin = new Color(0.9607844f, 0.7843138f, 0.7294118f);

//For references in CharacterGenerator.cs
public static Color[] hairColors = new Color[]
    {
        blondeColor,
        blueColor
    };
public static Color[] skinColors = new Color[]
    {
        whiteSkin,
        brownSkin,
        blackSkin,
        elfSkin
    };

Then for CharacterGenerator.cs:

public object CaptureState()
{
     return (new ModularData(isMale, hair, facialHair, head, eyebrow, skinColor, hairColor));
 }

public void RestoreState(object state)
 {
            equipment.equipmentUpdated -= LoadArmor; //prevent issues

            ModularData data = (ModularData)state;

            gender = data.isMale ? SyntyStatics.Gender.Male : SyntyStatics.Gender.Female;

            hair = data.hair;

            facialHair = data.facialHair;

            head = data.head;

            eyebrow = data.eyebrow;

            skinColor = data.skinColor;

            hairColor = data.hairColor;

            SetHairColor(hairColor);

            SetSkinColor(skinColor);

            equipment.equipmentUpdated += LoadArmor;

            Invoke(nameof(LoadArmor), .1f);

 }

Now in the LoadArmor(), there is a call for equipment.EquippedItems, when using auto actions created public IEnumerable<object> EquippedItems { get; internal set; } in Equipment.cs. I’m just a little confused about what is supposed to be called in EquippedItems as it’s vital for the rest of LoadArmor().

The other is the SyntyStatics.GetColorCount(parameterString); in CycleColor() and that created

 public static int GetColorCount(string parameterString)
        {
            throw new NotImplementedException();
        }

inside of SyntyStatics.cs and I’m unsure how to fill that method out and what it’s asking for. Other than those two issues everything has been working out.

The CycleColor had to do with the specific implementation I used in an outdated version of the script. That method was meant to be deleted before I pasted it.

I forgot to include the EquippedItems

public Dictionary<EquipLocation, EquipableItem> EquippedItems => equippedItems;
1 Like

Thank You,
With those things out of the way, I’ve been able to construct a working Customization UI w/added customization to body art color (potentially eye color customization as well, haven’t decided).

The issue now is the saving system for every time I go to finalize the character customization and save + go to the next scene (same for when player transitions through a portal in other scenes) I get an error either “Coroutine couldn’t be started because the game object ‘Saving’ was inactive” despite the Saving game object being present and active in the persistent objects prefab or “SerializationException: End of Stream encountered before parsing was completed” and I have my suspicions its because of the RestoreState() not calling the saved data properly in CharacterGenerator.cs, but I’m unsure on how to rewrite it. Either that or something in my SavingWrapper.cs needs to be added/replaced.

public struct ModularData

        {

            public bool isMale;

            public bool isHuman;

            public int hair;

            public int facialHair;

            public int head;

            public int eyebrow;

            public int skinColor;

            public int hairColor;

            public int bodyArtColor;

 public ModularData(bool _isMale, bool _isHuman, int _hair, int _facialHair, int _head, int _eyebrow, int _skinColor, int _hairColor, int _bodyArtColor)

            {

                isMale = _isMale;

                isHuman = _isHuman;

                hair = _hair;

                facialHair = _facialHair;

                head = _head;

                eyebrow = _eyebrow;

                skinColor = _skinColor;

                hairColor = _hairColor;

                bodyArtColor = _bodyArtColor;

            }

        }

public object CaptureState()

        {

            return (new ModularData(isMale, isHuman, hair, facialHair, head, eyebrow, skinColor, hairColor, bodyArtColor));

        }

public void RestoreState(object state)

        {    

            equipment.equipmentUpdated -= LoadArmor; //prevent issues

            ModularData data = (ModularData)state; //<< Most likely source of problem...

            gender = data.isMale ? SyntyStatics.Gender.Male : SyntyStatics.Gender.Female;

            race = data.isHuman ? SyntyStatics.Race.Human : SyntyStatics.Race.Elf;

            hair = data.hair;

            facialHair = data.facialHair;

            head = data.head;

            eyebrow = data.eyebrow;

            skinColor = data.skinColor;

            hairColor = data.hairColor;

            bodyArtColor = data.bodyArtColor;

            SetHairColor(hairColor);

            SetSkinColor(skinColor);

            SetBodyArtColor(bodyArtColor);

            equipment.equipmentUpdated += LoadArmor;

            Invoke(nameof(LoadArmor), .1f);

        }

This will probably be the last thing before fully completing this system integration.
Thank you again for your assistance in making this happen.

That may be my fault… The script I gave you is using my Json saving system to save, and the tag [System.Serializable] isn’t required.
Put

[System.Serializable] 

just before the struct declaration for ModularData. This should rectify the issue (though sadly any save file that was corrupted is still dead)

1 Like

Thank you again,
I was able to nail down the crux of the problem that first, I wasn’t using the Core prefab and was using the Player prefab. Second, there would be two Persistent Objects prefabs in the same scene, one from the prefab dragged in and the other coming in from the Persistent Object Spawner on the Core prefab, with the first one being saved and then deleted while the other continued to the next scene. This meant that I had to delete the Persistent Objects prefab, but also meant I couldn’t access the saving system in the persistent object spawner either.

So to get around this I just used a portal setActive(False) and have the collider engulf the player in the inspector and then rig the “finalize” button to setActive(True) to move onto the next scene with all changes intact. I know this is probably an archaic method of getting the job done, but it has worked out so far.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms