using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEditor; namespace InfinityPBR { public static class EquipObject { // rootBoneName needs to be the name of the Bone Root for this character! It may be different for each // character, depending on how it was set up. public static string rootBoneName = "BoneRoot"; // Not used if Transform rootBoneTransform is provided to EquipCharacter() //private static GameObject _target; // The target to match private static SkinnedMeshRenderer _targetRenderer; // Renderer we are targetting private static string _subRootBoneName; // Bone name private static GameObject _thisBoneRoot; // Current bone root public static void Equip(GameObject targetGameObject, Transform rootBoneTransform = null, SkinnedMeshRenderer targetSkinnedMeshRenderer = null) { TryDebugMessage("Starting Equip Workflow."); if (targetGameObject == null) { Debug.LogError($"Error: targetGameObject is null! Aborting."); return; } // If rootBoneTransform has not been provided then search for it. if (rootBoneTransform == null) rootBoneTransform = GetChildTransform(targetGameObject, rootBoneName); // Find the rootBoneTransform // If rootBoneTransform is null, then we must abort. if (rootBoneTransform == null) { Debug.LogError($"Error: A rootBoneTransform was not provided, and we did not find a transform called {rootBoneName} when searching for the root bone transform."); return; } // If skinnedMeshRenderer has not been provided then search for it. if (targetSkinnedMeshRenderer == null) targetSkinnedMeshRenderer = GetFirstSkinnedMeshRenderer(targetGameObject); // Find the skinnedMeshRenderer // If skinnedMeshRenderer is null, then we must abort. if (targetSkinnedMeshRenderer == null) { Debug.LogError($"Error: No SkinnedMeshRenderer was assigned, and one could not be found."); return; } // Get a list of all the equipment we need to equip var equipmentDictionary = GetEquipmentList(targetGameObject); TryDebugMessage($"There are {equipmentDictionary.Count} objects to equip."); // Equip each equipment object we found foreach (var equipment in equipmentDictionary) EquipObjectToParent(equipment.Key, equipment.Value, rootBoneTransform, targetSkinnedMeshRenderer); TryDebugMessage("Character Equip Complete!!"); } /// /// This will equip an object to the parent /// /// The game object being equipped /// The EquipmentObject component attached to the game object being equipped /// /// public static void EquipObjectToParent(GameObject equipmentGameObject, EquipmentObject equipmentObject, Transform rootBoneTransform, SkinnedMeshRenderer targetSkinnedMeshRenderer) { if (equipmentObject == null) return; if (equipmentObject.boneRoot == null) { Debug.Log($"No bone root assigned on object {equipmentGameObject.name}! Make sure the bone is selected on the prefab."); return; } // Confirm the two have the same root bone name if (equipmentObject.boneRoot.name != rootBoneTransform.name) { Debug.LogError($"Error: Root bone names do not match. Parent is {rootBoneTransform.name} while equipment is {equipmentObject.boneRoot.name}"); return; } MakeItNotAPrefab(equipmentGameObject); // If the object is a prefab, make it not a prefab, so we can delete things AddEquipmentBonesToParent(equipmentObject, rootBoneTransform, targetSkinnedMeshRenderer); // Add any NEW bones to the parent, from the equipment. MigrateBoneLinks(equipmentObject, targetSkinnedMeshRenderer); // Fit the equipment bones to the parent DeleteOldBonesAndRemoveComponent(equipmentObject); // Delete the bone objects from the equipment Transform[] childBoneArray = equipmentObject.skinnedMeshRenderer.bones; //Debug.Log($"END: childBoneArray has {childBoneArray.Length} bones"); //Debug.Log($"And the first one is {childBoneArray[0].name}"); } private static void DeleteOldBonesAndRemoveComponent(EquipmentObject equipmentObject) { #if UNITY_EDITOR GameObject.DestroyImmediate(equipmentObject.boneRoot.gameObject); #else GameObject.Destroy(equipmentObject.boneRoot.gameObject); #endif equipmentObject.SelfDestruct(); // Remove the component } private static void MakeItNotAPrefab(GameObject equipmentGameObject) { #if UNITY_EDITOR if (PrefabUtility.IsAnyPrefabInstanceRoot(equipmentGameObject)) { Debug.Log($"Will try to unpack prefab {equipmentGameObject.name}"); PrefabUtility.UnpackPrefabInstance(equipmentGameObject, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction); } #endif } /// /// This will relink the SkinnedMeshRenderer bons and rootBone to the parent object /// /// /// private static void MigrateBoneLinks(EquipmentObject equipmentObject, SkinnedMeshRenderer targetSkinnedMeshRenderer) { Transform[] equipmentObjectBoneArray = equipmentObject.skinnedMeshRenderer.bones; //Debug.Log($"equipmentObjectBoneArray has {equipmentObjectBoneArray.Length} bones"); var equipmentObjectBoneMap = GetBoneMap(equipmentObject.skinnedMeshRenderer); //Debug.Log($"equipmentObjectBoneMap has {equipmentObjectBoneMap.Count} bones"); var parentBoneMap = GetBoneMap(targetSkinnedMeshRenderer); // Populate a Dictionary holding all the current bones in the SkinnedMeshRenderer //Debug.Log($"Parentmap has {parentBoneMap.Count} bones -- Target SMR has {targetSkinnedMeshRenderer.bones.Length} bones"); for (int i = 0; i < equipmentObjectBoneArray.Length; i++) { if (!parentBoneMap.ContainsKey(equipmentObjectBoneArray[i].name)) { Debug.LogWarning($"Warning: Could not find a bone in the parent called {equipmentObjectBoneArray[i].name}. This should not happen..."); continue; // ...Technically this should not happen... } //Debug.Log($"Will make {equipmentObjectBoneArray[i].name} match {parentBoneMap[equipmentObjectBoneArray[i].name].name}"); equipmentObjectBoneArray[i] = parentBoneMap[equipmentObjectBoneArray[i].name]; //Debug.Log($"Check 1: {equipmentObjectBoneArray[i].name} should match {parentBoneMap[equipmentObjectBoneArray[i].name].name}"); //Debug.Log($"Check 2: {equipmentObjectBoneArray[i].gameObject.GetInstanceID()} should match {parentBoneMap[equipmentObjectBoneArray[i].name].gameObject.GetInstanceID()}"); } equipmentObject.skinnedMeshRenderer.bones = equipmentObjectBoneArray; // Set new values to the bones //Debug.Log($"Rootbone: {equipmentObject.skinnedMeshRenderer.rootBone} will now be {parentBoneMap[equipmentObject.skinnedMeshRenderer.rootBone.name].name}"); equipmentObject.skinnedMeshRenderer.rootBone = parentBoneMap[equipmentObject.skinnedMeshRenderer.rootBone.name]; } /// /// This will add any missing bones from the EquipmentObject.rootBone to the parent /// /// /// /// private static void AddEquipmentBonesToParent(EquipmentObject equipmentObject, Transform rootBoneTransform, SkinnedMeshRenderer targetSkinnedMeshRenderer) { var parentBones = rootBoneTransform.GetComponentsInChildren(); // Get the list of bones foreach (Transform child in equipmentObject.boneRoot.GetComponentsInChildren()) { if (BonesContain(child.name, parentBones)) continue; // If we have the bone already, we skip //Debug.Log($"Bone {child.name} was not found..."); // Cache the name of the parent bone from the child bone list and then get that bone from the parent as well. var parentBoneName = child.transform.parent.name; var parentBoneTransform = GetBone(parentBoneName, parentBones); // Create the new bone on the parent, and set the transform values to match the child bone var newBone = new GameObject(child.name); newBone.transform.parent = parentBoneTransform; newBone.transform.localPosition = child.localPosition; newBone.transform.localRotation = child.localRotation; newBone.transform.localScale = child.localScale; parentBones = rootBoneTransform.GetComponentsInChildren(); // Recompute the list of bones var bones = new List(); foreach (Transform bone in targetSkinnedMeshRenderer.bones) bones.Add(bone); bones.Add(newBone.transform); targetSkinnedMeshRenderer.bones = bones.ToArray(); TryDebugMessage($"Added a bone to the parent called {newBone.name}!"); } } private static bool BonesContain(string childName, Transform[] parentBones) => GetBone(childName, parentBones) != null; private static Transform GetBone(string boneName, Transform[] bones) => bones.FirstOrDefault(x => x.name == boneName); /// /// Searches the children of the target object for objects which have a EquipmentObject component on them, and adds /// those and the component to a Dictionary, which is returned. /// /// /// /// private static Dictionary GetEquipmentList(GameObject targetGameObject, bool includeInactive = true) { var objectList = new Dictionary(); foreach (Transform child in targetGameObject.GetComponentsInChildren(includeInactive)) { if (!child.TryGetComponent(out EquipmentObject equipmentObject)) continue; objectList.Add(equipmentObject.gameObject, equipmentObject); } return objectList; } /// /// This will search for the first SkinnedMeshRenderer on any child object of the parent. /// /// /// private static SkinnedMeshRenderer GetFirstSkinnedMeshRenderer(GameObject target) { foreach (Transform child in target.transform) { if (!child.TryGetComponent(out SkinnedMeshRenderer smr)) continue; //Debug.Log($"Got SMR: {smr.name}"); return smr; } return default; } /// /// This will return a Dictionary with all the names and transforms of the bone hierarchy /// /// /// /// private static Dictionary GetBoneMap(SkinnedMeshRenderer skinnedMeshRenderer) { var newBoneMap = new Dictionary(); // Add each bone from the SkinnedMeshRenderer to the dictionary. var boneTransforms = skinnedMeshRenderer.rootBone.GetComponentsInChildren(); foreach (Transform bone in boneTransforms) { newBoneMap.Add(bone.name, bone); } return newBoneMap; // March 19, 2022 -- Bug was that the code below was only finding the bones that are actually targetted // by the object I guess. Not ALL the bones. So the code above gets them all. // Add each bone from the SkinnedMeshRenderer to the dictionary. foreach (Transform bone in skinnedMeshRenderer.bones) { newBoneMap.Add(bone.name, bone); } return newBoneMap; } /// /// This will search the children of the target object for a transform named string childName, and return it. /// /// The GameObject to search /// The name of the object we are looking for /// If true, will search all children and grandchildren. Otherwise will search just one level down. /// If searching all, set whether to include inactive objects /// public static Transform GetChildTransform(GameObject target, string childName, bool searchAll = true, bool includeInactive = false) { // Start by looking one level down, returning if we find it. foreach (Transform childTransform in target.transform) { if (childTransform.name != childName) continue; //Debug.Log($"Got root bone: {childTransform.name}"); return childTransform; } //Debug.Log("Return default"); if (!searchAll) return default; // If we aren't searching all, end the search here. // Handle if searchAll = true -- this will include ALL children and grandchildren foreach (Transform childTransform in target.GetComponentsInChildren(includeInactive)) { if (childTransform.name != childName) continue; return childTransform; } return default; } private static void TryDebugMessage(string message) { #if UNITY_EDITOR Debug.Log($"{message} (This message only shows in the Editor)"); #endif } } }