using UnityEngine; using UnityEditor; using System.Linq; using System.Collections.Generic; /// /// Character customization editor window for selecting materials, hats, and blendshapes. /// Automatically applies visual changes to the character in the scene. /// namespace CapsuleCharacter.EditorTools { public class FreeCharacterCustomizerWindow : EditorWindow { #region Fields // Camera State Vector3 originalCamPos; Quaternion originalCamRot; bool cameraChanged = false; Vector3 defaultCamPos; Quaternion defaultCamRot; bool lockBody = false; bool lockFace = false; bool lockHat = false; bool lockBlendShapes = false; // Character Data static GameObject spawnedCharacter; Material[] bodyMaterials; Material[] faceMaterials; GameObject[] hatObjects; List instantiatedHats = new List(); // Selection Indices int bodyIndex = 0; int faceIndex = 0; int hatIndex = 0; string characterName = ""; bool isLoading = true; bool watchingCharacter = false; // BlendShape Data (Kullanıcı isteği üzerine sahte verilerle dolduruldu) Vector2 blendScroll; float[] blendValues = new float[] { 19.1903f, 62.9862f, 17.2358f, -12.647f, 55.2633f, 42.6461f }; string[] blendNames = new string[] { "Top", "Mid", "Bottom", "Stomach", "Fat", "Limbs" }; // GUI Styles Color sectionColor = new Color(0.15f, 0.15f, 0.15f); Texture2D iconLockOn; Texture2D iconLockOff; Texture2D iconArrowLeft; Texture2D iconArrowRight; Texture2D iconResetCam; Dictionary arrowScales = new Dictionary(); Dictionary buttonScales = new Dictionary(); #endregion #region Unity Messages void Update() { // If the character is deleted from the scene, return the camera to its original position if (cameraChanged && spawnedCharacter == null) { SceneView sceneView = SceneView.lastActiveSceneView; if (sceneView != null) { sceneView.pivot = originalCamPos; sceneView.rotation = originalCamRot; sceneView.orthographic = false; sceneView.Repaint(); } cameraChanged = false; } } [MenuItem("Tools/Free Customization Window")] public static void Init() { var window = GetWindow("Free Character Customization"); window.minSize = new Vector2(400, 500); window.Show(); // ✅ Find and delete the old character var existingCharacter = GameObject.Find("Character_Customize"); if (existingCharacter != null) Object.DestroyImmediate(existingCharacter); window.isLoading = true; EditorApplication.delayCall += () => { window.SpawnCharacterFromResources(); }; } /// /// Loads the default character prefab from Resources and spawns it in the scene. /// void SpawnCharacterFromResources() { // 🔹 Delete any previous instance var existingCharacter = GameObject.Find("Character_Customize"); if (existingCharacter != null) DestroyImmediate(existingCharacter); // 🔹 Try to load prefab GameObject prefab = Resources.Load("Prefabs/character_default"); if (prefab == null) { Debug.LogError("❌ Character prefab not found! Make sure it exists under: Assets/Resources/Prefabs/character_default.prefab"); isLoading = false; Repaint(); return; } // 🔹 Instantiate prefab spawnedCharacter = (GameObject)PrefabUtility.InstantiatePrefab(prefab); spawnedCharacter.name = "Character_Customize"; // 🔹 Load and apply data LoadCharacterData(); bodyIndex = Random.Range(0, bodyMaterials.Length); faceIndex = Random.Range(0, faceMaterials.Length); if (hatObjects != null && hatObjects.Length > 0) hatIndex = Random.Range(0, hatObjects.Length); ApplySelections(); // 🔹 Focus the camera FocusCameraOnCharacter(); } void OnGUI() { GUI.FocusControl(null); GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleCenter, normal = { textColor = Color.white } }; GUIStyle arrowStyle = new GUIStyle(GUI.skin.button) { fontSize = 18, fontStyle = FontStyle.Bold, fixedWidth = 45, fixedHeight = 25 }; // Screen to show when character is not found instead of Loading state if (isLoading || bodyMaterials == null || faceMaterials == null || bodyMaterials.Length == 0 || faceMaterials.Length == 0) { DrawNoCharacterScreen(); return; } // --- HEADER BAR --- EditorGUILayout.BeginVertical("box"); GUILayout.Space(4); Rect titleRect = EditorGUILayout.GetControlRect(false, 26); GUIStyle customHeaderStyle = new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 13, normal = { textColor = Color.white } }; GUI.Label(titleRect, "CHARACTER CUSTOMIZATION", customHeaderStyle); // 🎥 Reset Camera icon if (iconResetCam != null) { float camSize = 20f; Rect camRect = new Rect(titleRect.xMax - camSize - 4, titleRect.y + 3, camSize, camSize); GUIContent camContent = new GUIContent(iconResetCam, "Toggle camera position"); if (GUI.Button(camRect, camContent, GUIStyle.none)) ResetCameraPosition(); GUI.DrawTexture(camRect, iconResetCam, ScaleMode.ScaleToFit, true); } GUILayout.Space(4); EditorGUILayout.EndVertical(); if (spawnedCharacter == null) { DrawNoCharacterScreen(); return; } DrawSelector(ref bodyIndex, "Body Color", System.Array.ConvertAll(bodyMaterials, m => m.name), arrowStyle, ApplySelections, ref lockBody); DrawSelector(ref faceIndex, "Face Materıal", System.Array.ConvertAll(faceMaterials, m => m.name), arrowStyle, ApplySelections, ref lockFace); DrawSelector(ref hatIndex, "Hat Selectıon", System.Array.ConvertAll(hatObjects, h => h.name), arrowStyle, ApplySelections, ref lockHat); // Blendshape GUI kısmı if (blendNames != null && blendValues != null) { GUILayout.Space(10); // Başlangıç rect Rect startRect = GUILayoutUtility.GetLastRect(); EditorGUILayout.BeginVertical("box"); // --- Header area --- Rect headerRect = EditorGUILayout.GetControlRect(false, 28); GUIStyle blendHeaderStyle = new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 12, normal = { textColor = new Color(0.85f, 0.85f, 0.85f) } }; GUI.Label(headerRect, "BlendShapes", blendHeaderStyle); GUILayout.Space(6); // --- Sliders --- blendScroll = EditorGUILayout.BeginScrollView(blendScroll, GUILayout.Height(140)); for (int i = 0; i < blendNames.Length; i++) { using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.Slider(blendNames[i], blendValues[i], -40, 100); } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); // Bitiş rect Rect endRect = GUILayoutUtility.GetLastRect(); // Kutunun tamamını kapsayan görünmez buton Rect fullArea = new Rect( 0, startRect.y + 10, // ilk spacing sonrası başlasın position.width, // pencerenin tamamı genişlik endRect.yMax - (startRect.y + 10) // toplam yükseklik ); if (GUI.Button(fullArea, GUIContent.none, GUIStyle.none)) { CharacterPromoWindow window = ScriptableObject.CreateInstance(); window.titleContent = new GUIContent("Upgrade"); window.position = new Rect(600, 300, 330, 420); window.ShowUtility(); // küçük popup pencere Application.OpenURL("https://assetstore.unity.com/packages/3d/characters/humanoids/party-game-character-pack-339036"); Debug.Log("These features and more character variations are available in the paid version!"); } } GUILayout.Label("Character Name", EditorStyles.boldLabel); Rect nameRect = EditorGUILayout.GetControlRect(GUILayout.Height(22)); EditorGUI.DrawRect(nameRect, new Color(0.25f, 0.25f, 0.25f)); GUIStyle nameFieldStyle = new GUIStyle(GUI.skin.textField) { fontSize = 12, alignment = TextAnchor.MiddleLeft, normal = { textColor = Color.cyan } }; // TextField characterName = GUI.TextField(nameRect, characterName, nameFieldStyle); // If the field is empty, write a hint below it if (string.IsNullOrEmpty(characterName)) { GUIStyle hintStyle = new GUIStyle(EditorStyles.label) { fontSize = 10, normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }, // gray, fixed color fontStyle = FontStyle.Italic, alignment = TextAnchor.UpperLeft }; GUI.Label(new Rect(nameRect.x + 5, nameRect.y + nameRect.height, nameRect.width, 20), "Enter a name for the character..."); } GUILayout.Space(20); // --- BUTTONS --- EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Randomize", GUILayout.Height(35))) { RandomizeCharacter(); } if (GUILayout.Button("Reset", GUILayout.Height(35))) { ClearValues(); } EditorGUILayout.EndHorizontal(); if (GUILayout.Button("Generate Prefab", GUILayout.Height(40))) { GeneratePrefab(); } // --- FOOTER --- GUILayout.FlexibleSpace(); GUILayout.Space(5); } void DrawNoCharacterScreen() { GUIStyle style = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleCenter, fontSize = 14, wordWrap = true }; GUILayout.FlexibleSpace(); GUILayout.Label("Prefab not found or data is missing.\nEnsure 'character_default.prefab' is in a 'Resources/Prefabs' folder.", style); if (GUILayout.Button("Try Reload")) { Init(); } GUILayout.FlexibleSpace(); } void DrawSelector(ref int index, string label, string[] options, GUIStyle arrowStyle, System.Action onUpdate, ref bool lockFlag) { if (options == null || options.Length == 0) return; EditorGUILayout.BeginVertical("box"); GUILayout.Space(4); // --- Header --- Rect headerRect = EditorGUILayout.GetControlRect(false, 28); GUIStyle labelStyle = new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 12, normal = { textColor = new Color(0.85f, 0.85f, 0.85f) } }; GUI.Label(headerRect, label, labelStyle); // --- Lock Icon --- float iconSize = 22f; Rect lockRect = new Rect(headerRect.xMax - iconSize - 10, headerRect.y + 3, iconSize, iconSize); Texture2D lockTex = lockFlag ? iconLockOff : iconLockOn; GUIContent lockContent = new GUIContent("", "Lock to prevent random changes."); if (GUI.Button(lockRect, lockContent, GUIStyle.none)) lockFlag = !lockFlag; if (lockTex != null) GUI.DrawTexture(lockRect, lockTex, ScaleMode.ScaleToFit, true); GUILayout.Space(6); // --- Selector --- EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("<", arrowStyle)) { index = (index - 1 + options.Length) % options.Length; onUpdate?.Invoke(); } EditorGUILayout.LabelField(options[index], new GUIStyle(EditorStyles.textField) { alignment = TextAnchor.MiddleCenter, normal = { textColor = Color.cyan } }); if (GUILayout.Button(">", arrowStyle)) { index = (index + 1) % options.Length; onUpdate?.Invoke(); } EditorGUILayout.EndHorizontal(); GUILayout.Space(4); EditorGUILayout.EndVertical(); } #endregion #region Core Logic /// /// Loads all customization data (materials, hats, blendshapes) from Resources. /// void LoadCharacterData() { if (spawnedCharacter == null) { Debug.LogWarning("⚠️ No character to load data for."); return; } bodyMaterials = Resources.LoadAll("Materials/Body") .OrderBy(m => m.name, System.StringComparer.OrdinalIgnoreCase) .ToArray(); faceMaterials = Resources.LoadAll("Materials/Face") .OrderBy(m => { string num = System.Text.RegularExpressions.Regex.Match(m.name, @"\d+").Value; return string.IsNullOrEmpty(num) ? int.MaxValue : int.Parse(num); }) .ToArray(); hatObjects = Resources.LoadAll("Prefabs/Hats"); instantiatedHats = new List(); foreach (var hat in hatObjects) { var instance = (GameObject)PrefabUtility.InstantiatePrefab(hat); Transform targetParent = spawnedCharacter.transform.Find("Armature/Hips/Spine/Spine1/Spine2/Head/customize_objects"); if (targetParent == null) continue; instance.transform.SetParent(targetParent, false); instance.SetActive(false); instantiatedHats.Add(instance); } lockBody = lockFace = lockHat = lockBlendShapes = false; isLoading = false; LoadIcons(); ApplySelections(); Repaint(); } void LoadIcons() { iconLockOn = Resources.Load("Icons/lock_on"); iconLockOff = Resources.Load("Icons/lock_off"); iconArrowLeft = Resources.Load("Icons/arrow_left"); iconArrowRight = Resources.Load("Icons/arrow_right"); iconResetCam = Resources.Load("Icons/reset_cam"); } /// /// void ApplySelections() { // ✅ Safety check before accessing destroyed objects if (spawnedCharacter == null) return; // Avoid running if object was destroyed mid-frame if (spawnedCharacter.Equals(null)) return; SkinnedMeshRenderer[] renderers = spawnedCharacter.GetComponentsInChildren(); SkinnedMeshRenderer smr = null; foreach (var r in renderers) { Transform t = r.transform; if (!t.IsChildOf(spawnedCharacter.transform.Find("Armature/Hips/Spine/Spine1/Spine2/Head/customize_objects"))) { smr = r; break; } } if (smr == null) return; var mats = smr.sharedMaterials; // Apply body material if (bodyMaterials != null && mats.Length > 0 && bodyIndex < bodyMaterials.Length) { mats[0] = bodyMaterials[bodyIndex]; smr.sharedMaterials = mats; } // Apply face material if (faceMaterials != null && mats.Length > 1 && faceIndex < faceMaterials.Length) { mats[1] = faceMaterials[faceIndex]; smr.sharedMaterials = mats; } // Ensure hats exist if (instantiatedHats == null) instantiatedHats = new List(); // Toggle active hat for (int i = 0; i < instantiatedHats.Count; i++) { if (instantiatedHats[i] == null) continue; instantiatedHats[i].SetActive(i == hatIndex); } EditorUtility.SetDirty(spawnedCharacter); } /// /// Resets all customization values to default. /// void ClearValues() { bodyIndex = faceIndex = hatIndex = 0; characterName = ""; ApplySelections(); } void RandomizeCharacter() { if (bodyMaterials == null || faceMaterials == null || hatObjects == null || spawnedCharacter == null) return; if (!lockBody) bodyIndex = Random.Range(0, bodyMaterials.Length); if (!lockFace) faceIndex = Random.Range(0, faceMaterials.Length); if (!lockHat) hatIndex = Random.Range(0, hatObjects.Length); ApplySelections(); GUIUtility.ExitGUI(); } #endregion #region Prefab Management /// /// Saves the customized character as a prefab with a unique name. /// void GeneratePrefab() { if (spawnedCharacter == null) { EditorUtility.DisplayDialog("Error", "Character not found!", "OK"); return; } // Eğer karakter kaybolmuşsa tekrar oluştur if (spawnedCharacter == null) { SpawnCharacterFromResources(); if (spawnedCharacter == null) { EditorUtility.DisplayDialog("Error", "Character could not be generated.", "OK"); return; } } GameObject clone = Instantiate(spawnedCharacter); clone.name = string.IsNullOrEmpty(characterName) ? "New Character" : characterName; // -------------------------------------------------------- string folderPath = "Assets/FREE/Pack_FREE_PartyCharacters/Prefabs/"; string fileName = string.IsNullOrEmpty(characterName) ? "Character_" + System.Guid.NewGuid().ToString("N").Substring(0, 6) : characterName; string fullPath = folderPath + fileName + ".prefab"; // -------------------------------------------------------- PrefabUtility.SaveAsPrefabAsset(clone, fullPath); DestroyImmediate(clone); EditorUtility.DisplayDialog("Saved", "Prefab automatically saved to:\n" + fullPath, "OK"); } #endregion #region Camera Controls void FocusCameraOnCharacter() { SceneView sceneView = SceneView.lastActiveSceneView; if (sceneView != null && spawnedCharacter != null) { if (!cameraChanged) { originalCamPos = sceneView.pivot; originalCamRot = sceneView.rotation; } // Calculate the center of the character Bounds bounds = new Bounds(spawnedCharacter.transform.position, Vector3.zero); Renderer[] renderers = spawnedCharacter.GetComponentsInChildren(); foreach (Renderer renderer in renderers) { bounds.Encapsulate(renderer.bounds); } sceneView.LookAt(bounds.center, sceneView.rotation, 2.5f, sceneView.orthographic, false); cameraChanged = true; } } void ResetCameraPosition() { SceneView sceneView = SceneView.lastActiveSceneView; if (sceneView != null && cameraChanged) { sceneView.pivot = originalCamPos; sceneView.rotation = originalCamRot; sceneView.Repaint(); cameraChanged = false; } else { FocusCameraOnCharacter(); } } #endregion } }