场景设计

This commit is contained in:
SoulliesOfficial
2026-01-12 03:22:16 -05:00
parent 33b1795c1f
commit d94241f36c
8645 changed files with 1242757 additions and 1039728 deletions

View File

@@ -0,0 +1,691 @@
//--------------------------------------------------------------------------//
// Copyright 2023-2025 Chocolate Dinosaur Ltd. All rights reserved. //
// For full documentation visit https://www.chocolatedinosaur.com //
//--------------------------------------------------------------------------//
#if UIFX_TMPRO
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using TMPro;
namespace ChocDino.UIFX.Editor
{
[CustomEditor(typeof(FilterStackTextMeshPro), true)]
[CanEditMultipleObjects]
internal class FilterStackTextMeshProEditor : BaseEditor
{
private static readonly GUIContent Content_Filters = new GUIContent("Filters");
private static readonly GUIContent Content_FilterList = new GUIContent("Filter List");
private static readonly GUIContent Content_Filter88 = new GUIContent("Filter 88 ");
private static readonly GUIContent Content_AddFilter = new GUIContent("Add Filter:");
private static readonly GUIContent Content_SaveToPNG = new GUIContent("Save to PNG");
private static readonly GUIContent Content_PreviewTitle = new GUIContent("UIFX - FilterStack TextMeshPro");
private static readonly string Pref_Prefix = "UIFX.FilterStack.";
private static readonly string Pref_SelectedTypeIndex = Pref_Prefix + "SelectedTypeIndex";
private static readonly AboutInfo s_aboutInfo =
new AboutInfo(s_aboutHelp, "UIFX - Filter Stack TextMeshPro\n© Chocolate Dinosaur Ltd", "uifx-icon")
{
sections = new AboutSection[]
{
new AboutSection("Asset Guides")
{
buttons = new AboutButton[]
{
new AboutButton("Components Reference", "https://www.chocdino.com/products/uifx/bundle/components/filter-stack-text-mesh-pro/"),
}
},
new AboutSection("Unity Asset Store Review\r\n<color=#ffd700>★★★★☆</color>")
{
buttons = new AboutButton[]
{
new AboutButton("Review <b>UIFX Bundle</b>", AssetStoreBundleReviewUrl),
}
},
new AboutSection("UIFX Support")
{
buttons = new AboutButton[]
{
new AboutButton("Discord Community", DiscordUrl),
new AboutButton("Post to Unity Discussions", ForumBundleUrl),
new AboutButton("Post Issues to GitHub", GithubUrl),
new AboutButton("Email Us", SupportEmailUrl),
}
}
}
};
private static readonly AboutToolbar s_aboutToolbar = new AboutToolbar(new AboutInfo[] { s_upgradeToBundle, s_aboutInfo } );
private ReorderableList _reorderableList;
private SerializedProperty _propApplyToSprites;
private SerializedProperty _propRelativeToTransformScale;
private SerializedProperty _propRelativeFontSize;
private SerializedProperty _propUpdateOnTransform;
private SerializedProperty _propRenderSpace;
private static readonly GUIContent Content_Add = new GUIContent("Add");
private SerializedProperty _propFilters;
private List<System.Type> _filterTypes;
private List<FilterBase> _unusedFilterComponents = new List<FilterBase>(4);
private bool _hasEmptyFilterSlots = false;
private GUIContent[] _filterTypesNames;
private int _selectedTypeIndex;
private void ShowSaveToPngButton()
{
// TODO: Add baking to Image option that bakes to a new GameObject and doesn't destroy the old object
// TODO: Convert the image baking buttons into a EditorGUILayout.DropDownToggle()
if (GUILayout.Button(Content_SaveToPNG))
{
foreach (FilterStackTextMeshPro filter in this.targets)
{
System.IO.Directory.CreateDirectory(Application.dataPath + "/../Captures/");
string timestamp = System.DateTime.Now.ToString("yyyyMMdd-HHmmss");
string filterName = filter.GetType().Name;
string path = System.IO.Path.GetFullPath(Application.dataPath + string.Format("/../Captures/Image-{0}-{1}-{2}.png", timestamp, filter.gameObject.name, filterName));
if (filter.SaveToPNG(path))
{
Debug.Log("Saving image to: <b>" + path + "</b>");
}
else
{
Debug.LogError("Failed to save image to: <b>" + path + "</b>");
}
}
}
}
#if !UIFX_FILTER_HIDE_INSPECTOR_PREVIEW
public override GUIContent GetPreviewTitle()
{
return Content_PreviewTitle;
}
public override bool HasPreviewGUI()
{
// We can't support multiple targets because the below logic to only show the last item doesn't work
if (targets.Length > 1) return false;
var filter = target as FilterStackTextMeshPro;
if (!filter.isActiveAndEnabled) return false;
return true;
}
public override string GetInfoString()
{
var filter = target as FilterStackTextMeshPro;
var texture = filter.ResolveToTexture();
if (texture)
{
return (texture.width + " x " + texture.height);
}
return string.Empty;
}
public override void OnPreviewSettings()
{
ShowSaveToPngButton();
}
public override void OnPreviewGUI(Rect r, GUIStyle background)
{
if (Event.current.type == EventType.Repaint)
{
var filter = target as FilterStackTextMeshPro;
EditorGUI.DrawTextureTransparent(r, Texture2D.blackTexture, ScaleMode.StretchToFill);
var texture = filter.ResolveToTexture();
if (texture)
{
GUI.DrawTexture(r, texture, ScaleMode.ScaleToFit, true);
}
}
}
#endif
void OnEnable()
{
// Note: Had a case where in a prefab if the component is removed it can still call OnEnable()
if (this.target == null) return;
var extractedTypes = TypeCache.GetTypesDerivedFrom<FilterBase>();
if (extractedTypes.Count > 0)
{
_filterTypes = extractedTypes.OrderBy(type => type.Name).ToList();
_filterTypesNames = new GUIContent[_filterTypes.Count];
for (int i = 0; i < _filterTypes.Count; i++)
{
_filterTypesNames[i] = new GUIContent(GetDisplayNameForComponentType(_filterTypes[i]));
}
}
else
{
_filterTypesNames = new GUIContent[0];
}
_selectedTypeIndex = EditorPrefs.GetInt(Pref_SelectedTypeIndex, 0);
_selectedTypeIndex = Mathf.Clamp(_selectedTypeIndex, 0, _filterTypesNames.Length - 1);
_propFilters = VerifyFindProperty("_filters");
_propApplyToSprites = VerifyFindProperty("_applyToSprites");
_propRelativeToTransformScale = VerifyFindProperty("_relativeToTransformScale");
_propRelativeFontSize = VerifyFindProperty("_relativeFontSize");
_propUpdateOnTransform = VerifyFindProperty("_updateOnTransform");
_propRenderSpace = VerifyFindProperty("_renderSpace");
_reorderableList = new ReorderableList(serializedObject, _propFilters, true, false, true, true)
{
drawHeaderCallback = DrawListHeader,
drawElementCallback = DrawListElement,
onAddDropdownCallback = AddDropdownCallback
};
UpdateFilterList();
}
private void UpdateFilterList()
{
UpdateEmptyFilterSlots();
UpdateUnusedFilterList();
}
private void UpdateEmptyFilterSlots()
{
_hasEmptyFilterSlots = false;
foreach (var obj in this.targets)
{
FilterBase filterComponent = null;
var so = new SerializedObject(obj);
var prop = so.FindProperty("_filters");
IEnumerator it = prop.GetEnumerator();
while (it.MoveNext())
{
var property = it.Current as SerializedProperty;
filterComponent = property.objectReferenceValue as FilterBase;
if (filterComponent == null)
{
_hasEmptyFilterSlots = true;
break;
}
}
if (filterComponent == null)
{
break;
}
}
}
private void UpdateUnusedFilterList()
{
// Find all filters in the GameObjects
List<FilterBase> filtersInGameObjects = new List<FilterBase>(16);
foreach (var obj in this.targets)
{
var filterStack = obj as FilterStackTextMeshPro;
var filterComponents = filterStack.gameObject.GetComponents<FilterBase>();
filtersInGameObjects.AddRange(filterComponents);
}
// Remove any that aren't in the FilterList
foreach (var obj in this.targets)
{
var so = new SerializedObject(obj);
var prop = so.FindProperty("_filters");
IEnumerator it = prop.GetEnumerator();
while (it.MoveNext())
{
var property = it.Current as SerializedProperty;
var filterComponent = property.objectReferenceValue as FilterBase;
if (filtersInGameObjects.Contains(filterComponent))
{
filtersInGameObjects.Remove(filterComponent);
}
}
}
_unusedFilterComponents = filtersInGameObjects;
}
void OnDisable()
{
_filterTypes = null;
_filterTypesNames = null;
EditorPrefs.SetInt(Pref_SelectedTypeIndex, _selectedTypeIndex);
}
static void InsertIntoList(SerializedProperty listProp, Component component)
{
// Insert into the filter list, find the last null child, or append
int indexToInsert = -1;
if (listProp.arraySize > 0)
{
var lastChildProp = listProp.GetArrayElementAtIndex(listProp.arraySize - 1);
if (lastChildProp.objectReferenceValue == null)
{
indexToInsert = listProp.arraySize - 1;
}
}
if (indexToInsert < 0)
{
listProp.InsertArrayElementAtIndex(listProp.arraySize);
indexToInsert = listProp.arraySize - 1;
}
listProp.GetArrayElementAtIndex(indexToInsert).objectReferenceValue = component;
}
private void DrawListHeader(Rect rect)
{
EditorGUI.LabelField(rect, Content_FilterList);
}
private void OnClickFilterTypeName(object target)
{
serializedObject.Update();
if (target != null)
{
var type = (System.Type)target;
foreach (var obj in this.targets)
{
var filterStack = obj as FilterStackTextMeshPro;
var component = ObjectFactory.AddComponent(filterStack.gameObject, type);
var so = new SerializedObject(obj);
var prop = so.FindProperty("_filters");
InsertIntoList(prop, component);
so.ApplyModifiedProperties();
}
}
else
{
// Add an empty slot
foreach (var obj in this.targets)
{
var so = new SerializedObject(obj);
var prop = so.FindProperty("_filters");
InsertIntoList(prop, null);
so.ApplyModifiedProperties();
}
}
serializedObject.ApplyModifiedProperties();
serializedObject.SetIsDifferentCacheDirty();
serializedObject.Update();
}
private void AddDropdownCallback(Rect rect, ReorderableList list)
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Empty"), false, OnClickFilterTypeName, null);
int index = 0;
foreach (var filterTypeName in _filterTypesNames)
{
menu.AddItem(filterTypeName, false, OnClickFilterTypeName, _filterTypes[index]);
index++;
}
menu.ShowAsContext();
}
private void DrawListElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty prop = _reorderableList.serializedProperty.GetArrayElementAtIndex(index);
// By default, the array dropdown is offset to the left which intersects with
// the drag handle, so we can either indent the array property or inset the rect.
//EditorGUI.indentLevel++;
// Take note of the last argument, since this is an array,
// we want to draw it with all of its children.
Vector2 labelSize = GUI.skin.label.CalcSize(Content_Filter88);
Rect labelRect = rect;
labelRect.width = labelSize.x;
EditorGUI.PrefixLabel(labelRect, new GUIContent("Filter " + index));
rect.xMin += labelRect.width;
try
{
FilterBase filterComponent = prop.objectReferenceValue as FilterBase;
if (filterComponent)
{
Vector2 toggleSize = GUI.skin.toggle.CalcSize(GUIContent.none);
Rect toggleRect = rect;
toggleRect.width = toggleSize.x * 2f;
if (this.targets.Length > 1)
{
// Determine whether to show mixed values
bool prevMixed = EditorGUI.showMixedValue;
int lastState = -2;
foreach (var obj in this.targets)
{
var so = new SerializedObject(obj);
var prop2 = so.FindProperty("_filters");
SerializedProperty prop3 = prop2.GetArrayElementAtIndex(index);
FilterBase filterComponent2 = prop3.objectReferenceValue as FilterBase;
int state = -1;
if (filterComponent2)
{
state = filterComponent2.enabled ? 0 : 1;
}
if (lastState != -2 && state != lastState)
{
EditorGUI.showMixedValue = true;
break;
}
lastState = state;
}
EditorGUI.BeginChangeCheck();
bool newValue = EditorGUI.Toggle(toggleRect, filterComponent.enabled);
if (EditorGUI.EndChangeCheck())
{
foreach (var obj in this.targets)
{
var so = new SerializedObject(obj);
var prop2 = so.FindProperty("_filters");
SerializedProperty prop3 = prop2.GetArrayElementAtIndex(index);
FilterBase filterComponent2 = prop3.objectReferenceValue as FilterBase;
if (filterComponent2)
{
filterComponent2.enabled = newValue;
EditorUtility.SetDirty(filterComponent2);
}
}
}
EditorGUI.showMixedValue = prevMixed;
}
else
{
EditorGUI.BeginChangeCheck();
bool newValue = EditorGUI.Toggle(toggleRect, filterComponent.enabled);
if (EditorGUI.EndChangeCheck())
{
filterComponent.enabled = newValue;
EditorUtility.SetDirty(filterComponent);
}
}
rect.xMin += toggleRect.width;
}
}
catch(System.Exception)
{
}
EditorGUI.PropertyField(rect, prop, GUIContent.none, includeChildren: false);
//EditorGUI.indentLevel--;
//EditorGUI.PropertyField(rect, prop, new GUIContent("Filter " + index), includeChildren: false);
}
private void AddUnusedFilters()
{
serializedObject.Update();
foreach (var component in _unusedFilterComponents)
{
var filterStack = component.gameObject.GetComponent<FilterStackTextMeshPro>();
var so = new SerializedObject(filterStack);
var prop = so.FindProperty("_filters");
InsertIntoList(prop, component);
so.ApplyModifiedProperties();
}
_unusedFilterComponents.Clear();
serializedObject.ApplyModifiedProperties();
serializedObject.SetIsDifferentCacheDirty();
serializedObject.Update();
}
private void RemoveEmptySlots()
{
foreach (var obj in this.targets)
{
var so = new SerializedObject(obj);
var prop = so.FindProperty("_filters");
int arraySize = prop.arraySize;
for (int index = 0; index < arraySize;)
{
var arrayElement = prop.GetArrayElementAtIndex(index);
if (arrayElement.objectReferenceValue == null)
{
// Older versions (pre 2021.2 and 2020.3) of Unity requires 2 deletes, this code covers bother cases
var oldLength = prop.arraySize;
prop.DeleteArrayElementAtIndex(index);
if (prop.arraySize == oldLength)
{
prop.DeleteArrayElementAtIndex(index);
}
arraySize--;
}
else
{
index++;
}
}
so.ApplyModifiedProperties();
so.SetIsDifferentCacheDirty();
so.Update();
}
}
private const string TextAnimator2TypeName = "Febucci.UI.TextAnimator_TMP";
private const string TextAnimator1TypeName = "Febucci.UI.TextAnimator";
bool NeedsTextAnimatorSupportEnabled()
{
bool result = false;
#if !UIFX_SUPPORT_TEXT_ANIMATOR
foreach (var obj in this.targets)
{
var filterStack = obj as FilterStackTextMeshPro;
var component = filterStack.gameObject.GetComponent(TextAnimator2TypeName);
if (component == null)
{
component = filterStack.gameObject.GetComponent(TextAnimator1TypeName);
}
result = component != null;
}
#endif
return result;
}
bool IsTextureTooLarge()
{
bool result = false;
foreach (var obj in this.targets)
{
var filterStack = obj as FilterStackTextMeshPro;
if (filterStack.IsTextureTooLarge())
{
result = true;
break;
}
}
return result;
}
public override void OnInspectorGUI()
{
s_aboutToolbar.OnGUI();
if (NeedsTextAnimatorSupportEnabled())
{
EditorGUILayout.HelpBox("Text Animator support must be enabled in the UIFX Settings for your build platforms.", MessageType.Error, true);
if (GUILayout.Button("Open UIFX Settings"))
{
SettingsService.OpenProjectSettings(Preferences.SettingsPath);
}
EditorGUILayout.Space();
return;
}
if (IsTextureTooLarge())
{
EditorGUILayout.HelpBox("Requested texture is larger than the supported size of " + ChocDino.UIFX.Filters.GetMaxiumumTextureSize() + ", rescaling texture to supported size, this can lead to lower texture quality. Consider invstigating why such a large texture is required.", MessageType.Error, true);
EditorGUILayout.Space();
}
serializedObject.Update();
EditorGUILayout.PropertyField(_propApplyToSprites);
EditorGUILayout.PropertyField(_propRelativeToTransformScale);
GUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(_propRelativeFontSize);
if (_propRelativeFontSize.floatValue > 0f)
{
if (GUILayout.Button("Remove", GUILayout.ExpandWidth(false)))
{
_propRelativeFontSize.floatValue = 0f;
}
}
else
{
var textMeshPro = (target as Behaviour).GetComponent<TextMeshProUGUI>();
if (textMeshPro.fontSize != _propRelativeFontSize.floatValue)
{
if (GUILayout.Button("Set From Text", GUILayout.ExpandWidth(false)))
{
_propRelativeFontSize.floatValue = textMeshPro.fontSize;
}
}
}
GUILayout.EndHorizontal();
EditorGUILayout.PropertyField(_propUpdateOnTransform);
EnumAsToolbarCompact(_propRenderSpace);
EditorGUILayout.Space();
GUILayout.Label(Content_Filters, EditorStyles.boldLabel);
EditorGUI.indentLevel++;
if (_propFilters.arraySize == 0)
{
EditorGUILayout.HelpBox("No filters currently added", MessageType.Error, true);
}
if (_unusedFilterComponents.Count > 0)
{
{
var unusedList = string.Empty;
foreach (var component in _unusedFilterComponents)
{
unusedList += "\n - " + component.GetType().Name;
}
EditorGUILayout.HelpBox("There are " + _unusedFilterComponents.Count + " unused filter components:" + unusedList, MessageType.Warning, true);
}
GUILayout.BeginHorizontal();
if (GUI.Button(EditorGUI.IndentedRect(EditorGUILayout.GetControlRect()), "Add " + _unusedFilterComponents.Count + " Unused Filters"))
{
AddUnusedFilters();
UpdateFilterList();
}
if (GUI.Button(EditorGUI.IndentedRect(EditorGUILayout.GetControlRect()), "Remove " + _unusedFilterComponents.Count + " Unused Filters"))
{
foreach (var component in _unusedFilterComponents)
{
ObjectHelper.Destroy(component);
}
_unusedFilterComponents.Clear();
UpdateFilterList();
}
GUILayout.EndHorizontal();
}
EditorGUI.BeginChangeCheck();
_reorderableList.DoLayoutList();
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
serializedObject.SetIsDifferentCacheDirty();
serializedObject.Update();
UpdateFilterList();
}
if (_hasEmptyFilterSlots)
{
EditorGUILayout.HelpBox("There are empty slots in the filter list", MessageType.Info, true);
if (GUI.Button(EditorGUI.IndentedRect(EditorGUILayout.GetControlRect()), "Remove Empty Slots"))
{
RemoveEmptySlots();
UpdateFilterList();
}
EditorGUILayout.Space();
}
serializedObject.ApplyModifiedProperties();
if (_filterTypesNames != null && _filterTypesNames.Length > 0)
{
EditorGUILayout.PrefixLabel(Content_AddFilter);
EditorGUILayout.BeginHorizontal();
// Show dropdown list of filters that can be added
_selectedTypeIndex = EditorGUILayout.Popup(_selectedTypeIndex, _filterTypesNames);
// Show button to add selected filter
if (GUILayout.Button(Content_Add))
{
serializedObject.Update();
foreach (var obj in this.targets)
{
var so = new SerializedObject(obj);
var filterStack = obj as FilterStackTextMeshPro;
var prop = so.FindProperty("_filters");
var component = ObjectFactory.AddComponent(filterStack.gameObject, _filterTypes[_selectedTypeIndex]);
InsertIntoList(prop, component);
so.ApplyModifiedProperties();
}
serializedObject.ApplyModifiedProperties();
}
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
}
private static string GetDisplayNameForComponentType(System.Type type)
{
string title = null;
// Use the AddComponentMenu attribute to get the nice name for the component
var attr = type.GetCustomAttributes(typeof(AddComponentMenu), false).FirstOrDefault() as AddComponentMenu;
if (attr != null)
{
title = attr.componentMenu?.Trim();
if (!string.IsNullOrEmpty(title))
{
var lastPathCharIndex = title.LastIndexOf('/');
if (lastPathCharIndex >= 0)
{
if (lastPathCharIndex < title.Length - 1)
{
title = title.Substring(lastPathCharIndex + 1);
}
}
}
}
// Fallback to just use the type name
if (string.IsNullOrEmpty(title))
{
title = type.Name;
}
return title;
}
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 9f09a1b297cd32143beb00bf4debd069
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 274847
packageName: UIFX - Glow Filter
packageVersion: 1.9.10
assetPath: Assets/ChocDino/UIFX-TMP/Editor/Scripts/FilterStackTextMeshProEditor.cs
uploadId: 833966