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
}
}