Merge branch 'feature/石头完善' into 'develop'

Feature/石头完善

See merge request aibis-dream/aibis-dream!629
This commit is contained in:
2026-02-26 11:04:49 +00:00
214 changed files with 52294 additions and 14188 deletions
@@ -79,9 +79,40 @@ namespace AibisDream.Kit
private readonly List<IActionQueueCallback> _actionQueueCallbacks = new();
/// <summary>
/// 确保存在可用的 <see cref="ActionKitMonoBehaviourEvents"/> 实例。
/// 由于 <see cref="Singleton{T}"/> 不会自动创建对象,当场景里没有该组件时,某些 Action 的 Deinit 回收入队会触发 NRE。
/// </summary>
private static ActionKitMonoBehaviourEvents EnsureInstance()
{
if (Instance != null) return Instance;
if (!Application.isPlaying) return null;
var go = new GameObject(nameof(ActionKitMonoBehaviourEvents));
DontDestroyOnLoad(go);
return go.AddComponent<ActionKitMonoBehaviourEvents>();
}
/// <summary>
/// 获取或创建全局事件载体(用于 StartGlobal、回收队列等)。
/// </summary>
public static ActionKitMonoBehaviourEvents GetOrCreate()
{
return EnsureInstance();
}
public static void AddCallback(IActionQueueCallback actionQueueCallback)
{
Instance._actionQueueCallbacks.Add(actionQueueCallback);
var inst = EnsureInstance();
// 在退出/非运行态等情况下无法创建实例时,直接执行回调避免 NRE(回收/清理类回调通常是幂等的)。
if (inst == null)
{
actionQueueCallback?.Call();
return;
}
inst._actionQueueCallbacks.Add(actionQueueCallback);
}
// Update is called once per frame
@@ -1,4 +1,4 @@
using System;
using System;
using UnityEngine;
namespace AibisDream.Kit
@@ -135,12 +135,12 @@ namespace AibisDream.Kit
public static IActionController StartGlobal(this IAction self, Action<IActionController> onFinish = null)
{
return self.Start(ActionKitMonoBehaviourEvents.Instance, onFinish);
return self.Start(ActionKitMonoBehaviourEvents.GetOrCreate(), onFinish);
}
public static IActionController StartGlobal(this IAction self, Action onFinish)
{
return self.Start(ActionKitMonoBehaviourEvents.Instance, onFinish);
return self.Start(ActionKitMonoBehaviourEvents.GetOrCreate(), onFinish);
}
@@ -0,0 +1,194 @@
using UnityEngine;
namespace AibisDream
{
/// <summary>
/// 矩形区域组件
/// 用于划定一个矩形区域,并判断点是否在区域内
/// </summary>
public class BoxArea : MonoBehaviour
{
[Header("Box尺寸设置")]
[SerializeField] private float width = 10f;
[SerializeField] private float height = 10f;
[Header("原点位置设置")]
[Tooltip("设置(0,0)点在Box中的位置")]
[SerializeField] private AnchorPoint anchorPoint = AnchorPoint.Center;
/// <summary>
/// Box的宽度
/// </summary>
public float Width
{
get => width;
set => width = value;
}
/// <summary>
/// Box的高度
/// </summary>
public float Height
{
get => height;
set => height = value;
}
/// <summary>
/// 锚点位置
/// </summary>
public AnchorPoint AnchorPoint
{
get => anchorPoint;
set => anchorPoint = value;
}
/// <summary>
/// 获取Box的世界坐标边界
/// </summary>
public Bounds WorldBounds
{
get
{
Vector3 center = GetWorldCenter();
Vector3 size = new Vector3(width, height, 0f);
return new Bounds(center, size);
}
}
/// <summary>
/// 获取Box的本地坐标边界
/// </summary>
public Rect LocalRect
{
get
{
Vector2 offset = GetAnchorOffset();
return new Rect(offset.x, offset.y, width, height);
}
}
/// <summary>
/// 判断给定的世界坐标位置是否在Box内
/// </summary>
/// <param name="worldPosition">世界坐标位置</param>
/// <returns>如果位置在Box内返回true,否则返回false</returns>
public bool Contains(Vector3 worldPosition)
{
// 将世界坐标转换为本地坐标
Vector3 localPosition = transform.InverseTransformPoint(worldPosition);
// 获取本地坐标的矩形区域
Rect rect = LocalRect;
// 判断点是否在矩形内(只考虑X和Y轴,忽略Z轴)
return rect.Contains(new Vector2(localPosition.x, localPosition.y));
}
/// <summary>
/// 判断给定的本地坐标位置是否在Box内
/// </summary>
/// <param name="localPosition">本地坐标位置</param>
/// <returns>如果位置在Box内返回true,否则返回false</returns>
public bool ContainsLocal(Vector2 localPosition)
{
Rect rect = LocalRect;
return rect.Contains(localPosition);
}
/// <summary>
/// 获取Box的世界坐标中心点
/// </summary>
private Vector3 GetWorldCenter()
{
Vector2 anchorOffset = GetAnchorOffset();
Vector3 localCenter = new Vector3(anchorOffset.x + width * 0.5f, anchorOffset.y + height * 0.5f, 0f);
return transform.TransformPoint(localCenter);
}
/// <summary>
/// 根据锚点位置获取偏移量
/// </summary>
private Vector2 GetAnchorOffset()
{
return anchorPoint switch
{
AnchorPoint.BottomLeft => new Vector2(0f, 0f),
AnchorPoint.BottomCenter => new Vector2(-width * 0.5f, 0f),
AnchorPoint.BottomRight => new Vector2(-width, 0f),
AnchorPoint.CenterLeft => new Vector2(0f, -height * 0.5f),
AnchorPoint.Center => new Vector2(-width * 0.5f, -height * 0.5f),
AnchorPoint.CenterRight => new Vector2(-width, -height * 0.5f),
AnchorPoint.TopLeft => new Vector2(0f, -height),
AnchorPoint.TopCenter => new Vector2(-width * 0.5f, -height),
AnchorPoint.TopRight => new Vector2(-width, -height),
_ => new Vector2(-width * 0.5f, -height * 0.5f)
};
}
#if UNITY_EDITOR
/// <summary>
/// 在Scene视图中绘制Box边界(选中时)
/// </summary>
private void OnDrawGizmos()
{
// 保存原始颜色
Color originalColor = Gizmos.color;
// 设置Box边界颜色
Gizmos.color = Color.cyan;
// 获取Box的四个角点(世界坐标)
Vector2 offset = GetAnchorOffset();
Vector3[] corners = new Vector3[4]
{
transform.TransformPoint(new Vector3(offset.x, offset.y, 0f)), // 左下角
transform.TransformPoint(new Vector3(offset.x + width, offset.y, 0f)), // 右下角
transform.TransformPoint(new Vector3(offset.x + width, offset.y + height, 0f)), // 右上角
transform.TransformPoint(new Vector3(offset.x, offset.y + height, 0f)) // 左上角
};
// 绘制Box边界线
for (int i = 0; i < 4; i++)
{
int next = (i + 1) % 4;
Gizmos.DrawLine(corners[i], corners[next]);
}
// 绘制中心点标记
Gizmos.color = Color.green;
Vector3 center = GetWorldCenter();
Gizmos.DrawWireSphere(center, 0.15f);
// 恢复原始颜色
Gizmos.color = originalColor;
}
#endif
}
/// <summary>
/// 锚点位置枚举
/// 定义(0,0)点在Box中的位置
/// </summary>
public enum AnchorPoint
{
[Tooltip("左下角")]
BottomLeft,
[Tooltip("底部中心")]
BottomCenter,
[Tooltip("右下角")]
BottomRight,
[Tooltip("左侧中心")]
CenterLeft,
[Tooltip("中心")]
Center,
[Tooltip("右侧中心")]
CenterRight,
[Tooltip("左上角")]
TopLeft,
[Tooltip("顶部中心")]
TopCenter,
[Tooltip("右上角")]
TopRight
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a9831cebfabb09f4993b755f03fcc5eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
using System;
using System.Diagnostics;
namespace AibisDream.Framework
{
public class CodeTimer : IDisposable
{
private readonly Stopwatch _stopwatch;
private readonly string _name;
public CodeTimer(string name = "")
{
_name = name;
_stopwatch = Stopwatch.StartNew();
}
public void Dispose()
{
_stopwatch.Stop();
UnityEngine.Debug.Log($"[{_name}] 耗时: {_stopwatch.ElapsedMilliseconds}ms");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4637aacee2dd3146acb68ee955cc70c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,218 @@
using UnityEngine;
using DG.Tweening;
namespace AibisDream
{
/// <summary>
/// 三状态位置移动器
/// 提供Open、Close、Half三个目标位置,使用DOTween进行平滑移动
/// 移动过程不可被打断,并提供移动状态标记
/// </summary>
public class ThreeStatePositionMover : MonoBehaviour
{
[Header("目标位置设置")]
[SerializeField] private Transform openPosition;
[SerializeField] private Transform closePosition;
[SerializeField] private Transform halfPosition;
[Header("移动设置")]
[SerializeField] private float moveDuration = 0.5f;
[SerializeField] private Ease moveEase = Ease.OutQuad;
// 当前状态
private PositionState _currentState = PositionState.Close;
// 当前正在执行的Tween
private Tween _currentTween;
// 是否正在移动中
public bool IsMoving { get; private set; }
private void Awake()
{
// 初始化时设置到Close位置
if (closePosition != null)
{
transform.position = closePosition.position;
}
}
private void OnDestroy()
{
// 清理Tween
if (_currentTween != null)
{
_currentTween.Kill();
_currentTween = null;
}
}
/// <summary>
/// 设置到Open状态
/// </summary>
public void SetToOpen()
{
MoveToState(PositionState.Open);
}
/// <summary>
/// 设置到Close状态
/// </summary>
public void SetToClose()
{
MoveToState(PositionState.Close);
}
/// <summary>
/// 设置到Half状态
/// </summary>
public void SetToHalf()
{
MoveToState(PositionState.Half);
}
/// <summary>
/// 移动到指定状态
/// </summary>
/// <param name="targetState">目标状态</param>
public void MoveToState(PositionState targetState)
{
// 如果正在移动中,忽略新的移动请求
if (IsMoving)
{
return;
}
// 如果已经是目标状态,直接返回
if (_currentState == targetState)
{
return;
}
// 获取目标Transform
Transform targetTransform = GetTransformForState(targetState);
if (targetTransform == null)
{
Debug.LogWarning($"ThreeStatePositionMover: {targetState} 状态的Transform未设置!");
return;
}
// 获取目标位置
Vector3 targetPosition = targetTransform.position;
// 如果当前有正在执行的Tween,先停止它
if (_currentTween != null && _currentTween.IsActive())
{
_currentTween.Kill();
}
// 设置移动状态
IsMoving = true;
// 创建新的Tween
_currentTween = transform.DOMove(targetPosition, moveDuration)
.SetEase(moveEase)
.OnComplete(() =>
{
IsMoving = false;
_currentState = targetState;
_currentTween = null;
});
}
/// <summary>
/// 立即设置到指定状态(不播放动画)
/// </summary>
/// <param name="targetState">目标状态</param>
public void SetStateImmediate(PositionState targetState)
{
// 如果正在移动,先停止移动
if (_currentTween != null && _currentTween.IsActive())
{
_currentTween.Kill();
_currentTween = null;
}
IsMoving = false;
_currentState = targetState;
Transform targetTransform = GetTransformForState(targetState);
if (targetTransform != null)
{
transform.position = targetTransform.position;
}
}
/// <summary>
/// 获取指定状态对应的Transform
/// </summary>
private Transform GetTransformForState(PositionState state)
{
return state switch
{
PositionState.Open => openPosition,
PositionState.Close => closePosition,
PositionState.Half => halfPosition,
_ => closePosition
};
}
/// <summary>
/// 获取当前状态
/// </summary>
public PositionState GetCurrentState()
{
return _currentState;
}
/// <summary>
/// 设置移动持续时间(运行时)
/// </summary>
public void SetMoveDuration(float duration)
{
moveDuration = duration;
}
/// <summary>
/// 设置缓动类型(运行时)
/// </summary>
public void SetMoveEase(Ease ease)
{
moveEase = ease;
}
#if UNITY_EDITOR
// 编辑器辅助:在Scene视图中显示目标位置
private void OnDrawGizmosSelected()
{
if (openPosition != null)
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(openPosition.position, 0.1f);
}
if (closePosition != null)
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(closePosition.position, 0.1f);
}
if (halfPosition != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(halfPosition.position, 0.1f);
}
}
#endif
}
/// <summary>
/// 位置状态枚举
/// </summary>
public enum PositionState
{
Open,
Close,
Half
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 033a66b82d5f349458216810d638e1f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,9 +1,11 @@
using System;
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using AibisDream.Utility;
using UnityEditor.U2D.PSD;
#if UNITY_EDITOR
using UnityEditor;
@@ -92,6 +94,31 @@ namespace AibisDream.Framework
};
}
/// <summary>
/// 异步加载 Addressable 资源,callback 接收完整 handle。调用方需在适当时机调用 Release 释放。
/// </summary>
public static void LoadAssetAsyncWithHandle<T>(string key, Action<AsyncOperationHandle<T>> callback) where T : UnityEngine.Object
{
var handle = Addressables.LoadAssetAsync<T>(key);
handle.Completed += operation =>
{
if (operation.Status != AsyncOperationStatus.Succeeded)
{
Debug.Log($"资源{key}不存在");
}
callback(operation);
};
}
/// <summary>
/// 释放 Addressable 加载的 handle
/// </summary>
public static void Release<T>(AsyncOperationHandle<T> handle)
{
if (handle.IsValid())
Addressables.Release(handle);
}
#if UNITY_EDITOR
public static bool GetAssetAddressableKey<T>(T asset, out string addressableKey) where T : UnityEngine.Object
{
@@ -114,6 +141,19 @@ namespace AibisDream.Framework
}
}
}
else if (asset is Sprite texture && AssetImporter.GetAtPath(path) is PSDImporter psdImporter)
{
if (TryGetAddressableKey(path, out var addressableKeyPrefix))
{
addressableKey = $"{addressableKeyPrefix}[{texture.name}]";
return true;
}
else
{
addressableKey = addressableKeyPrefix;
return false;
}
}
// 其他情况正常输出
return TryGetAddressableKey(path, out addressableKey);
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Reflection;
namespace AibisDream.Kit
@@ -26,6 +26,6 @@ namespace AibisDream.Kit
}
}
public abstract void OnSingletonInit();
public virtual void OnSingletonInit() { }
}
}