RPG Character Creator & Modular Armor Tutorial

,

Hello Everyone,
For a while, I’ve been working on a Modular Armor system and Character creator for my RPG project and with the help of @Brian_Trotter I have succeeded with a link to that conversation on this site here. I wish to share my final findings and breaking them down to the best of my ability so that others may do the same in their games in the future. For those following along, I will be using the Synty Modular Fantasy Hero Character pack found on the Unity Asset Store here.

First I’ll start with the Character Creator since it is the largest part of this tutorial and it will be where we will be building up the Dictionaries and Lists that the Armor system will use later. So by the end of this tutorial, you will have a scene in your game that looks like this:

To prep the character creator you will need an empty scene (preferably set after your main menu in the build listing). Place in your Core prefab, your player character must be using the Modular Character prefab provided by the asset pack you are using, disable everything on your player character except the Animator, Collider (whatever shape you may be using, I am using the Character collider in this example), and Savable Entity. Also, remember to disable any other UI elements that you may have attached to your Core prefab, so your HUD and UI Canvas it will not be needed during character creation.

Now we will create a script called CharacterGenerator.cs, attach it to the player character, and place the script in the Inventories namespace. Our first lines of code will look like this;

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

namespace RPG.Inventories
{
    [DisallowMultipleComponent]
    [RequireComponent(typeof(Equipment))]
    public class CharacterGenerator : MonoBehaviour, ISaveable
    {
        #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;
            }
        }

These will be the dictionaries that will hold all of the parts attached to the modular character prefab.

        [SerializeField] private bool playerClone = false;
        //Character Customization
        [SerializeField] SyntyStatics.Gender gender = SyntyStatics.Gender.Male;
        [Range(-1, 37)] [SerializeField] int hair = 0;        
        [Range(-1, 3)] [SerializeField] int ears = -1;
        [Range(-1, 21)] [SerializeField] int head = 0;
        [Range(-1, 6)] [SerializeField] int eyebrow = 0;
        [Range(-1, 17)] [SerializeField] int facialHair = 0;
        //Armor System
        [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;

        //initialize colors for customization
        int hairColor = 0;
        int skinColor = 0;
        int stubbleColor = 0;
        int scarColor = 0;
        int bodyArtColor = 0;
        int eyeColor = 0;

        Equipment equipment; //saving armor changes once the armor system is implemented
        
#endregion

This section is to register all of the available components that are attached to the prefab and options that will be used later.

 #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;
            hair = otherGenerator.hair;
            head = otherGenerator.head;
            ears = otherGenerator.ears;
            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

This is to initialize a default character at the start of the scene so that all of the modular parts are not enabled all at once and to also set up the saving system to save the changes you will make later.

 #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;
            int maxHairStyles = CharacterGameObjects[SyntyStatics.All_01_Hair].Count;
            if (hair < -1) hair = maxHairStyles - 1;
            //hair %= CharacterGameObjects[SyntyStatics.All_01_Hair].Count;
            if(hair >= maxHairStyles) hair = -1;
            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);
        }

        public void CycleEars(int index)
        {
            ears += index;
            int maxEars = CharacterGameObjects[SyntyStatics.Elf_Ear].Count;
            if(ears < -1) ears = maxEars - 1;
            if(ears >= maxEars) ears= -1;
            ActivateElfEars(ears);
        }

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

        public void CycleBodyArtColor(int index)
        {
            bodyArtColor += index;
            if (bodyArtColor < 0) bodyArtColor = SyntyStatics.bodyArtColors.Length - 1;
            bodyArtColor = bodyArtColor % SyntyStatics.bodyArtColors.Length;
            SetBodyArtColor(bodyArtColor);
        }

        public void CycleEyeColor(int index)
        {
            eyeColor += index;
            if(eyeColor < 0) eyeColor = SyntyStatics.eyeColors.Length -1;
            eyeColor = eyeColor % SyntyStatics.eyeColors.Length;
            SetEyeColor(eyeColor);
        }

        /// <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;
            }
            if (index >= 0 && index < SyntyStatics.stubbleColors.Length)
            {
                stubbleColor = index;
            }
            if (index >= 0 && index < SyntyStatics.scarColors.Length)
            {
                scarColor = index;
            }

            foreach (var pair in CharacterGameObjects)
            {
                SetColorInCategory(pair.Key, "_Color_Skin", SyntyStatics.skinColors[skinColor]);
                SetColorInCategory(pair.Key, "_Color_Stubble", SyntyStatics.stubbleColors[stubbleColor]);
                SetColorInCategory(pair.Key, "_Color_Scar", SyntyStatics.scarColors[scarColor]);
            }
        }

        public void SetBodyArtColor(int index)
        {
            if (index >= 0 && index < SyntyStatics.bodyArtColors.Length)
            {
                bodyArtColor = index;
            }
            foreach (var pair in CharacterGameObjects)
            {
                SetColorInCategory(pair.Key, "_Color_BodyArt", SyntyStatics.bodyArtColors[bodyArtColor]);
            }
        }

        public void SetEyeColor(int index)
        {
            if(index >= 0 && index < SyntyStatics.eyeColors.Length)
            {
                eyeColor = index;
            }
            foreach (var pair in CharacterGameObjects)
            {
                SetColorInCategory(pair.Key, "_Color_Eyes", SyntyStatics.eyeColors[eyeColor]);
            }
        }

        #endregion

This section is to set up the cycling system for our buttons in our UI to go through the player options, hairstyle, facial hair, hair color, eye color, etc. (Do not worry about the SyntyStatics.cs script for now.)

#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);
            ActivateElfEars(ears);
            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) // add the line -> public Dictionary<EquipLocation, EquipableItem> EquippedItems => equippedItems; to Equipment.cs under STATE.
            {
                if (pair.Value is SyntyEquipableItem item)
                   //Debug.Log(pair.Key.GetDisplayName());
                {
                   foreach (string category in item.SlotsToDeactivate)
                   {
                       DeactivateCategory(category);
                   }

                   var colorChanger = item.ColorChangers;
                   if(gender == SyntyStatics.Gender.Male)
                   {
                    foreach (SyntyEquipableItem.ItemPair itemPair in item.ObjectsToActivateM)
                   {
                       //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;
                       }
                    }
                    }
                    else if(gender == SyntyStatics.Gender.Female)
                    {
                        foreach (SyntyEquipableItem.ItemPair itemPair in item.ObjectsToActivateF)
                        {
                            //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 ActivateElfEars(int selector)
        {
            ActivatePart(SyntyStatics.Elf_Ear, selector);
        }

        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

This section is mostly for the armor system, but it is for saving and loading gender choices and the parts registered to said gender. As you can see this is where the armor system will be acting like a on/off switch for example if you equip a helmet you will need to turn off the part that is already there and turn on the one that you have equipped, same goes for hands, hips, legs, etc. (Do not worry about the SyntyEquipableItem.cs for the time being as that will be attached to the inventory system)

 #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

This is the Static Dictionary that will be used to access the material attached to the armor parts that we will be manipulating later in our armor system.

 #region ISaveable

        [System.Serializable]
        public struct ModularData
        {
            public bool isMale;
            public int hair;
            public int facialHair;
            public int head;
            public int ears;
            public int eyebrow;
            public int skinColor;
            public int hairColor;
            public int bodyArtColor;
            public int eyeColor;

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

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

        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;
            ears = data.ears;
            eyebrow = data.eyebrow;
            skinColor = data.skinColor;
            hairColor = data.hairColor;
            bodyArtColor = data.bodyArtColor;
            eyeColor = data.eyeColor;
            SetHairColor(hairColor);
            SetSkinColor(skinColor);
            SetBodyArtColor(bodyArtColor);
            SetEyeColor(eyeColor);
            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);
            }
        }

        #endregion

This is the implementation of the ISavable system that we have created in the GameDev series, but for those that use the JSON saving system the Capture and Restore functions for that system can be found here.

Congratulations you have just finished the largest part of this tutorial. Now we must set up our SyntyStatics.cs to cache all of the colors and parts that we are going to be manipulating in both the armor system and character creator. You will need to create the SyntyStatics.cs script in the inventories namespace as well.

using UnityEngine;

namespace RPG.Inventories
{
    public static class SyntyStatics
    {
        public enum Gender { Male, Female}

        public const string HairColor = "_Color_Hair";
        public const string SkinColor = "_Color_Skin";        
        public const string BodyArtColor = "_Color_BodyArt"; 

        //Armor Color Cache
        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";

As you can see these color caches will be familiar to you as they’ve been referenced in our CharacterGenerator.cs and the armor cache is what we will be using in our armor system to change the color of the respective parts so the string names will be important later on.

//Armor Cache
        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",
        };

These are the strings that will be used to identify which parts of our armor need to be enabled or disabled within the armor system and to which gender those parts belong to or if they are universal.

Now we get to the fun part of the character creator, the color cache, where we will be registering colors for our player to use and cycle through.

 //Hair Color Cache
        public static Color blackColor = new Color(0.3098039f, 0.254902f, 0.1764706f);
        public static Color brownColor = new Color(0.333f, 0.162f, 0.014f);
        public static Color dirtyBlondeColor = new Color(0.468f,0.383f, 0.071f);
        public static Color whiteColor = new Color(1f, 1f, 1f);

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

        //Stubble Colors Cache
        public static Color whiteStubble = new Color(0.8039216f, 0.7019608f, 0.6313726f);
        public static Color brownStubble = new Color(0.6588235f, 0.572549f, 0.4627451f);
        public static Color blackStubble = new Color(0.3882353f, 0.2901961f, 0.2470588f);
        public static Color elfStubble = new Color(0.8627452f, 0.7294118f, 0.6862745f);

        //Scar Colors Cache
        public static Color whiteScar = new Color(0.9294118f, 0.6862745f, 0.5921569f);
        public static Color brownScar = new Color(0.6980392f, 0.5450981f, 0.4f);
        public static Color blackScar = new Color(0.4235294f, 0.3176471f, 0.282353f);
        public static Color elfScar = new Color(0.8745099f, 0.6588235f, 0.6313726f);

        //Tattoo Color Cache
        public static Color redColor = new Color (0.823f, 0.064f, 0.035f);
        public static Color blueColor = new Color(0.0509804f, 0.6745098f, 0.9843138f);
        public static Color greenColor = new Color(0.3098039f, 0.7058824f, 0.3137255f);
        public static Color orangeColor = new Color(0.8862745f, 0.39301f, 0.0862745f);
        public static Color yellowColor = new Color(0.8867924f,0.8002203f,0.08784264f);
        public static Color purpleColor = new Color(0.490172f, 0.0862745f, 0.8862745f);
        public static Color pitchBlackColor = new Color(0f, 0f, 0f);

        public static Color[] hairColors = new Color[]
        {
            blackColor,
            brownColor,
            dirtyBlondeColor,
            orangeColor,
            redColor,
            whiteColor,
            blueColor,
            greenColor,
            purpleColor, 
            pitchBlackColor
        };

        public static Color[] skinColors = new Color[]
        {
            whiteSkin,
            brownSkin,
            blackSkin,
            elfSkin
        };
        public static Color[] stubbleColors = new Color[]
        {
            whiteStubble,
            brownStubble,
            blackStubble,
            elfStubble
        };
        public static Color[] scarColors = new Color[]
        {
            whiteScar,
            brownScar,
            blackScar,
            elfScar
        };

        public static Color[] eyeColors = new Color[]
        {
            pitchBlackColor,
            greenColor,
            blueColor,
            yellowColor,
            orangeColor,
            redColor,
            whiteColor
        };

        public static Color[] bodyArtColors = new Color[]
        {
            greenColor,
            redColor,
            blueColor,
            orangeColor,
            yellowColor,
            purpleColor,
            whiteColor,
            pitchBlackColor
        };

Now you may use the colors that I have provided, or you can add more or less to your heart’s desire. If you wish for your players to have green skin, go for it, you just have to create a string reference to the color and then add it to the respective cache list. Note: When searching for color identification numbers you will need to set your color selector window to “RGB 0-1.0” and not the default “RGB 0-255” to copy-paste the correct color identification numbers.

Finally, you will need to create the string reference to the armor colors to manipulate colors in our armor system.

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

And there you go we are now finished with our SyntyStatics.cs script. For our armor system, we will need to create a SyntyEquipableItem.cs script in the inventories namespace.

using System.Collections.Generic;
using UnityEngine;

namespace RPG.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> objectsToActivateMale = new List<ItemPair>();
        [SerializeField]
        List<ItemPair> objectsToActivateFemale = 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> ObjectsToActivateM => objectsToActivateMale;
        public List<ItemPair> ObjectsToActivateF => objectsToActivateFemale;
        public List<ItemColor> ColorChangers => colorChanges;
    }
}

We make the SyntyEquipableItem into a scriptable object subscribed to the StatsEquipableItem from the inventories course and are created by right-clicking in an empty space > Create > Equipment > SyntyEquipableItem.

In the end, you should have something like this:

In the inspector, you can see the armor colors that we cached earlier and can manipulate there. You have to manually type the string reference to the parts you wish to activate and manipulate their color.


To have the parts you want to activate you need to reference the parts as they are listed in the prefab hierarchy for both Male and Female parts(otherwise you could end up with a male torso on a female player). To deactivate parts you will have to register that similar string reference for occasions such as equipping a helmet would require you to deactivate the player’s facial hair, ears, the head, or all of the above while the object is equipped.

To quickly go back to the Character creator for a moment the way you hook up the CharacterGenerator.cs script to the UI for the way this is set up is to add buttons either + or - , ← or → with the on click event add the player character as the object and search for the CharacterGenerator.Cycle(blank) so it should look like this:
Button UI
with a -1 to cycle down or 1 to cycle up. The only exception would be the bool CharacterGenerator.SetGender and from there show/hide the facial hair options panel if the player chooses female. Once the player is finished with customization have them move to the next scene, to do this I added a portal object at the bottom of the player character’s feet enabled by a “Finalize” button. With our saving system in place, all player changes will be saved and then loaded into your game’s next scene.

As a bonus the CharacterGenerator.cs can be used to create random NPCs in your world if you don’t mind them having the same look/equipment as your player and can save you some development time.

So there you go an entire modular character creator and armor customizer. Though this tutorial was shown with Synty assets the methods can be applied to almost any modular package available. I hope this has helped you on your game development journey and thank you for your time reading this.

Special thanks to @Brian_Trotter again for sharing his code to help me get this far and the entire @GameDev.tv crew for their hard work.

3 Likes

Sorry for the necromancing, but you are changing the hair and skin color in the material, on this way every character that will use that material will get the same color changed :thinking:

I guess we need special material for the player… or maybe you mention it and I missed? :sweat_smile:

When running the game, changes to the material are made to an instance of the material, and won’t affect other instances of the material.

1 Like

I was pretty sure that changing the material properties was definitive :thinking: