画报编辑和播放添加动画支持

main
liuchunguang 2026-03-18 14:34:22 +08:00
parent 7e7a2f4c37
commit c2f5ca10d0
5 changed files with 948 additions and 15 deletions

View File

@ -3,6 +3,7 @@ using UnityEngine.UI;
using System.Collections.Generic;
using System.Linq;
using PhxhSDK;
using Cysharp.Threading.Tasks;
#if UNITY_EDITOR
using UnityEditor;
@ -464,6 +465,19 @@ namespace Framework.UI.Pictorial
return;
}
// ---- 退出动画:在卸载旧画报前播放 ----
if (items.Count > 0 && pictorialData?.animData != null)
{
var oldAnimData = pictorialData.animData;
bool hasExitAnim = (oldAnimData.bgExitAnim != null && oldAnimData.bgExitAnim.enabled) ||
(oldAnimData.itemExitAnims != null && oldAnimData.itemExitAnims.Any(a => a != null && a.enabled));
if (hasExitAnim)
{
Debug.Log("[画报系统] 播放旧画报退出动画");
await PlayExitAnimAsync(oldAnimData);
}
}
// 隐藏CharatorItem模板
Transform templateTransform = transform.Find("CharatorItem");
if (templateTransform != null)
@ -553,12 +567,13 @@ namespace Framework.UI.Pictorial
}
// 恢复背景图片到Item_ATemp_Bg_Second节点
Image bgImage = null;
if (!string.IsNullOrEmpty(pictorialData.backgroundPath))
{
Transform bgImageTransform = transform.Find("Item_ATemp_Bg_Second");
if (bgImageTransform != null)
{
Image bgImage = bgImageTransform.GetComponent<Image>();
bgImage = bgImageTransform.GetComponent<Image>();
if (bgImage != null)
{
// 使用LoadSpriteFromPath加载背景图片支持子资源
@ -619,6 +634,214 @@ namespace Framework.UI.Pictorial
Debug.Log($"<color=yellow>[画报系统] 当前背景尺寸 {bgWidth}x{bgHeight} 没有精确匹配配置,使用插值计算位置</color>");
UpdateAllItemsPosition();
}
// ---- 进入动画:画报元素创建并定位完成后播放 ----
if (pictorialData.animData != null)
{
var animData = pictorialData.animData;
bool hasEnterAnim = (animData.bgEnterAnim != null && animData.bgEnterAnim.enabled) ||
(animData.itemEnterAnims != null && animData.itemEnterAnims.Any(a => a != null && a.enabled));
if (hasEnterAnim)
{
Debug.Log("[画报系统] 播放新画报进入动画");
await PlayEnterAnimAsync(animData, bgImage);
}
}
}
// ===================== 动画方法 =====================
/// <summary>
/// 播放进入动画(异步)
/// 进入动画:元素从(实际位置 + offset)移动回实际位置alpha 从 alphaStart 到 alphaEnd
/// </summary>
public async UniTask PlayEnterAnimAsync(PictorialAnimData animData, Image bgImageOverride = null)
{
if (animData == null) return;
List<PictorialItem> sceneItems = GetSortedSceneItems();
var targets = PrepareAnimTargets(sceneItems, animData, bgImageOverride, isEnter: true);
if (targets.Count == 0) return;
await RunAnimTween(targets, animData.enterDuration, isEnter: true);
}
/// <summary>
/// 播放退出动画(异步)
/// 退出动画:元素从实际位置移动到(实际位置 + offset)alpha 从 alphaStart 到 alphaEnd
/// </summary>
public async UniTask PlayExitAnimAsync(PictorialAnimData animData)
{
if (animData == null) return;
// 退出动画使用当前场景中的小背景图片
Image bgImg = null;
Transform bgT = transform.Find("Item_ATemp_Bg_Second");
if (bgT != null) bgImg = bgT.GetComponent<Image>();
List<PictorialItem> sceneItems = GetSortedSceneItems();
var targets = PrepareAnimTargets(sceneItems, animData, bgImg, isEnter: false);
if (targets.Count == 0) return;
await RunAnimTween(targets, animData.exitDuration, isEnter: false);
}
/// <summary>
/// 动画目标数据
/// </summary>
private struct AnimTarget
{
public RectTransform rectTransform;
public Image image;
public Vector3 basePosition; // 实际计算位置anchoredPosition3D
public PictorialItemAnimData animData;
}
/// <summary>
/// 构建动画目标列表
/// </summary>
private List<AnimTarget> PrepareAnimTargets(
List<PictorialItem> sceneItems,
PictorialAnimData animData,
Image bgImageRef,
bool isEnter)
{
var targets = new List<AnimTarget>();
// 处理小背景
var bgAnim = isEnter ? animData.bgEnterAnim : animData.bgExitAnim;
if (bgAnim != null && bgAnim.enabled && bgImageRef != null)
{
var bgRect = bgImageRef.GetComponent<RectTransform>();
if (bgRect != null)
{
targets.Add(new AnimTarget
{
rectTransform = bgRect,
image = bgImageRef,
basePosition = bgRect.anchoredPosition3D,
animData = bgAnim
});
}
}
// 处理各画报元素
var itemAnimList = isEnter ? animData.itemEnterAnims : animData.itemExitAnims;
if (itemAnimList == null) return targets;
for (int i = 0; i < sceneItems.Count; i++)
{
if (i >= itemAnimList.Count) break;
var ia = itemAnimList[i];
if (ia == null || !ia.enabled) continue;
var item = sceneItems[i];
var itemRect = item.GetComponent<RectTransform>();
var itemImage = item.GetComponentInChildren<Image>(true);
if (itemRect == null) continue;
targets.Add(new AnimTarget
{
rectTransform = itemRect,
image = itemImage,
basePosition = itemRect.anchoredPosition3D,
animData = ia
});
}
return targets;
}
/// <summary>
/// 执行动画补间SmoothStep 曲线)
/// isEnter=true: 从 (basePos + offset, alphaStart) 到 (basePos, alphaEnd)
/// isEnter=false: 从 (basePos, alphaStart) 到 (basePos + offset, alphaEnd)
/// </summary>
private async UniTask RunAnimTween(List<AnimTarget> targets, float duration, bool isEnter)
{
if (duration <= 0f || targets.Count == 0) return;
// 设置初始状态
foreach (var t in targets)
{
if (t.rectTransform == null) continue;
if (isEnter)
{
t.rectTransform.anchoredPosition3D = t.basePosition + t.animData.offset;
}
else
{
t.rectTransform.anchoredPosition3D = t.basePosition;
}
if (t.image != null)
SetImageAlpha(t.image, t.animData.alphaStart);
}
float elapsed = 0f;
while (elapsed < duration)
{
await UniTask.Yield();
elapsed += Time.unscaledDeltaTime;
float rawT = Mathf.Clamp01(elapsed / duration);
float smoothT = Mathf.SmoothStep(0f, 1f, rawT);
foreach (var t in targets)
{
if (t.rectTransform == null) continue;
Vector3 fromPos = isEnter ? (t.basePosition + t.animData.offset) : t.basePosition;
Vector3 toPos = isEnter ? t.basePosition : (t.basePosition + t.animData.offset);
t.rectTransform.anchoredPosition3D = Vector3.Lerp(fromPos, toPos, smoothT);
if (t.image != null)
{
float alpha = Mathf.Lerp(t.animData.alphaStart, t.animData.alphaEnd, smoothT);
SetImageAlpha(t.image, alpha);
}
}
}
// 确保最终状态精确
foreach (var t in targets)
{
if (t.rectTransform == null) continue;
t.rectTransform.anchoredPosition3D = isEnter ? t.basePosition : (t.basePosition + t.animData.offset);
if (t.image != null)
SetImageAlpha(t.image, t.animData.alphaEnd);
}
}
/// <summary>
/// 获取场景中按层次排序的 PictorialItem 列表(过滤特殊节点)
/// </summary>
private List<PictorialItem> GetSortedSceneItems()
{
var result = new List<PictorialItem>();
for (int i = 0; i < transform.childCount; i++)
{
Transform child = transform.GetChild(i);
if (child.name.Contains("Item_ATemp_Bg_Second") || child.name.Contains("CharatorItem"))
continue;
var item = child.GetComponent<PictorialItem>();
if (item != null)
result.Add(item);
}
return result;
}
/// <summary>
/// 设置 Image 组件的 alpha 值
/// </summary>
private static void SetImageAlpha(Image image, float alpha)
{
if (image == null) return;
Color c = image.color;
c.a = Mathf.Clamp01(alpha);
image.color = c;
}
/// <summary>

View File

@ -40,6 +40,11 @@ namespace Framework.UI.Pictorial
/// 保存的分辨率配置列表
/// </summary>
public List<ResolutionConfig> resolutionConfigs = new List<ResolutionConfig>();
/// <summary>
/// 画报动画配置(可选)
/// </summary>
public PictorialAnimData animData = new PictorialAnimData();
}
/// <summary>
@ -180,4 +185,70 @@ namespace Framework.UI.Pictorial
/// </summary>
public Vector3 scale = Vector3.one;
}
/// <summary>
/// 画报整体动画配置
/// </summary>
[Serializable]
public class PictorialAnimData
{
/// <summary>
/// 进入动画时长(秒)
/// </summary>
public float enterDuration = 0.5f;
/// <summary>
/// 退出动画时长(秒)
/// </summary>
public float exitDuration = 0.3f;
/// <summary>
/// 各元素的进入动画数据(与 PictorialData.items 索引对应)
/// </summary>
public List<PictorialItemAnimData> itemEnterAnims = new List<PictorialItemAnimData>();
/// <summary>
/// 各元素的退出动画数据(与 PictorialData.items 索引对应)
/// </summary>
public List<PictorialItemAnimData> itemExitAnims = new List<PictorialItemAnimData>();
/// <summary>
/// 小背景Item_ATemp_Bg_Second的进入动画
/// </summary>
public PictorialItemAnimData bgEnterAnim = new PictorialItemAnimData();
/// <summary>
/// 小背景Item_ATemp_Bg_Second的退出动画
/// </summary>
public PictorialItemAnimData bgExitAnim = new PictorialItemAnimData();
}
/// <summary>
/// 单个元素的动画属性
/// </summary>
[Serializable]
public class PictorialItemAnimData
{
/// <summary>
/// 是否启用动画
/// </summary>
public bool enabled = false;
/// <summary>
/// 位移偏移量。
/// 进入动画:元素从(实际位置 + offset移动回实际位置。
/// 退出动画:元素从实际位置移动到(实际位置 + offset
/// </summary>
public Vector3 offset = Vector3.zero;
/// <summary>
/// 起始 Alpha动画开始时
/// </summary>
public float alphaStart = 1f;
/// <summary>
/// 结束 Alpha动画结束时
/// </summary>
public float alphaEnd = 1f;
}
}

View File

@ -6944,5 +6944,153 @@
"backgroundHeight": 756.0,
"timestamp": "2026-03-04 12:05:16"
}
]
],
"animData": {
"enterDuration": 0.5,
"exitDuration": 0.30000001192092898,
"itemEnterAnims": [
{
"enabled": true,
"offset": {
"x": 50.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.43799999356269839,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 50.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.31299999356269839,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 60.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.20800000429153443,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 50.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": -50.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.3109999895095825,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
}
],
"itemExitAnims": [
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 60.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 50.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
}
],
"bgEnterAnim": {
"enabled": true,
"offset": {
"x": 30.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.5,
"alphaEnd": 1.0
},
"bgExitAnim": {
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
}
}
}

View File

@ -3116,5 +3116,153 @@
"backgroundHeight": 756.0,
"timestamp": "2026-02-27 14:45:36"
}
]
],
"animData": {
"enterDuration": 0.5,
"exitDuration": 0.30000001192092898,
"itemEnterAnims": [
{
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
},
{
"enabled": true,
"offset": {
"x": 30.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
}
],
"itemExitAnims": [
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
},
{
"enabled": true,
"offset": {
"x": 30.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
}
],
"bgEnterAnim": {
"enabled": true,
"offset": {
"x": 20.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 0.0,
"alphaEnd": 1.0
},
"bgExitAnim": {
"enabled": true,
"offset": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"alphaStart": 1.0,
"alphaEnd": 0.0
}
}
}

View File

@ -32,6 +32,9 @@ namespace PictorialEditor
{
// 取消注册
EditorApplication.update -= OnEditorUpdate;
// 确保动画预览清理
if (isAnimPreviewPlaying)
StopAnimPreview();
Debug.Log("[画报编辑器] 已取消EditorApplication.update");
}
@ -60,7 +63,7 @@ namespace PictorialEditor
private Vector2 scrollPos;
private string pictorialName = "NewPictorial";
private string savePath = "Assets/Resources/Pictorial/";
private string savePath = "Assets/Config/Data/ActivityPicturialData/";
private Sprite selectedMainBackgroundSprite; // 大背景图片Sprite
private Texture2D selectedBackgroundTexture;
private Texture2D selectedItemTexture;
@ -74,6 +77,20 @@ namespace PictorialEditor
private double lastCheckTime;
private bool forceCheck = false; // 强制检查标志
// 动画编辑面板
private bool animSectionFoldout = false;
private bool animItemsFoldout = false;
// 动画预览状态
private bool isAnimPreviewPlaying = false;
private double animPreviewStartTime;
private float animPreviewDuration;
private bool animPreviewIsEnter;
// 预览前保存的元素状态:(RectTransform, origPos, origAlpha, Image)
private System.Collections.Generic.List<(UnityEngine.RectTransform rt, Vector3 origPos, float origAlpha, Image img)> animPreviewSnapshot;
// 预览动画目标
private System.Collections.Generic.List<(UnityEngine.RectTransform rt, Image img, Vector3 basePos, PictorialItemAnimData anim)> animPreviewTargets;
private const string EDITOR_SCENE_PATH = "Assets/Editor/Pictorial/PictorialEditor.unity";
private const string CANVAS_PATH = "Canvas";
// 注意:大背景路径是相对路径,在编辑器场景中可能不存在
@ -426,35 +443,333 @@ namespace PictorialEditor
private void DrawPictorialManagement()
{
EditorGUILayout.LabelField("2. 画报元素管理", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"当前画报: {pictorialData.pictorialName}");
EditorGUILayout.LabelField($"元素数量: {pictorialData.items.Count}");
EditorGUILayout.LabelField($"分辨率配置: {pictorialData.resolutionConfigs.Count}");
// 显示提示信息
EditorGUILayout.HelpBox(
"层次顺序说明:\n" +
"- 在Hierarchy视图中可以直接拖拽元素调整渲染顺序\n" +
"- 调整后,点击‘保存当前分辨率数据’即可保存层次关系\n" +
"- 调整后,点击'保存当前分辨率数据'即可保存层次关系\n" +
"- 层次关系会同步应用到所有分辨率配置",
MessageType.Info
);
EditorGUILayout.Space();
// 添加画报元素
DrawAddItemSection();
EditorGUILayout.Space();
// 动画配置
DrawAnimSection();
EditorGUILayout.Space();
// 分辨率配置
DrawResolutionSection();
EditorGUILayout.Space();
// 保存/导出
DrawSaveSection();
}
/// <summary>
/// 动画配置区域
/// </summary>
private void DrawAnimSection()
{
// 确保 animData 不为 null
if (pictorialData.animData == null)
pictorialData.animData = new PictorialAnimData();
var anim = pictorialData.animData;
animSectionFoldout = EditorGUILayout.Foldout(animSectionFoldout, "动画配置", true, EditorStyles.foldoutHeader);
if (!animSectionFoldout) return;
EditorGUI.indentLevel++;
// --- 全局时长 ---
EditorGUILayout.LabelField("全局动画时长", EditorStyles.boldLabel);
anim.enterDuration = EditorGUILayout.FloatField("进入动画时长(秒)", anim.enterDuration);
anim.exitDuration = EditorGUILayout.FloatField("退出动画时长(秒)", anim.exitDuration);
EditorGUILayout.Space(5);
// --- 预览按钮 ---
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(isAnimPreviewPlaying);
if (GUILayout.Button("进入动画预览", GUILayout.Height(28)))
{
StartAnimPreview(isEnter: true);
}
if (GUILayout.Button("退出动画预览", GUILayout.Height(28)))
{
StartAnimPreview(isEnter: false);
}
EditorGUI.EndDisabledGroup();
if (isAnimPreviewPlaying)
{
if (GUILayout.Button("停止预览", GUILayout.Width(70), GUILayout.Height(28)))
{
StopAnimPreview();
}
}
EditorGUILayout.EndHorizontal();
if (isAnimPreviewPlaying)
{
double elapsed = EditorApplication.timeSinceStartup - animPreviewStartTime;
float t = Mathf.Clamp01((float)(elapsed / animPreviewDuration));
EditorGUILayout.HelpBox($"预览播放中... {t * 100f:F0}%", MessageType.None);
}
EditorGUILayout.Space(5);
// --- 小背景动画 ---
EditorGUILayout.LabelField("小背景 (Item_ATemp_Bg_Second) 动画", EditorStyles.boldLabel);
DrawItemAnimDataFields("进入动画", ref anim.bgEnterAnim);
DrawItemAnimDataFields("退出动画", ref anim.bgExitAnim);
EditorGUILayout.Space(5);
// --- 各元素动画 ---
animItemsFoldout = EditorGUILayout.Foldout(animItemsFoldout, $"画报元素动画 (共 {pictorialData.items.Count} 个)", true);
if (animItemsFoldout)
{
// 确保列表长度与元素数量一致
EnsureAnimListSize(anim.itemEnterAnims, pictorialData.items.Count);
EnsureAnimListSize(anim.itemExitAnims, pictorialData.items.Count);
EditorGUI.indentLevel++;
for (int i = 0; i < pictorialData.items.Count; i++)
{
string itemName = pictorialData.items[i].itemName;
EditorGUILayout.LabelField($"[{i}] {itemName}", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
DrawItemAnimDataFields("进入动画", ref anim.itemEnterAnims, i);
DrawItemAnimDataFields("退出动画", ref anim.itemExitAnims, i);
EditorGUI.indentLevel--;
EditorGUILayout.Space(3);
}
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
}
/// <summary>
/// 绘制单个 PictorialItemAnimData 字段(列表版本,按索引)
/// </summary>
private void DrawItemAnimDataFields(string label, ref System.Collections.Generic.List<PictorialItemAnimData> list, int index)
{
var data = list[index];
DrawItemAnimDataFields(label, ref data);
list[index] = data;
}
/// <summary>
/// 绘制单个 PictorialItemAnimData 字段(引用版本)
/// </summary>
private void DrawItemAnimDataFields(string label, ref PictorialItemAnimData data)
{
if (data == null) data = new PictorialItemAnimData();
data.enabled = EditorGUILayout.Toggle(label + " 启用", data.enabled);
if (data.enabled)
{
EditorGUI.indentLevel++;
data.offset = EditorGUILayout.Vector3Field("位移偏移量", data.offset);
data.alphaStart = EditorGUILayout.Slider("Alpha 起始", data.alphaStart, 0f, 1f);
data.alphaEnd = EditorGUILayout.Slider("Alpha 结束", data.alphaEnd, 0f, 1f);
EditorGUI.indentLevel--;
}
}
/// <summary>
/// 确保动画列表长度至少有 targetCount 个,只添加不截断
/// (截断逻辑由 SyncItemDataWithScene/AddPictorialItem 精确管理)
/// </summary>
private void EnsureAnimListSize(System.Collections.Generic.List<PictorialItemAnimData> list, int targetCount)
{
while (list.Count < targetCount)
list.Add(new PictorialItemAnimData());
}
// ===================== 动画预览 =====================
/// <summary>
/// 开始动画预览(编辑器模式下使用 EditorApplication.update 驱动)
/// </summary>
private void StartAnimPreview(bool isEnter)
{
if (pictorialData?.animData == null) return;
var anim = pictorialData.animData;
animPreviewIsEnter = isEnter;
animPreviewDuration = isEnter ? anim.enterDuration : anim.exitDuration;
if (animPreviewDuration <= 0f) animPreviewDuration = 0.01f;
// 收集场景中的元素(编辑器里用 PictorialItemIdentifier 标识,按 pictorialData.items 顺序匹配)
var bgT = backgroundObject?.transform.Find("Item_ATemp_Bg_Second");
Image bgImg = bgT?.GetComponent<Image>();
// 按 pictorialData.items 顺序,通过 itemId 找到对应 GameObject
var sceneItems = new System.Collections.Generic.List<(UnityEngine.RectTransform, Image)>();
foreach (var itemData in pictorialData.items)
{
GameObject itemObj = itemObjects.FirstOrDefault(obj =>
{
if (obj == null) return false;
var id = obj.GetComponent<PictorialItemIdentifier>();
return id != null && id.itemId == itemData.itemId;
});
if (itemObj != null)
{
var rt = itemObj.GetComponent<UnityEngine.RectTransform>();
var img = itemObj.GetComponentInChildren<Image>(true);
sceneItems.Add((rt, img));
}
else
{
// 占位,保持与 itemEnterAnims/itemExitAnims 的索引对齐
sceneItems.Add((null, null));
}
}
// 保存初始状态快照
animPreviewSnapshot = new System.Collections.Generic.List<(UnityEngine.RectTransform, Vector3, float, Image)>();
if (bgImg != null)
{
var bgRt = bgImg.GetComponent<UnityEngine.RectTransform>();
animPreviewSnapshot.Add((bgRt, bgRt.anchoredPosition3D, bgImg.color.a, bgImg));
}
foreach (var (rt, img) in sceneItems)
{
if (rt == null) continue; // 跳过占位
animPreviewSnapshot.Add((rt, rt.anchoredPosition3D, img != null ? img.color.a : 1f, img));
}
// 构建动画目标
animPreviewTargets = new System.Collections.Generic.List<(UnityEngine.RectTransform, Image, Vector3, PictorialItemAnimData)>();
var bgAnimData = isEnter ? anim.bgEnterAnim : anim.bgExitAnim;
if (bgAnimData != null && bgAnimData.enabled && bgImg != null)
{
var rt = bgImg.GetComponent<UnityEngine.RectTransform>();
animPreviewTargets.Add((rt, bgImg, rt.anchoredPosition3D, bgAnimData));
}
var itemAnimList = isEnter ? anim.itemEnterAnims : anim.itemExitAnims;
EnsureAnimListSize(itemAnimList, sceneItems.Count);
for (int i = 0; i < sceneItems.Count; i++)
{
if (i >= itemAnimList.Count) break;
var ia = itemAnimList[i];
if (ia == null || !ia.enabled) continue;
var (rt, img) = sceneItems[i];
if (rt == null) continue; // 跳过占位(元素不在场景中)
animPreviewTargets.Add((rt, img, rt.anchoredPosition3D, ia));
}
if (animPreviewTargets.Count == 0)
{
Debug.Log("[画报编辑器] 动画预览:没有启用动画的元素");
return;
}
// 设置初始状态
foreach (var (rt, img, basePos, ia) in animPreviewTargets)
{
rt.anchoredPosition3D = isEnter ? (basePos + ia.offset) : basePos;
if (img != null) SetPreviewImageAlpha(img, ia.alphaStart);
}
animPreviewStartTime = EditorApplication.timeSinceStartup;
isAnimPreviewPlaying = true;
// 注册帧更新
EditorApplication.update += OnAnimPreviewUpdate;
Debug.Log($"[画报编辑器] 开始{(isEnter ? "" : "退")}动画预览,时长={animPreviewDuration}s目标数={animPreviewTargets.Count}");
}
/// <summary>
/// 动画预览逐帧更新
/// </summary>
private void OnAnimPreviewUpdate()
{
if (!isAnimPreviewPlaying) return;
double elapsed = EditorApplication.timeSinceStartup - animPreviewStartTime;
float rawT = Mathf.Clamp01((float)(elapsed / animPreviewDuration));
float smoothT = Mathf.SmoothStep(0f, 1f, rawT);
bool isEnter = animPreviewIsEnter;
foreach (var (rt, img, basePos, ia) in animPreviewTargets)
{
if (rt == null) continue;
Vector3 fromPos = isEnter ? (basePos + ia.offset) : basePos;
Vector3 toPos = isEnter ? basePos : (basePos + ia.offset);
rt.anchoredPosition3D = Vector3.Lerp(fromPos, toPos, smoothT);
if (img != null)
SetPreviewImageAlpha(img, Mathf.Lerp(ia.alphaStart, ia.alphaEnd, smoothT));
}
SceneView.RepaintAll();
Repaint();
if (rawT >= 1f)
{
// 播放结束,恢复原始状态
StopAnimPreview();
}
}
/// <summary>
/// 停止并恢复动画预览
/// </summary>
private void StopAnimPreview()
{
isAnimPreviewPlaying = false;
EditorApplication.update -= OnAnimPreviewUpdate;
// 恢复所有元素到初始状态
if (animPreviewSnapshot != null)
{
foreach (var (rt, origPos, origAlpha, img) in animPreviewSnapshot)
{
if (rt != null) rt.anchoredPosition3D = origPos;
if (img != null) SetPreviewImageAlpha(img, origAlpha);
}
}
SceneView.RepaintAll();
Repaint();
Debug.Log("[画报编辑器] 动画预览结束,已恢复元素状态");
}
private static void SetPreviewImageAlpha(Image image, float alpha)
{
if (image == null) return;
Color c = image.color;
c.a = Mathf.Clamp01(alpha);
image.color = c;
}
/// <summary>
/// 新建画报(清空当前状态,进入新画报编辑流程)
@ -747,12 +1062,33 @@ namespace PictorialEditor
}
// 删除已不存在于场景中的元素数据
// 先收集所有要删除的原始索引,然后从大到小倒序删除,避免顺序删除时索引偏移
if (itemsToRemove.Count > 0)
{
// 收集原始索引(在任何删除发生前)
var indexesToRemove = itemsToRemove
.Select(itemData => pictorialData.items.IndexOf(itemData))
.Where(idx => idx >= 0)
.OrderByDescending(idx => idx)
.ToList();
// 先从 items 列表中移除
foreach (var itemData in itemsToRemove)
{
pictorialData.items.Remove(itemData);
Debug.Log($"[画报编辑器] 检测到元素已被删除,同步移除数据: {itemData.itemName}");
pictorialData.items.Remove(itemData);
}
// 从大到小倒序删除动画数据,保证索引不偏移
if (pictorialData.animData != null)
{
foreach (int idx in indexesToRemove)
{
if (idx < pictorialData.animData.itemEnterAnims.Count)
pictorialData.animData.itemEnterAnims.RemoveAt(idx);
if (idx < pictorialData.animData.itemExitAnims.Count)
pictorialData.animData.itemExitAnims.RemoveAt(idx);
}
}
}
}
@ -1265,7 +1601,7 @@ namespace PictorialEditor
}
// 打开文件选择对话框
string jsonPath = EditorUtility.OpenFilePanel("选择画报JSON文件", "Assets/Resources/Pictorial", "json");
string jsonPath = EditorUtility.OpenFilePanel("选择画报JSON文件", "Assets/Config/Data/ActivityPicturialData", "json");
if (string.IsNullOrEmpty(jsonPath))
{
@ -1592,6 +1928,13 @@ namespace PictorialEditor
pictorialData.items.Add(itemData);
// 同步添加动画数据占位(保持与 items 索引对齐)
if (pictorialData.animData != null)
{
pictorialData.animData.itemEnterAnims.Add(new PictorialItemAnimData());
pictorialData.animData.itemExitAnims.Add(new PictorialItemAnimData());
}
// 克隆模板元素
GameObject itemObj = Instantiate(templateItemObject, backgroundObject.transform);
itemObj.name = itemName;