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.

11 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:

This is absolute fantastic and is going to save a couple weeks time to implement into my project. Ive’ been combing through the code before getting down into the weeds with the implementation and I do have a couple questions, which revolve around what’s happening in Awake and Start. I’m wondering if dealing with the Synty object is just difficult to work around.

I don’t understand why LoadDefaultConfiguration is being called in Awake, Start and then once more in LoadArmor. In addition we’re subscribing to equipment.equipmentUpdate += LoadArmor in Awake, but then unsubscribing and resubscribing in RestoreState and then Invoking LoadArmor on a frame or two delay. Taking the Pickup object out of the equation (I think I am going to remove this and create an equipment injector component), wouldn’t the net effect of all this to just

  1. Call InitiGameObjects in Awake and remove the reference to LoadDefaultCharacter from Awake.
  2. Call LoadDefaultCharacter in Start, then bootstrap LoadArmor in Start. Remove the reference to LoadDefaultCharacter from LoadArmor.
  3. Subscribe once to equipment.equipmentUpdate += LoadArmor in Start (or put this in OnEnable and use a LazyValue/ForceInit for equipment like we do with currentWeapon in Fighter and remove the LoadArmor in Start as well)

It seems like calling LoadDefaultCharacter 3 times for a saved game and 2 times for a new game is a bit redundant.

Edit: Once I started implementing, I discovered why LoadDefaultCharacter is being called repeatedly, but LoadArmor can simplify the calls a bit by taking it out of Awake and RestoreState and moving it into Start, which allows us to move the LoadDefaultCharacter in Start to RestoreState. Really fantastic stuff in this tutorial; completely elevates the polish level of the game. Kudos for sharing this.

        void Awake()
        {
            LoadDefaultCharacter();
        }

        private void Start()
        {
            if (TryGetComponent(out equipment))
            {
                LoadArmor();
                equipment.equipmentUpdated += LoadArmor;
            }
        }

        public void RestoreState(object state)
        {

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

            LoadDefaultCharacter();
        }

1 Like

OK I’m not sure if I missed something here, but how do you connect the SyntyEquipableItem to the system? (I’m guessing the ‘SyntyStatics.cs’ script is an internal database that is internally accessed by the two other scripts, so I won’t ask where this goes). I’m currently facing weird issues, and I have no idea why. This is how my character looks like before I start the game (P.S: I deactivated Synty’s ‘Character Randomizer’ for the time being).:

And this is how it looks like when I start the game:

And when I bring the whole body up, half of it is invisible. This was the case from before we even started… As you can see below:

Can you please help me out? (And one quick last question on the fly… the Synty Equipable Item is called by ‘CharacterGenerator.cs’ through script, right? If not, how do you connect that to the rest of the system)

It’s showing that the Character Randomizer script is currently active. This is Synty’s script, and is useful for random enemies in the game, but completely and totally conflicts with the CharacterGenerator (they’ll both be turning things on and off against each other.

Completely remove Synty’s CharacterGenerator script from the Player.

That, I did… but it’s still acting weird. Everything is concentrated at the head, some meshes are completely invisible, and the character… well… won’t show anything up. Any other suggestions?

I have no explanation for everything being bunched into one spot…

Is the Avatar in the Animator set to the Avatar for the Polygon Fantasy Hero (ModularCharactersAvatar)?

Are there any errors in the inspector?

Yup… that was, somehow, what made it all go wrong. No errors in the inspector, I just didn’t have the right avatar setup. For now, I get a character that shows up properly :slight_smile:

However, my ‘CharacterGenerator.cs’ script is still not working. In other words, when I play with the sliders in the inspector of my game, it does nothing. Any possible solutions for that?

Are you talking about the sliders on the Character Generator component, or the sliders on the canvas in the scene?

I’m talking about the sliders on the Character Generator Component :slight_smile: - I haven’t made any sliders on the canvas in the scene yet, I’m still trying to figure out where to get the assets for these from

Without a custom editor script, these sliders will do nothing when the game is not playing, they just set the values for when you press play.

OK so my idea would be to just set up buttons that cycle through the values, using their Unity Event “OnClick()” function. Is that it, or is there more to it…?

1 Like

After a bit more fiddling, I realized the armor doesn’t have a cycle… Is this expected? Just curious.

How was the Armor handled here, or where is the “LoadArmor()” function called, to be more specific? :slight_smile: (If I have to create my own function, please let me know)

Worst part is, I really don’t even know why in the world do I want to place my armor into the Character Generator… I guess I just want to know how to put armor on at the end of the day, or at least to get it to physically show up on the player… (The character generator was a beautiful add-on I didn’t know I wanted till I implemented it, but at the end of the day, I truly want a solution to be able to put armor on through the equipment slots, which will show up on the player)

especially considering that I still don’t have a way to properly play my game again after this scene (Edit: This was a silly mistake of mine… my camera lost focus on who to follow after the modifications I did)

Oh, and how do you allign your Cinemachine camera with your view in Unity btw? (for this one so far, I turned off the Cinemachine off my Main camera, and it seems to be doing the job just fine)

OK Last question I promise (for this… comment). Can we add a 360 degree view slider that, based on where a circular GUI stays on the slider, the player can rotate the player? Both ends have the same angle, and in the middle the player can rotate his character for the GUI as he desires

And is there a finalize function that loads the final character to the main game when we click it? In other words when it’s clicked, it goes to the main game as the customized character now :slight_smile:

When the game is running, when you drag a piece of armor into the Equipment, the CharacterGenerator will automatically query the Equipment to find any SyntyEquipableIItems on it, and will then turn on/off the correct body parts.

That truly is how it works. In CharacterGenerator.Awake(), there is a line equipment.equipmentUpdated += LoadArmor. Whenever equipment changes, LoadArmor is called and the character is regenerated.

OK I’ll fiddle with the armor then a little bit more, I think I’m starting to see how this all works together

But how about starting a new game, do we use the SavingWrapper’s “NewGame()” function after we finalize our player, or do we create something new?

Here’s my workflow…
First, I have the normal main menu from the Shops and Abilities course
Then, for a new game, I open a new scene for character creation. This scene has just the character and background as well as the character customization buttons.
Once the character is created, I return to the main menu (with a coroutine like Portal, saving the "scene’ with just the character in it), with the new character set as the current game when you get back to the main menu.

So there’s some coding to do as well… OK then, I’ll give this a go and update you along the way of any problems I face :slight_smile:

Mine is just… going to the main game scene once the character is confirmed

OK so here’s a bit of an update… After mindlessly playing around for an hour or so trying to figure out what to do, I am suddenly getting the same error we once had with the respawners that caused my screen to permanently fade to black (after a long loading time wait). I did NOT touch the ‘RespawnManager.cs’ script, if that helps. I did some changes to SavingWrapper and my Hierarchy in my ‘CharacterGenerator’ scene, but I’m not sure what went wrong. Here’s what I did:

  1. I went to “SavingWrapper.cs”, and added the following scripts (along the way, I learned that the button click only takes in public void functions, so I twisted things up for that):

    [SerializeField] int mainLevelBuildIndex = 2;

    public void ContinueToMainLevel() {
        // Transitioning to the main game scene:
        StartCoroutine(LoadMainGameScene());
    }

    private IEnumerator LoadMainGameScene() {

        Fader fader = FindObjectOfType<Fader>();
        yield return fader.FadeOut(fadeOutTime);
        yield return SceneManager.LoadSceneAsync(mainLevelBuildIndex);
        yield return fader.FadeIn(fadeInTime);

    }

Where the ‘mainLevelBuildIndex’ was assigned to be as number “2” within the script itself, and added to the build settings of my game later on

  1. Next up, I added a ‘Main Level Teleporter’ in my ‘Character Generator’ scene, responsible for teleporting my player from the character generator scene, to the main level. It’s an empty gameObject that has the ‘SavingWrapper.cs’ script, and it’s called by the ‘Confirm’ button that is nested within that empty gameObject, on the game hierarchy.

Unfortunately, it got stuck on the black screen (i.e: the IEnumerable faded in, but never faded out). Any idea what went wrong? (There are no compiler errors for that).

Edit: I don’t know if that’s the ideal fix for this bug, but apparently replacing this line:

yield return SceneManager.LoadSceneAsync(mainLevelBuildIndex);

With this one:

SceneManager.LoadScene(mainLevelBuildIndex);

fixed the black out issue… is this correct? (but the character we get is the Synty one that has quite literally everything on it turned on… So it’s a total mess. To fix that, place the ‘CharacterGenerator.cs’ script on all the player instances of your project, be it main menu, character creator, etc)

And somehow… this entire system now means my player can no longer wield anything in his Equipment slots (even for the weapons, which probably have nothing to do with the armor), neither can he attack (he only receives damage now), even when I play just that scene.

Edit 2: A little bit further, I can no longer deal damage. I can only accept damage, and the death bug which we discussed earlier keeps occuring like normal now (and no log errors were given out)

Privacy & Terms