Story流程+本地化
This commit is contained in:
@@ -47,12 +47,12 @@ namespace Ichni.Story
|
||||
public GameObject tutorialBlockPrefab;
|
||||
public GameObject connectorPrefab;
|
||||
|
||||
[Header("Layout Settings")]
|
||||
/// <summary>
|
||||
/// 相邻叙事列之间的固定距离。它是 StoryData.gridColumn 的坐标步距,
|
||||
/// 不等于也不会覆盖任何 Block Prefab 的宽度。
|
||||
/// 默认值为 Important TextBlock 宽度 600 加最小横向留白 120。
|
||||
/// </summary>
|
||||
[Header("Layout Settings")]
|
||||
[Tooltip("StoryData.gridColumn 的水平步距,不会改变任何 Prefab 的实际宽度。")]
|
||||
public float columnStep = 720f;
|
||||
|
||||
@@ -72,8 +72,15 @@ namespace Ichni.Story
|
||||
/// 最左 Block 的真实左边缘会被放置在 <see cref="marginLeft"/> 加本值的位置;
|
||||
/// 默认 1600,等价于在原有布局基础上将整棵剧情树向右平移 1600 单位。
|
||||
/// </summary>
|
||||
[Tooltip("为左侧 Helper 等常驻 UI 预留的额外空间。默认 1600,会让所有 Block 整体向右移动 1600。")]
|
||||
public float helperReservedLeftSpace = 1600f;
|
||||
[Tooltip("为左侧 Helper 等常驻 UI 预留的额外空间。默认 1200,会让所有 Block 整体向右移动 1200。")]
|
||||
public float helperReservedLeftSpace = 1200f;
|
||||
/// <summary>
|
||||
/// 为覆盖在剧情页底部的常驻 UI(当前为 Helper)预留的额外可滚动空间。
|
||||
/// Content 使用垂直中心 Pivot;布局时会同步将树向上平移本值的一半,
|
||||
/// 从而使本值完整地增加在内容下方,而非被平均分配到上下两侧。
|
||||
/// </summary>
|
||||
[Tooltip("为底部 Helper 等常驻 UI 预留的额外可滚动空间。值越大,树拖到顶部时越能完整显示底部 Block。")]
|
||||
public float helperReservedLowerSpace = 150f;
|
||||
|
||||
public float marginRight = 50f;
|
||||
public float marginTop = 50f;
|
||||
@@ -93,6 +100,15 @@ namespace Ichni.Story
|
||||
// 未来 Timeline 在独立 UI 容器中生成 Marker 时,也必须叠加此偏移量才能与 Block 中心对齐。
|
||||
private float _layoutHorizontalOffset;
|
||||
|
||||
// Content 使用垂直中心 Pivot。此偏移把 helperReservedLowerSpace 完整分配给内容下方;
|
||||
// SetUpBackground 可能重复执行,因此每次计算前都必须先撤销上一次偏移,避免位置累积漂移。
|
||||
private float _layoutVerticalOffset;
|
||||
|
||||
// StoryTree 的“开头”由场景中 Content 的初始位置决定,而不是硬编码为 Vector2.zero。
|
||||
// 因此 UI 设计调整默认视口时,不需要同步修改导航代码。
|
||||
private Vector2 _initialContentPosition;
|
||||
private bool _hasInitialContentPosition;
|
||||
|
||||
/// <summary>当前已构建章节的 StoryData(供对话控制器切换 YarnProject 等使用)。</summary>
|
||||
public StoryData ActiveStoryData => _storyData;
|
||||
|
||||
@@ -105,6 +121,15 @@ namespace Ichni.Story
|
||||
/// </summary>
|
||||
public float LayoutHorizontalOffset => _layoutHorizontalOffset;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (content == null)
|
||||
return;
|
||||
|
||||
_initialContentPosition = content.anchoredPosition;
|
||||
_hasInitialContentPosition = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定叙事列在 BlockContainer 中的最终中心 X 坐标。
|
||||
/// Timeline Marker 必须使用此方法,而不是自行只计算 gridColumn * columnStep,
|
||||
@@ -620,6 +645,15 @@ namespace Ichni.Story
|
||||
return;
|
||||
}
|
||||
|
||||
// 以未附加底部预留区的原始网格位置重新计算边界,保证重复布局保持稳定。
|
||||
if (!Mathf.Approximately(_layoutVerticalOffset, 0f))
|
||||
{
|
||||
foreach (StoryBlockView block in blocks)
|
||||
block.blockRect.anchoredPosition -= Vector2.up * _layoutVerticalOffset;
|
||||
|
||||
_layoutVerticalOffset = 0f;
|
||||
}
|
||||
|
||||
float minLeft = float.PositiveInfinity;
|
||||
float maxRight = float.NegativeInfinity;
|
||||
float maxVertical = 0f; // 距垂直中线的最大延伸(上下取较大者,用于对称扩展 content 高度)
|
||||
@@ -648,18 +682,29 @@ namespace Ichni.Story
|
||||
float horizontalCorrection = targetLeftEdge - minLeft;
|
||||
if (!Mathf.Approximately(horizontalCorrection, 0f))
|
||||
{
|
||||
foreach (StoryBlockView block in blocks)
|
||||
block.blockRect.anchoredPosition += Vector2.right * horizontalCorrection;
|
||||
|
||||
foreach (StoryBlockView block in blocks) block.blockRect.anchoredPosition += Vector2.right * horizontalCorrection;
|
||||
maxRight += horizontalCorrection;
|
||||
}
|
||||
|
||||
|
||||
// SetUpBackground 可能因尺寸变化被重复调用;偏移量必须累积,不能在第二次调用时
|
||||
// 被归零,否则未来 Timeline 会失去与已平移 Block 的横向对齐。
|
||||
_layoutHorizontalOffset += horizontalCorrection;
|
||||
|
||||
float width = Mathf.Max(maxRight + marginRight, minContentWidth);
|
||||
float height = Mathf.Max(maxVertical * 2f + marginTop + marginBottom, minContentHeight);
|
||||
float baseHeight = Mathf.Max(maxVertical * 2f + marginTop + marginBottom, minContentHeight);
|
||||
float lowerReservedSpace = Mathf.Max(0f, helperReservedLowerSpace);
|
||||
float height = baseHeight + lowerReservedSpace;
|
||||
|
||||
// 将 Content 扩展出的空间全部留在下方:Content 顶边会上移 lower/2,
|
||||
// Block 也上移 lower/2,因此顶部视觉边界不变;Content 底边下移 lower/2,
|
||||
// 与 Block 的上移共同形成完整 lower 的新增底部拖拽空间。
|
||||
_layoutVerticalOffset = lowerReservedSpace * 0.5f;
|
||||
if (!Mathf.Approximately(_layoutVerticalOffset, 0f))
|
||||
{
|
||||
foreach (StoryBlockView block in blocks)
|
||||
block.blockRect.anchoredPosition += Vector2.up * _layoutVerticalOffset;
|
||||
}
|
||||
|
||||
content.sizeDelta = new Vector2(width, height);
|
||||
}
|
||||
@@ -686,6 +731,61 @@ namespace Ichni.Story
|
||||
public StoryBlockView GetBlockView(string blockId) =>
|
||||
blocks.FirstOrDefault(b => b.blockId == blockId);
|
||||
|
||||
/// <summary>
|
||||
/// 将 StoryTree 视口居中到指定 Block。使用实际 RectTransform 的屏幕位置计算偏移,
|
||||
/// 因而适用于不同尺寸的 Block、不同分辨率以及包含 Helper 预留区的布局。
|
||||
/// </summary>
|
||||
public void FocusBlock(StoryBlockView block)
|
||||
{
|
||||
if (block == null || block.blockRect == null || content == null || storyScrollRect == null)
|
||||
{
|
||||
FocusStart();
|
||||
return;
|
||||
}
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
RectTransform viewport = storyScrollRect.viewport != null
|
||||
? storyScrollRect.viewport
|
||||
: storyScrollRect.GetComponent<RectTransform>();
|
||||
Canvas canvas = storyScrollRect.GetComponentInParent<Canvas>();
|
||||
Camera uiCamera = canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay
|
||||
? canvas.worldCamera
|
||||
: null;
|
||||
|
||||
Vector3 blockCenterWorld = block.blockRect.TransformPoint(block.blockRect.rect.center);
|
||||
Vector2 blockCenterScreen = RectTransformUtility.WorldToScreenPoint(uiCamera, blockCenterWorld);
|
||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
viewport, blockCenterScreen, uiCamera, out Vector2 blockCenterInViewport))
|
||||
{
|
||||
FocusStart();
|
||||
return;
|
||||
}
|
||||
|
||||
// 局部坐标原点位于 Viewport 中心时,目标在右侧意味着 Content 应向左移动;
|
||||
// 直接减去该偏移即可,且不依赖 Content 或 Block 的 Pivot 配置。
|
||||
content.anchoredPosition -= blockCenterInViewport;
|
||||
storyScrollRect.StopMovement();
|
||||
currentBlock = block;
|
||||
storyTimeline?.RefreshMarkerPositions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复剧情树在场景中设计的默认浏览位置,用于没有对应 SongBlock 时的安全回退。
|
||||
/// </summary>
|
||||
public void FocusStart()
|
||||
{
|
||||
if (content == null)
|
||||
return;
|
||||
|
||||
if (_hasInitialContentPosition)
|
||||
content.anchoredPosition = _initialContentPosition;
|
||||
|
||||
storyScrollRect?.StopMovement();
|
||||
currentBlock = null;
|
||||
storyTimeline?.RefreshMarkerPositions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 Timeline、Yarn 和调试工具提供当前章节静态 Block 定义的查询入口。
|
||||
/// </summary>
|
||||
@@ -704,6 +804,7 @@ namespace Ichni.Story
|
||||
connectors.Clear();
|
||||
|
||||
_layoutHorizontalOffset = 0f;
|
||||
_layoutVerticalOffset = 0f;
|
||||
currentBlock = null;
|
||||
|
||||
if (content != null)
|
||||
|
||||
Reference in New Issue
Block a user