Files
XUniWindowController/Editor/Scripts/UniWindowControllerEditor.cs
T
2026-07-10 12:20:54 +08:00

591 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* UniWindowControllerEditor.cs
*
* Author: Kirurobo http://x.com/kirurobo
* License: MIT
*/
// 启用了程序集定义后,如果构建时没有 Editor 类会报错。
// 因此整体用 UNITY_EDITOR 包裹,没有时直接忽略
#if UNITY_EDITOR
using System.Linq;
using UnityEngine;
using UnityEditor;
using System.Reflection;
using UnityEngine.Rendering;
namespace Kirurobo
{
/// <summary>
/// UniWindowController 的编辑器自定义部分
/// </summary>
[CustomEditor(typeof(UniWindowController))]
public class UniWindowControllerEditor : Editor
{
/// <summary>
/// 用于显示光标下方颜色的属性
/// </summary>
SerializedProperty pickedColor;
/// <summary>
/// 游戏视图窗口
/// </summary>
private EditorWindow gameViewWindow;
/// <summary>
/// 是否已关闭项目设置相关的警告
private bool isWarningDismissed = false;
/// <summary>
/// 是否已关闭 URP 相关警告
/// </summary>
private bool isUrpWarningDismissed = true;
/// <summary>
/// URP 是否有效
/// </summary>
private bool hasUrp = false;
void OnEnable()
{
LoadSettings();
pickedColor = serializedObject.FindProperty("pickedColor");
// 判断 URP 是否有效
hasUrp = GetUrpSettings();
}
void OnDisable()
{
SaveSettings();
}
/// <summary>
/// 检测 URP 是否有效
/// </summary>
/// <returns></returns>
private bool GetUrpSettings()
{
var renderPipelineAsset = GraphicsSettings.defaultRenderPipeline;
if (renderPipelineAsset == null || renderPipelineAsset.GetType().Name != "UniversalRenderPipelineAsset")
{
// URP 未设置
return false;
}
return true;
}
private void LoadSettings()
{
isWarningDismissed = EditorUserSettings.GetConfigValue("WindowController_IS_WARNING DISMISSED") == "1";
}
private void SaveSettings()
{
EditorUserSettings.SetConfigValue("WindowController_IS_WARNING DISMISSED", isWarningDismissed ? "1" : "0");
}
/// <summary>
/// 自定义检视面板的显示
/// </summary>
/// <description>
/// 显示参考信息以及推荐设置的更改栏。
/// </description>
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
// 如果获取到了光标下的颜色,则显示其透明度作为参考
if (pickedColor != null)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.LabelField("选取的Alpha值", pickedColor.colorValue.a.ToString("P0"));
EditorGUI.EndDisabledGroup();
}
// 显示 Project Settings 的推荐设置
isWarningDismissed = ShowPlayerSettingsValidation(isWarningDismissed);
// 显示 URP 相关推荐设置
isUrpWarningDismissed = ShowUrpSettingsValidation(isUrpWarningDismissed);
}
/// <summary>
/// 显示 Project Settings 相关推荐设置的自动配置栏
/// </summary>
private bool ShowPlayerSettingsValidation(bool dismissed) {
// 以下是 Project Settings 相关
EditorGUILayout.Space();
bool enableValidation = EditorGUILayout.Foldout(!dismissed, "播放器设置验证");
// 记录是否启用检查
if (enableValidation == dismissed)
{
dismissed = !enableValidation;
}
// 检查推荐设置
//if (!isWarningDismissed)
if (enableValidation)
{
if (ValidateSettings(false))
{
// 应用所有推荐设置
GUI.backgroundColor = Color.green;
if (GUILayout.Button(
"✔ 将所有设置修复为推荐值",
GUILayout.MinHeight(25f)
))
{
ValidateSettings(true);
}
// 关闭验证
GUI.backgroundColor = Color.yellow;
if (GUILayout.Button(
"✘ 关闭此验证",
GUILayout.MinHeight(25f)
))
{
dismissed = true;
//SaveSettings(); // 如果需要立即保存,取消注释
}
EditorGUILayout.Space();
}
else
{
GUI.color = Color.green;
GUILayout.Label("OK");
}
// 打开播放器设置页面
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.color = Color.white;
GUI.backgroundColor = Color.white;
if (GUILayout.Button(
"打开播放器设置",
GUILayout.MinHeight(25f), GUILayout.Width(200f)
))
{
SettingsService.OpenProjectSettings("Project/Player");
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
}
return dismissed;
}
/// <summary>
/// 显示 URP 相关推荐设置的自动配置栏
/// </summary>
private bool ShowUrpSettingsValidation(bool dismissed) {
// 如果 URP 未启用则不执行任何操作
if (!hasUrp) return dismissed;
// 以下是 URP 相关自动设置
EditorGUILayout.Space();
bool enableValidation = EditorGUILayout.Foldout(!dismissed, "URP 设置验证");
// 记录是否启用检查
if (enableValidation == dismissed)
{
dismissed = !enableValidation;
}
// 检查推荐设置
if (enableValidation)
{
if (ValidateUrpSettings(false))
{
// 应用所有推荐设置
GUI.backgroundColor = Color.green;
if (GUILayout.Button(
"✔ 将所有设置修复为推荐值",
GUILayout.MinHeight(25f)
))
{
ValidateUrpSettings(true);
}
// 关闭验证
GUI.backgroundColor = Color.yellow;
if (GUILayout.Button(
"✘ 关闭此验证",
GUILayout.MinHeight(25f)
))
{
dismissed = true;
}
EditorGUILayout.Space();
}
else
{
GUI.color = Color.green;
GUILayout.Label("OK");
}
EditorGUILayout.Space();
}
return dismissed;
}
private delegate void FixMethod();
/// <summary>
/// 显示或修复设置
/// </summary>
/// <param name="message">警告消息</param>
/// <param name="fixAction">修复操作</param>
/// <param name="silentFix">false: 显示警告和修复按钮, true: 静默修复</param>
private void FixSetting(string message, FixMethod fixAction, bool silentFix = false)
{
if (silentFix)
{
// 修复
fixAction.Invoke();
}
else
{
// 显示消息和修复按钮
EditorGUILayout.BeginHorizontal();
EditorGUILayout.HelpBox(message, MessageType.Warning, true);
GUILayout.FlexibleSpace();
EditorGUILayout.BeginVertical();
EditorGUILayout.Space();
if (GUILayout.Button("修复", GUILayout.Width(60f))) { fixAction.Invoke(); }
//GUILayout.FlexibleSpace();
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
}
/// <summary>
/// 仅显示建议
/// </summary>
/// <param name="message">警告消息</param>
private void ShowInfo(string message, Object target = null)
{
// 显示消息和定位按钮
EditorGUILayout.BeginHorizontal();
EditorGUILayout.HelpBox(message, MessageType.Info, true);
GUILayout.FlexibleSpace();
// 无法自动设置的对象,仅在项目窗口中标识
if (target != null)
{
EditorGUILayout.BeginVertical();
EditorGUILayout.Space();
if (GUILayout.Button("定位", GUILayout.Width(60f))) { EditorGUIUtility.PingObject(target); }
//GUILayout.FlexibleSpace();
EditorGUILayout.EndVertical();
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 验证播放器设置
/// </summary>
/// <param name="silentFix">false: 显示警告和修复按钮, true: 静默修复</param>
/// <returns>如果有无效项则返回 true</returns>
private bool ValidateSettings(bool silentFix = false)
{
bool invalid = false;
// 在后台运行。点击穿透切换等功能需要此设置
if (!PlayerSettings.runInBackground)
{
invalid = true;
FixSetting(
"强烈建议启用'后台运行'。",
() => PlayerSettings.runInBackground = true,
silentFix
);
}
// 设为可调整大小的窗口。非必需,但当窗口边框无效时大小会变化,因此可调整更自然
if (!PlayerSettings.resizableWindow)
{
invalid = true;
FixSetting(
"建议启用'可调整窗口大小'。",
() => PlayerSettings.resizableWindow = true,
silentFix
);
}
// 设为窗口模式而非全屏
#if UNITY_2018_1_OR_NEWER
// Unity 2018 起全屏指定的行为发生了变化
if (PlayerSettings.fullScreenMode != FullScreenMode.Windowed)
{
invalid = true;
FixSetting(
"在'全屏模式'中选择'窗口化'。",
() => PlayerSettings.fullScreenMode = FullScreenMode.Windowed,
silentFix
);
}
#else
if (PlayerSettings.defaultIsFullScreen)
{
invalid = true;
FixSetting(
"不建议启用'默认全屏'。",
() => PlayerSettings.defaultIsFullScreen = false,
silentFix
);
}
#endif
// 禁止全屏与窗口模式的切换
if (PlayerSettings.allowFullscreenSwitch)
{
invalid = true;
FixSetting(
"禁止全屏切换。",
() => PlayerSettings.allowFullscreenSwitch = false,
silentFix
);
}
// Windows 下不禁用 Use DXGI Flip Mode Swapchain 则无法透明
// ↓Unity 2019.1.6 以下应该没有 useFlipModelSwapchain
// 因此为了排除这些版本而逐一列出,但可能不需要支持到这么老的版本
#if UNITY_2019_1_6
#elif UNITY_2019_1_5
#elif UNITY_2019_1_4
#elif UNITY_2019_1_3
#elif UNITY_2019_1_2
#elif UNITY_2019_1_1
#elif UNITY_2019_1_0
#elif UNITY_2019_1_OR_NEWER
// Unity 2019.1.7 以上推荐在 Player 设置中禁用 Use DXGI Flip...
if (PlayerSettings.useFlipModelSwapchain)
{
invalid = true;
FixSetting(
"禁用'使用 DXGI 翻转模式交换链'以使窗口透明。",
() => PlayerSettings.useFlipModelSwapchain = false,
silentFix
);
}
// Direct3D12 不支持透明窗口,建议从 Graphics APIs for Windows 中移除
if (PlayerSettings.GetUseDefaultGraphicsAPIs(BuildTarget.StandaloneWindows))
{
// 自动选择图形 API 时也发出警告
ShowInfo(
"Direct3D12 不支持透明窗口。" +
"请考虑在播放器设置中使用 Direct3D11 替代'自动图形 API for Windows'设置。",
null
);
}
else if (PlayerSettings.GetGraphicsAPIs(BuildTarget.StandaloneWindows).Contains(GraphicsDeviceType.Direct3D12))
{
// Graphics APIs for Windows 中包含 Direct3D12 时发出警告
ShowInfo(
"Direct3D12 不支持透明窗口。" +
"请从播放器设置的'图形 API for Windows'中移除 Direct3D12。",
null
);
}
#endif
return invalid;
}
/// <summary>
/// 验证播放器设置
/// </summary>
/// <param name="silentFix">false: 显示警告和修复按钮, true: 静默修复</param>
/// <returns>如果有无效项则返回 true</returns>
private bool ValidateUrpSettings(bool silentFix = false)
{
bool invalid = false;
// 如果启用了 Universal Render Pipeline,建议禁用 HDR
foreach (var cam in Camera.allCameras)
{
if (cam.allowHDR) {
string name = cam.name;
invalid = true;
FixSetting(
$"{name}:禁用摄像机中的'HDR'以使窗口透明。",
() => cam.allowHDR = false,
silentFix
);
}
if (cam.allowMSAA) {
string name = cam.name;
invalid = true;
FixSetting(
$"{name}:禁用摄像机中的'MSAA'以使窗口透明。",
() => cam.allowMSAA = false,
silentFix
);
}
}
var urpAsset = GraphicsSettings.defaultRenderPipeline;
if (hasUrp && urpAsset != null)
{
// hasUrp == true 时 urpAsset 应该是 UniversalRenderPipelineAsset,因此应该存在 allowPostProcessAlphaOutput
var alphaProcessingProperty = urpAsset.GetType().GetProperty("allowPostProcessAlphaOutput", BindingFlags.Public | BindingFlags.Instance);
if (alphaProcessingProperty != null)
{
var alphaProcessing = alphaProcessingProperty.GetValue(urpAsset);
if (!(bool)alphaProcessing)
{
invalid = true;
ShowInfo(
"在 URP 资源中启用'Alpha 处理'",
urpAsset
);
}
}
}
return invalid;
}
}
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class UniWindowControllerReadOnlyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GUI.enabled = false;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
}
}
/// <summary>
/// 设置布尔属性为可编辑
/// 参考: http://ponkotsu-hiyorin.hateblo.jp/entry/2015/10/20/003042
/// 参考: https://forum.unity.com/threads/c-class-property-with-reflection-in-propertydrawer-not-saving-to-prefab.473942/
/// </summary>
[CustomPropertyDrawer(typeof(EditablePropertyAttribute))]
public class UniWindowControllerDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
//base.OnGUI(position, property, label);
Object obj = property.serializedObject.targetObject;
// 如果设置了 Range(min, max) 则获取
FieldInfo fieldInfo = obj.GetType().GetField(
property.name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static
);
var rangeAttrs = fieldInfo?.GetCustomAttributes(typeof(RangeAttribute), true) as RangeAttribute[];
RangeAttribute range = (rangeAttrs?.Length > 0 ? rangeAttrs.First() : null);
if (EditorApplication.isPlayingOrWillChangePlaymode)
{
// 以 '_' 开头作为操作条件的依据
if (property.name[0] == '_')
{
string propertyName = property.name.Substring(1); // 获取去掉 '_' 后的属性名
PropertyInfo info = obj.GetType().GetProperty(propertyName);
MethodInfo getMethod = default(MethodInfo);
MethodInfo setMethod = default(MethodInfo);
if (info.CanRead) { getMethod = info.GetGetMethod(); }
if (info.CanWrite) { setMethod = info.GetSetMethod(); }
if (property.type == "bool")
{ var oldValue = property.boolValue;
if (getMethod != null)
{
oldValue = (bool)getMethod.Invoke(obj, null);
}
GUI.enabled = (setMethod != null);
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
var newValue = property.boolValue;
if ((setMethod != null) && (oldValue != newValue))
{
setMethod.Invoke(obj, new[] { (object)newValue });
}
}
else if (property.type == "float")
{
var oldValue = property.floatValue;
if (getMethod != null)
{
oldValue = (float) getMethod.Invoke(obj, null);
}
GUI.enabled = (setMethod != null);
if (range != null)
{
EditorGUI.Slider(position, property, range.min, range.max, label);
}
else
{
EditorGUI.PropertyField(position, property, label, true);
}
GUI.enabled = true;
var newValue = property.floatValue;
if ((setMethod != null) && (oldValue != newValue))
{
setMethod.Invoke(obj, new[] {(object) newValue});
}
}
else
{
// bool、float 以外的类型暂不支持,设为只读
GUI.enabled = false;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
}
}
else
{
// 只读
GUI.enabled = false;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
}
}
else
{
// 有 Range 指定则显示为滑块
if (range != null)
{
EditorGUI.Slider(position, property, range.min, range.max, label);
}
else
{
EditorGUI.PropertyField(position, property, label, true);
}
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, true);
}
}
}
#endif