Files
XericLibrary-WebGL-Publish/Runtime/Blueprint/Render/BlueprintCanvasRenderTool.cs
T
2026-07-30 09:18:26 +08:00

134 lines
7.7 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericLibrary.Runtime.Blueprint.Render
{
/// <summary>Chunk renderer 后端基类:仅消费 Graph.ChunkState 的提交,不计算归属或可见性。</summary>
public abstract class BlueprintCanvasRenderTool<TChildRenderer> : BlueprintTool where TChildRenderer : MonoBehaviour
{
protected int MaxPoolSize = 100;
protected readonly Dictionary<ulong, TChildRenderer> ActiveChunks = new Dictionary<ulong, TChildRenderer>();
protected readonly Stack<TChildRenderer> ChunkPool = new Stack<TChildRenderer>();
protected Transform ChunksParent;
protected RectTransform ViewportRect;
private Vector2 _prevPan;
private float _prevZoom;
protected abstract TChildRenderer BuildChunkRenderer(GameObject chunkGo);
/// <summary>当前模板方法提交;子类不得重新计算 chunk 本地偏移。</summary>
protected BlueprintElementRenderSubmission CurrentElementSubmission { get; private set; }
protected abstract void AppendElementData(TChildRenderer renderer, IBlueprintElement element, ulong chunkID);
protected abstract void ClearChunkData(TChildRenderer renderer);
protected abstract void SetChunkDirty(TChildRenderer renderer);
protected abstract bool AcceptsElement(IBlueprintElement element);
/// <summary>每个非退出 Chunk 提交的开始钩子。</summary>
protected virtual void BeginChunkSubmission(ulong chunkID, TChildRenderer renderer) { }
/// <summary>每个非退出 Chunk 提交的结束钩子。</summary>
protected virtual void EndChunkSubmission(ulong chunkID, TChildRenderer renderer) { }
protected virtual void OnChunkRecycled(ulong chunkID, TChildRenderer renderer) { }
public override void OnInitialize() => EnsureViewport();
public override void OnConfigChanged() => Graph?.MarkAllElementsDirty();
public override void OnRender()
{
if (Graph?.Canvas == null || Graph.ChunkState == null) return;
EnsureViewport();
if (ViewportRect == null) return;
SyncViewportTransform();
foreach (var submission in Graph.ChunkState.Submissions)
{
if (submission.Kind == ChunkSubmissionKind.Exited) { RecycleChunk(submission.ChunkId); continue; }
var renderer = GetOrCreateChunk(submission.ChunkId);
if (renderer == null) continue;
ClearChunkData(renderer);
BeginChunkSubmission(submission.ChunkId, renderer);
var session = new RenderSession(Graph, submission);
if (submission.Elements != null)
foreach (var element in submission.Elements)
if (AcceptsElement(element)) { CurrentElementSubmission = session.CreateElementSubmission(element); AppendElementData(renderer, element, submission.ChunkId); }
CurrentElementSubmission = null;
EndChunkSubmission(submission.ChunkId, renderer);
SetChunkDirty(renderer);
}
}
public override void OnDestroy()
{
DestroyAllChunks(); ViewportRect = null; ChunksParent = null;
}
protected virtual void EnsureViewport()
{
if (ViewportRect != null || Graph?.Canvas == null) return;
var root = Graph.Canvas.GetRootTransform();
if (root == null) return;
ViewportRect = root.Find("__Bp_Viewport") as RectTransform;
if (ViewportRect == null)
{
var go = new GameObject("__Bp_Viewport", typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave;
ViewportRect = go.GetComponent<RectTransform>(); ViewportRect.SetParent(root, false);
}
var rootRect = root as RectTransform;
var pivot = rootRect != null ? rootRect.pivot : new Vector2(.5f, .5f);
ViewportRect.anchorMin = pivot; ViewportRect.anchorMax = pivot; ViewportRect.pivot = pivot;
ViewportRect.anchoredPosition = Vector2.zero; ViewportRect.sizeDelta = rootRect != null ? rootRect.rect.size : Vector2.zero;
ChunksParent = ViewportRect; _prevPan = Graph.Canvas.PanOffset; _prevZoom = Graph.Canvas.ZoomLevel;
ViewportRect.anchoredPosition = _prevPan; ViewportRect.localScale = Vector3.one * _prevZoom;
}
protected virtual void SyncViewportTransform()
{
var pan = Graph.Canvas.PanOffset; var zoom = Graph.Canvas.ZoomLevel;
if (_prevPan != pan) { ViewportRect.anchoredPosition = pan; _prevPan = pan; }
if (!Mathf.Approximately(_prevZoom, zoom)) { ViewportRect.localScale = Vector3.one * zoom; _prevZoom = zoom; }
}
protected Vector2 ChunkOrigin(ulong id) => Graph.ChunkState.GetChunkOrigin(id);
/// <summary>一次 chunk 提交的标准化坐标、LOD 与版本上下文。</summary>
protected sealed class RenderSession
{
private readonly BlueprintGraph _graph; private readonly ChunkSubmission _submission; private readonly Vector2 _origin; private readonly Rect _bounds;
public RenderSession(BlueprintGraph graph, ChunkSubmission submission) { _graph=graph; _submission=submission; _origin=graph.ChunkState.GetChunkOrigin(submission.ChunkId); _bounds=graph.ChunkState.GetChunkRect(submission.ChunkId); }
public BlueprintElementRenderSubmission CreateElementSubmission(IBlueprintElement element) => new BlueprintElementRenderSubmission(element, element.BoundingBox.center, element.BoundingBox, _origin, _bounds, _graph.CurrentLod, _submission.ContentRevision, _submission.LodRevision);
}
public sealed class BlueprintElementRenderSubmission
{
public readonly IBlueprintElement Element; public readonly Vector2 CanvasPosition, ChunkOrigin, ChunkLocalPosition; public readonly Rect CanvasBounds, ChunkLocalBounds; public readonly BlueprintLodLevel LOD; public readonly int ContentRevision, LodRevision;
public BlueprintElementRenderSubmission(IBlueprintElement element, Vector2 canvasPosition, Rect canvasBounds, Vector2 chunkOrigin, Rect chunkBounds, BlueprintLodLevel lod, int contentRevision, int lodRevision)
{ Element=element; CanvasPosition=canvasPosition; CanvasBounds=canvasBounds; ChunkOrigin=chunkOrigin; ChunkLocalPosition=canvasPosition-chunkOrigin; ChunkLocalBounds=new Rect(canvasBounds.position-chunkOrigin, canvasBounds.size); LOD=lod; ContentRevision=contentRevision; LodRevision=lodRevision; }
/// <summary>将提交元素相关的 Canvas 空间数据转换为当前 Chunk 局部空间。</summary>
public Vector2 ToChunkLocal(Vector2 canvasPosition) => canvasPosition - ChunkOrigin;
public Rect ToChunkLocal(Rect canvasBounds) => new Rect(canvasBounds.position - ChunkOrigin, canvasBounds.size);
}
protected TChildRenderer GetOrCreateChunk(ulong id)
{
if (ActiveChunks.TryGetValue(id, out var renderer) && renderer != null) return renderer;
BlueprintChunkState.DecodeCoord(id, out int x, out int y);
GameObject go;
if (ChunkPool.Count > 0) { go = ChunkPool.Pop().gameObject; go.SetActive(true); }
else { go = new GameObject("Chunk_" + x + "_" + y, typeof(RectTransform)); go.hideFlags = HideFlags.HideAndDontSave; go.transform.SetParent(ChunksParent, false); }
var rt = go.GetComponent<RectTransform>(); var pivot = ViewportRect.pivot;
rt.anchorMin = pivot; rt.anchorMax = pivot; rt.pivot = pivot; rt.anchoredPosition = ChunkOrigin(id);
rt.sizeDelta = new Vector2(Graph.ChunkState.ChunkSize, Graph.ChunkState.ChunkSize);
renderer = go.GetComponent<TChildRenderer>() ?? BuildChunkRenderer(go); ActiveChunks[id] = renderer; return renderer;
}
private void RecycleChunk(ulong id)
{
if (!ActiveChunks.TryGetValue(id, out var renderer) || renderer == null) { ActiveChunks.Remove(id); return; }
OnChunkRecycled(id, renderer);
if (ChunkPool.Count < MaxPoolSize) { renderer.gameObject.SetActive(false); ChunkPool.Push(renderer); }
else UnityEngine.Object.Destroy(renderer.gameObject);
ActiveChunks.Remove(id);
}
protected virtual void DestroyAllChunks()
{
foreach (var pair in ActiveChunks) if (pair.Value != null) UnityEngine.Object.Destroy(pair.Value.gameObject);
ActiveChunks.Clear();
while (ChunkPool.Count > 0) { var item = ChunkPool.Pop(); if (item != null) UnityEngine.Object.Destroy(item.gameObject); }
}
}
}