backup, and update vessel

This commit is contained in:
2025-07-23 13:56:37 +08:00
parent 410b277252
commit b8c6890e04
34 changed files with 237 additions and 1740 deletions
@@ -1,18 +0,0 @@
{
"name": "Lrss3.ConfigUIElement",
"rootNamespace": "",
"references": [
"GUID:6055be8ebefd69e48b49212b09b47b2f",
"GUID:b1727b829ae4f0641acc8ad28ed624c2",
"GUID:343deaaf83e0cee4ca978e7df0b80d21"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 25409440eb1d34849a42df21f5989356
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: d53a566643e99d74292ec8e451eb7356
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,62 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace LRC
{
public class ConfigButton : ConfigSelectableItem
{
#region 属性字段
public Button targetButton;
public override bool AllowInput
{
get => base.AllowInput;
set
{
if (targetButton != null)
targetButton.interactable = !value;
base.AllowInput = value;
}
}
#endregion
#region 实现
protected override void SetValue(object newValue)
{
throw new System.NotImplementedException();
}
public override void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
base.RefreshValueWithoutEvent(newValue, forceSet);
}
private bool _doOnce = false;
protected override void Initialization_ChildConstruction(UIBehaviour component)
{
base.Initialization_ChildConstruction(component);
var name = component.transform.name;
if (name is not "button") return;
if (_doOnce)
{
Debug.LogError("存在重复命名的属性模块:" + name);
return;
}
if (component is Button button)
{
_doOnce = true;
if (targetButton != null)
targetButton = button;
}
}
#endregion
}
}
@@ -1,153 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace LRC
{
/// <summary>
/// 一种数值增减输入框,将使用浮点值作为默认
/// </summary>
public class ConfigDigitalInputField : ConfigSelectableItem
{
#region 属性字段
#if UNITY_EDITOR
public float editorInput;
#endif
public TMP_InputField targetInput;
public Button digitalAddButton;
public Button digitalSubButton;
public double maxValue = 114514;
public double minValue = -114515;
public int digits = 6;
/// <summary>
/// 增量
/// </summary>
public double delta = 1;
public override bool AllowInput
{
get => base.AllowInput;
set
{
if (targetInput != null)
targetInput.readOnly = !value;
if (digitalAddButton != null)
digitalAddButton.interactable = value;
if (digitalSubButton != null)
digitalSubButton.interactable = value;
base.AllowInput = value;
}
}
#endregion
#region 生命周期
protected override void OnValidate()
{
base.OnValidate();
#if UNITY_EDITOR
if (targetInput != null)
targetInput.text = editorInput.ToString();
#endif
}
protected override void Awake()
{
base.Awake();
if (ReferenceEquals(targetInput, null))
{
Debug.LogError($"{name}配置项目条目的基本元素不存在");
return;
}
targetInput.onSelect.AddListener(a =>
{
WhenStartEdit();
});
targetInput.onValueChanged.AddListener(o =>
{
double value = 0;
if (double.TryParse(o, out value))
{
SetValue(value);
}
});
targetInput.onEndEdit.AddListener(delegate
{
WhenEndEdit();
});
if (!ReferenceEquals(digitalAddButton, null))
digitalAddButton.onClick.AddListener(delegate
{
SetValue(CastDigitalNumber(Value) + delta);
RefreshFormatValue(Value);
});
if (!ReferenceEquals(digitalSubButton, null))
digitalSubButton.onClick.AddListener(delegate
{
SetValue(CastDigitalNumber(Value) - delta);
RefreshFormatValue(Value);
});
}
#endregion
#region 实现
protected override void SetValue(object newValue)
{
base.SetValue(CastDigitalNumber(newValue, minValue, maxValue, digits));
}
public override void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
base.RefreshValueWithoutEvent(CastDigitalNumber(newValue, minValue, maxValue, digits), forceSet);
}
protected override void Initialization_ChildConstruction(UIBehaviour component)
{
base.Initialization_ChildConstruction(component);
if (component is TMP_InputField inputField)
{
if (targetInput != null)
targetInput = inputField;
}
else if(component is Button button)
{
var name = component.transform.name;
switch (name)
{
case "subbutton":
case "SubButton":
if (digitalSubButton != null)
digitalSubButton = button;
break;
case "addbutton":
case "AddButton":
if (digitalAddButton != null)
digitalAddButton = button;
break;
}
}
}
protected override void RefreshFormatValue(object newValue)
{
base.RefreshFormatValue(newValue);
targetInput.text = CastDigitalNumber(newValue).ToString(numberFormat);
}
#endregion
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d84fd414ac6743e46909249427b18364
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,78 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.UI;
namespace LRC
{
public class ConfigDropdown : ConfigSelectableItem
{
#region ÊôÐÔ×ÖÎ
public TMP_Dropdown targetDropdown;
#endregion
#region ÉúÃüÖÜÆÚ
protected override void Awake()
{
base.Awake();
targetDropdown.onValueChanged.AddListener(index =>
{
SetValue(index);
});
}
public void InitializationDropdown(IEnumerable<string> options)
{
targetDropdown.options = options
.Select(option => new TMP_Dropdown.OptionData(option)).ToList();
targetDropdown.RefreshShownValue();
if (IsValueInitNull)
RefreshValueWithoutEvent(0);
}
#endregion
#region ʵÏÖ
protected override void SetValue(object newValue)
{
base.SetValue(CastIntNumber(newValue));
}
public override void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
base.RefreshValueWithoutEvent(CastIntNumber(newValue), forceSet);
}
protected override void Initialization_ChildConstruction(UIBehaviour component)
{
base.Initialization_ChildConstruction(component);
var name = component.transform.name;
if (name is not "dropdown") return;
if (component is TMP_Dropdown dropdown)
{
if (targetDropdown != null)
targetDropdown = dropdown;
}
}
protected override void RefreshFormatValue(object newValue)
{
base.RefreshFormatValue(newValue);
if (newValue == null)
return;
targetDropdown.value = CastIntNumber(newValue);
targetDropdown.RefreshShownValue();
}
#endregion
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 471d16b38f4449a46b9fcf5b312f65d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,712 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.UI;
using XericLibrary.Runtime.CustomEditor;
using XericLibrary.Runtime.MacroLibrary;
namespace LRC
{
/// <summary>
/// 一种用于基础面板设置条目的类
/// <code>
/// 在此类中提供了双向的属性委托修改,并且拥有脏标记。
/// 该类在初始化时以输入为优先。
/// 脏标记仅当界面首次发生改变时进行记录,并不会影响值。
/// </code>
/// </summary>
public abstract class ConfigSelectableItem : MonoBehaviour
{
#region 静态事件
private static event Action<bool, bool> ForceUpdateAllConfigEvent;
/// <summary>
/// 强制所有激活的成员产生请求更新的事件(比如初始化时,外部更新时)
/// </summary>
/// <param name="resetDirty"></param>
public static void ForceUpdateAll(bool resetDirty = false, bool forceSet = false)
{
ForceUpdateAllConfigEvent?.Invoke(resetDirty, forceSet);
}
private static event Action ForceSetAllSourceValueEvent;
/// <summary>
/// 强制所有激活的成员产生请求设置的事件(比如按下应用按钮时)
/// </summary>
public static void ForceSetAllSourceValue()
{
ForceSetAllSourceValueEvent?.Invoke();
}
/// <summary>
/// 数值遭到修改时
/// </summary>
public static event Action<ConfigSelectableItem> OnAnyValueDirty;
/// <summary>
/// 当数值收到修改时,产生事件,其中值的含义是修改前的值
/// </summary>
public static event Action<ConfigSelectableItem, object> OnAnyValueChange;
#endregion
#region 静态成员
/// <summary>
/// 所有有效的配置项目,当项目被隐藏(不处于激活状态时)就会从列表中删除。
/// </summary>
private static readonly List<ConfigSelectableItem> ConfigItemList = new List<ConfigSelectableItem>();
/// <summary>
/// 项目列表有效成员的数量
/// </summary>
public static int ConfigItemListCount => ConfigItemList.Count;
/// <summary>
/// 获取
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public static ConfigSelectableItem GetConfigItemByIndex(int index) => ConfigItemList[index];
public static IEnumerable<ConfigSelectableItem> ConfigItemLists => ConfigItemList;
private static bool autoUpdateValueAll;
/// <summary>
/// 自动更新所有数值
/// </summary>
public static bool AutoUpdateValueAll
{
get => autoUpdateValueAll;
set
{
foreach (var item in ConfigItemList)
item.autoUpdateValue = value;
autoUpdateValueAll = value;
}
}
#endregion
#region 状态
/// <summary>
/// 数值遭到修改时,标记脏,这样在后续的应用中有可以保存的变化
/// <code>
/// 建议:这是可选的
/// 这会立刻发生在值修改时,且比OnValueChange更早,但在复位之前只会调用一次。
/// </code>
/// </summary>
public event Action<ConfigSelectableItem> OnValueDirty;
/// <summary>
/// 当数值收到修改时,产生事件,其中值的含义是修改前的值
/// <code>
/// 建议:这是可选的
/// 这会立刻发生在值修改时,所以如果需要进行页面刷新的话,建议进行等待
/// </code>
/// </summary>
public event Action<ConfigSelectableItem, object> OnValueChange;
/// <summary>
/// 当数值结束修改时
/// </summary>
public event Action<ConfigSelectableItem, object> OnEndEdit;
/// <summary>
/// 当前属性请求获取属性值
/// <code>
/// 建议:这是必要的
/// 当发生刷新事件时,将产生调用。
/// 含义是将外部的值设定到此。
/// </code>
/// </summary>
public event Func<ConfigSelectableItem, object> GetSourceValue;
/// <summary>
/// 当前属性请求设置属性值
/// <code>
/// 建议:这是必要的
/// 当被应用时,比如调用了ForceSetAllSourceValue,将产生调用。
/// 含义是将此处的界面值返回到属性中。
/// 警告:不要在这里面调用ForceSetAllSourceValue,SetSourceValueRequest等方法,
/// 这会造成死循环以及内存溢出等问题。
/// </code>
/// </summary>
public event Action<ConfigSelectableItem, object> SetSourceValue;
/// <summary>
/// 当前属性的自动更新请求获取
/// <code>
/// 建议:这是条件可选的
/// 当被管理的目标中存在Toggle时(对应autoUpdateValueToggle项目)
/// 此处将用于从外部的值是否勾选值设定到此
/// </code>
/// </summary>
public event Func<ConfigSelectableItem, bool> GetSourceValueAutoUpdate;
/// <summary>
/// 当前属性的自动更新请求设置
/// <code>
/// 建议:这是条件可选的
/// 当被管理的目标中存在Toggle时(对应autoUpdateValueToggle项目)
/// 此处将用于设置到外部的是否勾选值设定到此
/// </code>
/// </summary>
public event Action<bool> SetSourceValueAutoUpdate;
/// <summary>
/// 请求此属性设置值(将值应用到寄存中)
/// </summary>
public void SetSourceValueRequest()
{
SetSourceValue?.Invoke(this, Value);
}
#endregion
#region 属性字段
/// <summary>
/// 项目对应的标记tag
/// </summary>
[Rename("映射标记")]
public string configTag;
/// <summary>
/// 设定是否有效。
/// 如果无效,则不允许输入
/// </summary>
[SerializeField]
[Rename("允许输入")]
private bool allowInput = true;
/// <summary>
/// 自动更新数值
/// </summary>
[SerializeField]
[Rename("主动更新")]
private bool autoUpdateValue = true;
/// <summary>
/// 允许阻塞自动更新,表示此项目可以脱离自动更新状态选框的状态运行
/// </summary>
[SerializeField]
[Rename("允许阻塞自动更新")]
private bool allowBlockUpdate = true;
/// <summary>
/// 阻塞自动更新,一般由程序自动根据焦点更新
/// </summary>
[SerializeField]
[Rename("阻塞自动更新")]
private bool forceBlockUpdate = false;
/// <summary>
/// 格式化文本
/// </summary>
public string numberFormat = "0.##";
/// <summary>
/// 项目标题组件
/// </summary>
public TextMeshProUGUI titleLabel;
#if UNITY_EDITOR
[SerializeField]
#endif
private string titleTextContext;
/// <summary>
/// 帮助文本组件
/// </summary>
public TextMeshProUGUI helpLabel;
#if UNITY_EDITOR
[SerializeField]
#endif
private string helpTextContext;
/// <summary>
/// 单位文本组件
/// </summary>
public TextMeshProUGUI unitLabel;
#if UNITY_EDITOR
[SerializeField]
#endif
private string unitTextContext;
/// <summary>
/// 条目前的选框,与autoUpdateValue对应
/// </summary>
[Rename("自动更新状态选框")]
public Toggle autoUpdateValueToggle;
/// <summary>
/// 反转选框状态
/// </summary>
[Rename("反转自动更新按钮状态")]
public bool autoUpdateValueToggleReversalState;
[Rename("反转自动更新状态")]
public bool autoUpdateValueReversalState;
/// <summary>
/// 脏属性自动复位。
/// 当属性被重新开关时,将取消脏状态
/// </summary>
[Rename("脏标记自动复位")]
public bool dirtyAutoReset = false;
// ==== 属性 ==== //
/// <summary>
/// 设定是否输入有效
/// </summary>
public virtual bool AllowInput
{
get => allowInput;
set => allowInput = value;
}
/// <summary>
/// 此属性项目已经初始化
/// </summary>
[HideInInspector]
public bool IsValueInitNull { get; private set; } = true;
// [Obsolete("请使用访问器")]
private bool _isDirty;
/// <summary>
/// 脏属性标记,首次脏会进行广播
/// </summary>
private bool IsDirty
{
get => _isDirty;
set
{
if (_isDirty)
{
if (!value)
_isDirty = false;
return;
}
if (!value) return;
_isDirty = true;
OnAnyValueDirty?.Invoke(this);
OnValueDirty?.Invoke(this);
}
}
/// <summary>
/// 设定自动更新状态,并同步选框
/// <code>
/// 为了书写方便,保存的状态是和外部暂存值一致,
/// 此访问器将作为本地状态的标准访问。
///
/// 访问将自动进行转换,而设置则不进行转换,
/// 所以应当使用此访问器进行设置或内部访问,而与外部通讯应当使用字段。
///
/// 如果需要将此内部判断状态转为界面状态:
/// 应当先通过autoUpdateValueReversalState转为暂存值,
/// 然后再通过autoUpdateValueToggleReversalState转为界面值。
/// </code>
/// </summary>
public bool AutoUpdateValue
{
get => autoUpdateValueReversalState ^ autoUpdateValue;
set
{
autoUpdateValue = value;
if (autoUpdateValueToggle != null)
autoUpdateValueToggle.isOn = autoUpdateValueToggleReversalState ^ value;
}
}
/// <summary>
/// 设定阻塞自动更新
/// </summary>
public bool ForceBlockUpdate
{
get => forceBlockUpdate;
set => forceBlockUpdate = value;
}
/// <summary>
/// 项目标题
/// </summary>
public string TitleName
{
get => titleLabel.text;
set => titleLabel.text = value;
}
/// <summary>
/// 帮助文本,再默认情况下应该是不显示的通过赋予空文本来关闭显示
/// </summary>
public string HelpName
{
get
{
if (helpLabel != null) return helpLabel.text;
return null;
}
set
{
if (helpLabel != null)
{
if (value != null)
{
helpLabel.text = value;
helpLabel.gameObject.SetActive(true);
}
else
helpLabel.gameObject.SetActive(false);
}
}
}
public string UnitName
{
get => unitLabel.text;
set => unitLabel.text = value;
}
[SerializeField]
// [Obsolete("请使用访问器")]
private object _value;
/// <summary>
/// 项目值
/// </summary>
public object Value
{
get => _value;
protected set
{
if (IsValueInitNull && value != null)
IsValueInitNull = false;
_value = value;
}
}
// ==== 暂存 ==== //
public FieldInfo FieldInfo;
public Type FieldType;
#endregion
#region 生命周期
protected virtual void OnValidate()
{
if (titleLabel != null)
TitleName = titleTextContext;
if (helpLabel != null)
HelpName = helpTextContext;
if (unitLabel != null)
UnitName = unitTextContext;
}
protected virtual void Awake()
{
foreach (var child in transform.GetChildren())
{
var component = child.GetComponent<UIBehaviour>();
if (component != null)
Initialization_ChildConstruction(component);
}
Initialization_EventBinding();
}
protected virtual void OnEnable()
{
ConfigItemList.Add(this);
ForceUpdateAllConfigEvent += ForceUpdate;
ForceSetAllSourceValueEvent += SetSourceValueRequest;
if (dirtyAutoReset)
IsDirty = false;
}
protected virtual void OnDisable()
{
ConfigItemList.Remove(this);
ForceUpdateAllConfigEvent -= ForceUpdate;
ForceSetAllSourceValueEvent -= SetSourceValueRequest;
}
protected virtual void Start()
{
ForceUpdate(true);
}
private void Update()
{
if (IsValueInitNull || (AutoUpdateValue && !(allowBlockUpdate && forceBlockUpdate)))
{
ForceUpdate();
}
}
private void OnDestroy()
{
ForceUpdateAllConfigEvent -= ForceUpdate;
}
#endregion
#region 换算与转换
protected static double CastDigitalNumber(object newValue)
{
if (double.TryParse(newValue.ToString(), out var value))
{
return value;
}
return -1;
}
/// <summary>
/// 转换到数值类型(数值钳制与四舍五入)
/// </summary>
/// <param name="newValue"></param>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
/// <param name="digits"></param>
/// <returns></returns>
protected static double CastDigitalNumber(object newValue, double minValue, double maxValue, int digits)
{
// var resultValue = newValue switch
// {
// bool boolValue => boolValue ? 1 : 0,
// short intValue => intValue,
// int intValue => intValue,
// long intValue => intValue,
// float floatValue => Math.Round(floatValue, digits),
// double doubleValue => Math.Round(doubleValue, digits),
// _ => 0
// };
#if UNITY_EDITOR
if (newValue == null)
{
Debug.LogError($"参数转换错误:目标是一个空值,请在发生空值时提前退出");
return -1;
}
#endif
if (double.TryParse(newValue.ToString(), out var value))
{
var resultValue = Math.Round(value, digits);
return Math.Clamp(resultValue, minValue, maxValue);
}
Debug.LogError($"参数转换错误:{newValue}可能不是一个有效的数值");
return -1;
}
protected static int CastIntNumber(object newValue)
{
var resultValue = Convert.ToInt32(newValue);
return resultValue;
}
protected static Vector2 CastVector2(object newValue)
{
return newValue switch
{
Vector2 vec2Value => vec2Value,
System.Numerics.Vector2 sysVec2Value => new Vector2(sysVec2Value.X, sysVec2Value.Y),
_ => Vector2.zero
};
}
protected static Vector3 CastVector3(object newValue)
{
return newValue switch
{
Vector3 vec3Value => vec3Value,
System.Numerics.Vector3 sysVec3Value => new Vector3(sysVec3Value.X, sysVec3Value.Y, sysVec3Value.Z),
_ => Vector3.zero
};
}
#endregion
#region 初始化方法
/// <summary>
/// 可选的基本初始化
/// </summary>
/// <param name="autoUpdateValue">设定自动更新</param>
protected void Initialization(bool autoUpdateValue = true)
{
AutoUpdateValue = autoUpdateValue;
}
private bool _doOnce = false;
/// <summary>
/// 构建属性,在初始化过程中,通过识别项目来构建域
/// </summary>
protected virtual void Initialization_ChildConstruction(UIBehaviour component)
{
// 跟踪节点的名称
var name = component.gameObject.name;
// 初始化自动更新对勾
if (name == "toggle" && component is Toggle toggle)
{
if (autoUpdateValueToggle == null)
autoUpdateValueToggle = toggle;
return;
}
if (name is not ("title" or "name")) return;
if (_doOnce)
{
Debug.LogError("存在重复命名的属性模块:" + name);
return;
}
if (component is TextMeshProUGUI textMeshProUGUI)
{
_doOnce = true;
if (titleLabel != null)
titleLabel = textMeshProUGUI;
}
}
/// <summary>
/// 构建事件,在初始化结束前,需要对所有动态绑定的按键进行事件绑定
/// </summary>
protected virtual void Initialization_EventBinding()
{
// 无论如何,事件需要被注册
if (autoUpdateValueToggle != null)
{
autoUpdateValueToggle.onValueChanged.AddListener(a =>
{
autoUpdateValue = autoUpdateValueToggleReversalState ^ a;
// 访问器将会自动转换,所以使用字段
SetSourceValueAutoUpdate?.Invoke(autoUpdateValue);
// 可能不会脏
// OnValueDirty?.Invoke(this);
});
}
}
#endregion
#region 更新方法
/// <summary>
/// 更新函数
/// <code>
/// 在其他任意时刻调用时,将强制刷新其中的功能,如果已经是脏属性则不会有任何行为。
/// 更新是指从本地数据中更新,这会立刻产生值获取回调。
/// </code>
/// </summary>
public virtual void ForceUpdate(bool resetDirty = false, bool forceSet = false)
{
if (resetDirty)
IsDirty = false;
var value = GetSourceValue?.Invoke(this);
if (value == null)
return ;
RefreshValueWithoutEvent(value, forceSet);
RefreshAutoUpdateValue();
}
/// <summary>
/// 设置值,并触发更改事件,通常由界面发起所以不会主动更新界面
/// </summary>
/// <param name="newValue"></param>
protected virtual void SetValue(object newValue)
{
if (Value == newValue)
return;
var lastValue = Value;
// 如果不允许输入,将复位为现在的值
if (!allowInput)
RefreshValueWithoutEvent(Value);
else
{
Value = newValue;
IsDirty = true;
OnAnyValueChange?.Invoke(this, lastValue);
OnValueChange?.Invoke(this, lastValue);
}
}
/// <summary>
/// 设置值,不触发更改时的事件,但应该进行界面数值更新。
/// </summary>
/// <param name="newValue"></param>
/// <param name="forceSet"></param>
public virtual void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
if (!AutoUpdateValue && !forceSet)
return;
if (newValue == null)
return;
if (Value != null && (Value.ToString() == newValue.ToString()))
return;
Value = newValue;
RefreshFormatValue(newValue);
}
/// <summary>
/// 更新自动更新状态
/// </summary>
protected void RefreshAutoUpdateValue()
{
var isSourceValueAutoUpdate = GetSourceValueAutoUpdate?.Invoke(this);
if (isSourceValueAutoUpdate != null)
AutoUpdateValue = isSourceValueAutoUpdate.Value;
}
/// <summary>
/// 刷新格式化文本,在触发刷新时将产生更新
/// </summary>
protected virtual void RefreshFormatValue(object newValue)
{
}
protected void WhenStartEdit()
{
// Debug.Log("开始编辑");
ForceBlockUpdate = true;
}
/// <summary>
/// 数值输入完毕,或焦点移除时调用。
/// </summary>
protected void WhenEndEdit()
{
// Debug.Log("结束编辑");
ForceBlockUpdate = false;
OnEndEdit?.Invoke(this, Value);
}
#endregion
#region 外观设定
/// <summary>
/// 销毁自动更新复选框,并设定当前是否允许自动更新
/// </summary>
/// <param name="autoUpdateValue"></param>
public void DestroyAutoUpdateValue(bool autoUpdateValue)
{
Destroy(autoUpdateValueToggle.gameObject);
this.autoUpdateValue = autoUpdateValueReversalState ^ autoUpdateValue;
}
#endregion
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 3e3c729eeb8e03f459a9c95dc6e239d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,100 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace LRC
{
/// <summary>
/// 一种普通文本输入框
/// </summary>
public class ConfigTextInputField : ConfigSelectableItem
{
#region 属性字段
#if UNITY_EDITOR
public string editorInput;
#endif
public TMP_InputField targetInput;
public override bool AllowInput
{
get => base.AllowInput;
set
{
if (targetInput != null)
targetInput.readOnly = !value;
base.AllowInput = value;
}
}
#endregion
#region 生命周期
protected override void OnValidate()
{
base.OnValidate();
#if UNITY_EDITOR
if (targetInput != null)
targetInput.text = editorInput;
#endif
}
protected override void Awake()
{
base.Awake();
targetInput.onSelect.AddListener(a =>
{
WhenStartEdit();
});
targetInput.onValueChanged.AddListener(o =>
{
if(double.TryParse(o, out var value))
SetValue(value);
});
targetInput.onEndEdit.AddListener(delegate
{
WhenEndEdit();
});
}
#endregion
#region 实现
protected override void SetValue(object newValue)
{
base.SetValue(newValue);
}
public override void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
base.RefreshValueWithoutEvent(newValue, forceSet);
}
protected override void Initialization_ChildConstruction(UIBehaviour component)
{
base.Initialization_ChildConstruction(component);
if (component is TMP_InputField inputField)
{
if (targetInput != null)
targetInput = inputField;
}
}
protected override void RefreshFormatValue(object newValue)
{
base.RefreshFormatValue(newValue);
targetInput.text = newValue.ToString();
}
#endregion
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c9b27b4ed9920c947b157c1d7702372c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,86 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.UI;
namespace LRC
{
/// <summary>
/// 单选项控制组,将返回一个代表当前有效单选项索引作为值。
/// </summary>
public class ConfigToggleGroup : ConfigSelectableItem
{
#region 属性字段
public ToggleGroup targetToggle;
public List<Toggle> toggles;
#endregion
#region 生命周期
protected override void Awake()
{
base.Awake();
for (int i = 0; i < toggles.Count; i++)
{
void BuildInGroupInit()
{
var index = i;
var toggle = toggles[i];
toggle.onValueChanged.AddListener(a =>
{
if (a)
SetValue(index);
});
}
BuildInGroupInit();
}
}
#endregion
#region 实现
protected override void SetValue(object newValue)
{
base.SetValue(CastIntNumber(newValue));
}
public override void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
base.RefreshValueWithoutEvent(CastIntNumber(newValue), forceSet);
}
protected override void Initialization_ChildConstruction(UIBehaviour component)
{
base.Initialization_ChildConstruction(component);
if (component is Toggle toggle &&
!toggles.Contains(toggle))
toggles.Add(toggle);
}
protected override void RefreshFormatValue(object newValue)
{
base.RefreshFormatValue(newValue);
var newIndex = CastIntNumber(newValue);
// var index = 0;
// foreach (var toggle in targetToggle.ActiveToggles())
// {
// if (newIndex != index++)
// continue;
// toggle.isOn = true;
// Debug.Log(TitleName +":设置布尔项目");
// }
toggles[newIndex].SetIsOnWithoutNotify(true);
}
#endregion
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 1b1b61fd3a211734eb3d51cedcbe579f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,38 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace LRC
{
[AttributeUsage(AttributeTargets.Field)]
public class ConfigTypeOverrideAttribute : PropertyAttribute
{
public readonly string TypeName;
public ConfigTypeOverrideAttribute(string name)
{
TypeName = name;
}
/// <summary>
/// 尝试获取其中的rename定义名称
/// </summary>
/// <param name="field"></param>
/// <param name="name">通过特性获取的名称,如果没有则为原生的名称</param>
/// <returns>是否成功通过特性获取名称,否则是普通类型名称</returns>
public static bool TryGetMemberName(FieldInfo field, out string name)
{
if (field.GetCustomAttribute(typeof(ConfigTypeOverrideAttribute)) is ConfigTypeOverrideAttribute rename)
{
name = rename.TypeName;
return true;
}
name = field.FieldType.Name;
return false;
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a130623ae66ff564a9e14580549d2858
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,129 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace LRC
{
public class ConfigVec2InputField : ConfigSelectableItem
{
#region ÊôÐÔ×ÖÎ
#if UNITY_EDITOR
public Vector2 editorInput;
#endif
public TMP_InputField targetInputX;
public TMP_InputField targetInputY;
public override bool AllowInput
{
get => base.AllowInput;
set
{
if (targetInputX != null)
targetInputX.readOnly = !value;
if (targetInputY != null)
targetInputY.readOnly = value;
base.AllowInput = value;
}
}
#endregion
#region ÉúÃüÖÜÆÚ
protected override void OnValidate()
{
base.OnValidate();
#if UNITY_EDITOR
if (targetInputX != null)
targetInputX.text = editorInput.x.ToString();
if (targetInputY != null)
targetInputY.text = editorInput.y.ToString();
#endif
}
protected override void Awake()
{
base.Awake();
targetInputX.onSelect.AddListener(a =>
{
WhenStartEdit();
});
targetInputY.onSelect.AddListener(a =>
{
WhenStartEdit();
});
targetInputX.onValueChanged.AddListener(a =>
{
var value = CastVector3(Value);
value.x = float.Parse(a);
SetValue(value);
});
targetInputY.onValueChanged.AddListener(a =>
{
var value = CastVector3(Value);
value.y = float.Parse(a);
SetValue(value);
});
targetInputX.onEndEdit.AddListener(delegate
{
WhenEndEdit();
});
targetInputY.onEndEdit.AddListener(delegate
{
WhenEndEdit();
});
}
#endregion
#region ʵÏÖ
protected override void SetValue(object newValue)
{
base.SetValue(CastVector2(newValue));
}
public override void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
base.RefreshValueWithoutEvent(CastVector2(newValue), forceSet);
}
protected override void Initialization_ChildConstruction(UIBehaviour component)
{
base.Initialization_ChildConstruction(component);
var name = component.transform.name;
if (component is not TMP_InputField inputField)
return;
switch (name)
{
case "inputx":
case "inputX":
if (targetInputX != null)
targetInputX = inputField;
break;
case "inputy":
case "inputY":
if (targetInputY != null)
targetInputY = inputField;
break;
default:
break;
}
}
protected override void RefreshFormatValue(object newValue)
{
base.RefreshFormatValue(newValue);
var value = CastVector2(newValue);
targetInputX.text = value.x.ToString(numberFormat);
targetInputY.text = value.y.ToString(numberFormat);
}
#endregion
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 118ff8f18137da54087c6d572fc5db75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,158 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace LRC
{
public class ConfigVec3InputField : ConfigSelectableItem
{
#region ÊôÐÔ×ÖÎ
#if UNITY_EDITOR
public Vector3 editorInput;
#endif
public TMP_InputField targetInputX;
public TMP_InputField targetInputY;
public TMP_InputField targetInputZ;
public override bool AllowInput
{
get => base.AllowInput;
set
{
if (targetInputX != null)
targetInputX.readOnly = !value;
if (targetInputY != null)
targetInputY.readOnly = value;
if (targetInputZ != null)
targetInputZ.readOnly = value;
base.AllowInput = value;
}
}
#endregion
#region ÉúÃüÖÜÆÚ
protected override void OnValidate()
{
base.OnValidate();
#if UNITY_EDITOR
if (targetInputX != null)
targetInputX.text = editorInput.x.ToString();
if (targetInputY != null)
targetInputY.text = editorInput.y.ToString();
if (targetInputZ != null)
targetInputZ.text = editorInput.z.ToString();
#endif
}
protected override void Awake()
{
base.Awake();
targetInputX.onSelect.AddListener(a =>
{
WhenStartEdit();
});
targetInputY.onSelect.AddListener(a =>
{
WhenStartEdit();
});
targetInputZ.onSelect.AddListener(a =>
{
WhenStartEdit();
});
targetInputX.onValueChanged.AddListener(a =>
{
var value = CastVector3(Value);
value.x = float.Parse(a);
SetValue(value);
});
targetInputY.onValueChanged.AddListener(a =>
{
var value = CastVector3(Value);
value.y = float.Parse(a);
SetValue(value);
});
targetInputZ.onValueChanged.AddListener(a =>
{
var value = CastVector3(Value);
value.z = float.Parse(a);
SetValue(value);
});
targetInputX.onEndEdit.AddListener(delegate
{
WhenEndEdit();
});
targetInputY.onEndEdit.AddListener(delegate
{
WhenEndEdit();
});
targetInputZ.onEndEdit.AddListener(delegate
{
WhenEndEdit();
});
}
#endregion
#region ʵÏÖ
protected override void SetValue(object newValue)
{
base.SetValue(CastVector3(newValue));
}
public override void RefreshValueWithoutEvent(object newValue, bool forceSet = false)
{
base.RefreshValueWithoutEvent(CastVector3(newValue), forceSet);
}
protected override void Initialization_ChildConstruction(UIBehaviour component)
{
base.Initialization_ChildConstruction(component);
var name = component.transform.name;
if (component is not TMP_InputField inputField)
return;
switch (name)
{
case "inputx":
case "inputX":
if (targetInputX != null)
targetInputX = inputField;
break;
case "inputy":
case "inputY":
if (targetInputY != null)
targetInputY = inputField;
break;
case "inputz":
case "inputZ":
if (targetInputZ != null)
targetInputZ = inputField;
break;
default:
break;
}
}
protected override void RefreshFormatValue(object newValue)
{
base.RefreshFormatValue(newValue);
var value = CastVector3(newValue);
targetInputX.text = value.x.ToString(numberFormat);
targetInputY.text = value.y.ToString(numberFormat);
targetInputZ.text = value.z.ToString(numberFormat);
}
#endregion
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: b6e1779348dcc2b48a6f009b828e9b5d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
Binary file not shown.
-33
View File
@@ -1,33 +0,0 @@
fileFormatVersion: 2
guid: c754cc17629bc0f4cac54d1417f746b3
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
-33
View File
@@ -1,33 +0,0 @@
fileFormatVersion: 2
guid: dad679042ecb6164d815157fcf4dd580
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7758a0679c9038144a4b0d007d44618f
guid: 85f1b523843f5884bb9f6b47500bccd0
folderAsset: yes
DefaultImporter:
externalObjects: {}
+164
View File
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using XericLibrary.Runtime.MacroLibrary;
using XericLibrary.Runtime.Type;
namespace Deconstruction.UI.TmpText
{
/// <summary>
/// TMP超链接点击管理组件
/// </summary>
public class TMPHyperlinkManager : MonoBehaviour
{
/// <summary>
/// 画布组件
/// </summary>
#if ODIN_INSPECTOR
[LabelText("画布*")]
#endif
[Tooltip("组件可为空,将自动向上查找")]
[SerializeField] public Canvas canvas;
#if ODIN_INSPECTOR
[LabelText("点击超链接时回调")]
#endif
[SerializeField]
private UnityEvent<string> _onClickLink;
public Action<string> onClickLink;
/// <summary>
/// 当前摄像机
/// </summary>
private Camera _camera;
/// <summary>
/// 存储链接ID与对应回调函数的映射
/// </summary>
private Dictionary<string, Action> _linkCallbacks = new Dictionary<string, Action>();
protected void Awake()
{
if (canvas == null)
transform.GetParents().Startup(transform).GetComponent(out canvas);
if (canvas == null)
{
Debug.LogError("TMP文本对象必须在Canvas下才能使用超链接功能");
return;
}
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
_camera = null;
else
_camera = canvas.worldCamera;
}
/// <summary>
/// 注册链接回调函数
/// </summary>
public void RegisterLinkCallback(string linkID, Action callback)
{
if (_linkCallbacks.ContainsKey(linkID))
{
Debug.LogWarning($"链接ID '{linkID}' 已存在,将覆盖之前的回调函数");
_linkCallbacks[linkID] = callback;
}
else
{
_linkCallbacks.Add(linkID, callback);
}
}
/// <summary>
/// 注销链接回调函数
/// </summary>
public void UnregisterLinkCallback(string linkID)
{
if (_linkCallbacks.ContainsKey(linkID))
{
_linkCallbacks.Remove(linkID);
}
}
/// <summary>
/// 处理链接点击事件
/// </summary>
public void HandleLinkClick(string linkID)
{
if (_linkCallbacks.TryGetValue(linkID, out Action callback))
{
callback?.Invoke();
Debug.Log($"执行链接 '{linkID}' 的回调函数");
}
else
{
Debug.LogWarning($"未找到链接ID '{linkID}' 的回调函数");
}
}
/// <summary>
/// 检查坐标下的超链接索引
/// </summary>
/// <param name="text"></param>
/// <param name="position"></param>
/// <returns></returns>
internal int CheckLinkIndex(TMP_Text text, Vector3 position)
{
return TMP_TextUtilities.FindIntersectingLink(text, position, _camera);
}
/// <summary>
/// 检查坐标下的超链接id信息
/// </summary>
/// <param name="text"></param>
/// <param name="position"></param>
/// <param name="linkInfo"></param>
/// <returns></returns>
internal bool CheckLinkInfo(TMP_Text text, Vector3 position, out TMP_LinkInfo linkInfo)
{
var index = CheckLinkIndex(text, position);
if (index != -1)
{
linkInfo = text.textInfo.linkInfo[index];
return true;
}
linkInfo = default;
return false;
}
/// <summary>
/// 检查坐标下的超链接id
/// </summary>
/// <param name="text"></param>
/// <param name="position"></param>
/// <param name="linkID"></param>
/// <returns></returns>
internal bool CheckLinkID(TMP_Text text, Vector3 position, out string linkID)
{
if (CheckLinkInfo(text, position, out var info))
{
linkID = info.GetLinkID();
return true;
}
linkID = string.Empty;
return false;
}
/// <summary>
/// 当任意tmp对象超链接点击时调用,从这里发送事件
/// </summary>
/// <param name="linkID"></param>
internal void OnAnyLinkClick(string linkID)
{
onClickLink?.Invoke(linkID);
_onClickLink.Invoke(linkID);
Debug.Log($"click link{linkID}");
// 这里不需要处理报错,应当由发送消息的成员处理
}
// todo 主动查找所有的tmp组件,或是等组件唤醒后区管理它们,用来获得所有的带有标记的超链接组件
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 73f7a373611d15741b1093e0669abea9
guid: 6b48ffc6688579846a29db364326940a
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,67 @@
using System;
using Sirenix.OdinInspector;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using XericLibrary.Runtime.MacroLibrary;
namespace Deconstruction.UI.TmpText
{
/// <summary>
/// 附加到TMP文本对象的组件,用于捕获点击事件并转发给管理器
/// </summary>
[RequireComponent(typeof(TextMeshProUGUI))]
public class TMPHyperlinkReceiver : MonoBehaviour, IPointerClickHandler
{
private TextMeshProUGUI _tmpText;
#if ODIN_INSPECTOR
[LabelText("超链接管理器*")]
#endif
[Tooltip("组件可为空,将自动向上查找")]
public TMPHyperlinkManager manager;
#if ODIN_INSPECTOR
[LabelText("点击超链接时回调")]
#endif
[SerializeField]
private UnityEvent<string> _onClickLink;
public Action<string> onClickLink;
private void Awake()
{
_tmpText = GetComponent<TextMeshProUGUI>();
if (manager == null)
transform.GetParents().Startup(transform).GetComponent(out manager);
if (manager == null)
throw new Exception("TMP文本超链接集成捕获组件必须放在具有管理器的父级下");
}
public void OnPointerClick(PointerEventData eventData)
{
ClickPoint(eventData.position);
}
/// <summary>
/// 在坐标处点击超链接文本
/// </summary>
/// <param name="position"></param>
public void ClickPoint(Vector3 position)
{
if (!manager.CheckLinkID(_tmpText, position, out var linkID))
return;
try
{
onClickLink?.Invoke(linkID);
_onClickLink.Invoke(linkID);
manager.OnAnyLinkClick(linkID);
}
catch (Exception e)
{
Debug.LogError($"点击 {_tmpText.text.LimitStringLength(32)} 的超链接 {linkID} 时出错:\n{e.Message}\n{e.Data}", this);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bb6cf745165c40f4ad82a5303ec21696
timeCreated: 1750765466
Binary file not shown.
-33
View File
@@ -1,33 +0,0 @@
fileFormatVersion: 2
guid: 3f1521b0317329a40980ac84e1253425
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -1,6 +1,5 @@
fileFormatVersion: 2
guid: d98e03d6c3f900341838908516882fd0
folderAsset: yes
guid: 5f497a3b416599744ba515971811704f
DefaultImporter:
externalObjects: {}
userData: