Files
2026-07-05 19:05:34 +08:00

429 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
namespace IndianOceanAssets.Engine2_5D
{
/// <summary>
/// 空气墙自动生成器 —— 从 Sprite Physics Shape 或 PolygonCollider2D 提取边界,
/// 沿边界路径生成 3D 碰撞墙,阻止玩家 / 敌人走出地图。
///
/// 使用方式:
/// 1. 场景中挂有此脚本的 GameObject(与 GroundBuilder 放在同一位置即可)
/// 2. 指定 groundSprite 或 polygonCollider 作为数据源
/// 3. Inspector 点 "Build Walls" 按钮生成碰撞墙
/// 4. 点 "Clear Walls" 可清除已生成的碰撞墙
/// </summary>
public class BoundaryWallGenerator : MonoBehaviour
{
// ============================================================
// 数据源
// ============================================================
[Header("数据源")]
[Tooltip("地面精灵渲染器。从此 Sprite 的 Physics Shape 自动提取边界轮廓。\n" +
"需要在 Sprite Editor 里 Generate Physics Shape(默认自动生成)。")]
[SerializeField] SpriteRenderer groundSprite;
[Tooltip("可选:直接用 PolygonCollider2D 的路径作为边界。\n" +
"留空则用 Sprite Physics Shape。")]
[SerializeField] PolygonCollider2D polygonCollider;
// ============================================================
// 碰撞设置
// ============================================================
[Header("碰撞设置")]
[Tooltip("碰撞墙高度(Y 轴方向延伸)")]
[SerializeField] float wallHeight = 3f;
[Tooltip("碰撞墙厚度(XZ 平面法线方向)。BoxCollider 模式下建议 ≥ 0.3,太薄容易穿透。")]
[SerializeField] float wallThickness = 0.5f;
[Tooltip("碰撞墙所在的 Layer")]
[SerializeField] int wallLayer = 0;
[Tooltip("碰撞模式:BoxCollider 阵列(推荐,有厚度,碰撞可靠);MeshCollider(零厚度,可能穿透)。")]
[SerializeField] ColliderMode colliderMode = ColliderMode.BoxColliderArray;
[Header("调试")]
[Tooltip("勾选后为每段墙体添加可见 Cube,方便在 Scene 中查看位置和大小")]
[SerializeField] bool showDebugVisuals = true;
[Header("优化")]
[Tooltip("Douglas-Peucker 简化容差(世界单位)。0 = 不简化。\n" +
"建议 0.05~0.2,减少顶点数同时保持轮廓形状。")]
[SerializeField] float simplifyTolerance = 0.1f;
public enum ColliderMode
{
MeshCollider,
BoxColliderArray
}
private const string GENERATED = "Generated_BoundaryWalls";
// ============================================================
// 公开 API
// ============================================================
/// <summary>
/// 从数据源提取边界点(世界坐标)。与 GroundBuilder 逻辑一致。
/// </summary>
public List<List<Vector3>> GetBoundaryPaths()
{
var paths = new List<List<Vector3>>();
if (polygonCollider != null)
{
for (int i = 0; i < polygonCollider.pathCount; i++)
{
Vector2[] path = polygonCollider.GetPath(i);
var worldPath = new List<Vector3>();
foreach (var p in path)
{
Vector3 world = polygonCollider.transform.TransformPoint(p.x, p.y, 0f);
worldPath.Add(world);
}
if (worldPath.Count >= 3)
paths.Add(worldPath);
}
}
else if (groundSprite != null && groundSprite.sprite != null)
{
Sprite sprite = groundSprite.sprite;
int shapeCount = sprite.GetPhysicsShapeCount();
for (int i = 0; i < shapeCount; i++)
{
var shape = new List<Vector2>();
sprite.GetPhysicsShape(i, shape);
var worldPath = new List<Vector3>();
foreach (var p in shape)
{
Vector3 world = groundSprite.transform.TransformPoint(p.x, p.y, 0f);
worldPath.Add(world);
}
if (worldPath.Count >= 3)
paths.Add(worldPath);
}
}
return paths;
}
/// <summary>
/// 生成碰撞墙。
/// </summary>
[ContextMenu("Build Walls")]
public void Build()
{
var paths = GetBoundaryPaths();
if (paths.Count == 0)
{
Debug.LogWarning("[BoundaryWallGenerator] 没有找到边界数据。\n" +
"请指定 groundSpriteSprite 需有 Physics Shape)或 polygonCollider。");
return;
}
// 简化
if (simplifyTolerance > 0f)
{
for (int i = 0; i < paths.Count; i++)
paths[i] = SimplifyClosedPath(paths[i], simplifyTolerance);
}
// 清除旧的
ClearGenerated();
// 创建容器(放在场景根层级,确保 mesh 世界坐标 = 本地坐标)
GameObject container = new GameObject(GENERATED);
container.transform.position = Vector3.zero;
container.transform.rotation = Quaternion.identity;
container.transform.localScale = Vector3.one;
container.layer = wallLayer;
container.isStatic = true;
switch (colliderMode)
{
case ColliderMode.MeshCollider:
BuildMeshColliders(container, paths);
break;
case ColliderMode.BoxColliderArray:
BuildBoxColliders(container, paths);
break;
}
int totalEdges = 0;
foreach (var p in paths) totalEdges += p.Count;
Debug.Log($"[BoundaryWallGenerator] 生成完成:{paths.Count} 条路径,{totalEdges} 个边界点," +
$"碰撞模式 {colliderMode},墙高 {wallHeight},厚度 {wallThickness}。");
}
/// <summary>
/// 清除所有已生成的碰撞墙。
/// </summary>
[ContextMenu("Clear Walls")]
public void ClearGenerated()
{
// 容器在场景根层级(非子物体),用 FindObjectOfType 查找
GameObject old = GameObject.Find(GENERATED);
if (old != null)
{
if (Application.isPlaying)
Destroy(old);
else
DestroyImmediate(old);
}
}
// ============================================================
// MeshCollider 模式
// ============================================================
void BuildMeshColliders(GameObject container, List<List<Vector3>> paths)
{
for (int i = 0; i < paths.Count; i++)
{
var path = paths[i];
GameObject wallObj = new GameObject($"BoundaryWall_{i}");
wallObj.transform.SetParent(container.transform, false);
wallObj.layer = wallLayer;
wallObj.isStatic = true;
MeshFilter mf = wallObj.AddComponent<MeshFilter>();
mf.sharedMesh = GenerateWallMesh(path);
// 不添加 MeshRenderer —— 空气墙只需要碰撞,不需要渲染(无阴影、无绘制开销)
MeshCollider mc = wallObj.AddComponent<MeshCollider>();
mc.sharedMesh = mf.sharedMesh;
mc.convex = false;
}
}
/// <summary>
/// 沿闭合路径生成垂直墙体 Mesh(双面),用于 MeshCollider。
/// 直接使用世界坐标(容器物体与 BoundaryWallGenerator 同 Transform,本地偏移为零)。
/// </summary>
Mesh GenerateWallMesh(List<Vector3> worldPoints)
{
int n = worldPoints.Count;
// 顶点:top ring (0..n-1) + bottom ring (n..2n-1)
Vector3[] verts = new Vector3[n * 2];
Vector3[] norms = new Vector3[n * 2];
for (int i = 0; i < n; i++)
{
int next = (i + 1) % n;
verts[i] = worldPoints[i] + new Vector3(0, wallHeight, 0); // 顶部
verts[n + i] = worldPoints[i]; // 底部(地面高度)
// 法线:边的垂直方向,朝外
Vector3 edge = worldPoints[next] - worldPoints[i];
Vector3 normal = Vector3.Cross(edge, Vector3.down).normalized;
norms[i] = normal;
norms[n + i] = normal;
}
// 三角形:双面
var tris = new List<int>(n * 12);
for (int i = 0; i < n; i++)
{
int next = (i + 1) % n;
int ti = i, tj = next, bi = n + i, bj = n + next;
// 正面
tris.Add(ti); tris.Add(bi); tris.Add(bj);
tris.Add(ti); tris.Add(bj); tris.Add(tj);
// 背面(反绕)
tris.Add(ti); tris.Add(bj); tris.Add(bi);
tris.Add(ti); tris.Add(tj); tris.Add(bj);
}
Mesh mesh = new Mesh { name = "BoundaryWall" };
mesh.SetVertices(verts);
mesh.SetNormals(norms);
mesh.SetTriangles(tris, 0);
mesh.RecalculateBounds();
return mesh;
}
// ============================================================
// BoxCollider 阵列模式
// ============================================================
void BuildBoxColliders(GameObject container, List<List<Vector3>> paths)
{
int segIndex = 0;
for (int p = 0; p < paths.Count; p++)
{
var path = paths[p];
int n = path.Count;
for (int i = 0; i < n; i++)
{
int next = (i + 1) % n;
Vector3 a = path[i];
Vector3 b = path[next];
// 边段中点(世界坐标)
Vector3 mid = (a + b) * 0.5f;
float length = Vector3.Distance(a, b);
if (length < 0.001f) continue;
// 边段方向(投影到 XZ 平面,忽略 Y 分量)
Vector3 dir = (b - a).normalized;
dir.y = 0f;
if (dir.sqrMagnitude < 0.0001f) continue;
dir.Normalize();
// 创建子物体
GameObject segObj = new GameObject($"WallSeg_{segIndex}");
segObj.transform.SetParent(container.transform, false);
segObj.layer = wallLayer;
segObj.isStatic = true;
// 定位到中点,Y 偏移到墙高一半
segObj.transform.position = mid + new Vector3(0, wallHeight * 0.5f, 0);
// 旋转使 Z 轴沿边段方向(LookRotation: forward=Z 对齐 dirup=Y 保持朝上)
segObj.transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
// 添加 BoxColliderX=厚度, Y=墙高, Z=边段长度
BoxCollider bc = segObj.AddComponent<BoxCollider>();
bc.size = new Vector3(wallThickness, wallHeight, length);
bc.center = Vector3.zero;
// 调试可视化:添加 Cube 让墙体可见
if (showDebugVisuals)
{
GameObject vis = GameObject.CreatePrimitive(PrimitiveType.Cube);
vis.name = "Visual";
vis.transform.SetParent(segObj.transform, false);
vis.transform.localPosition = Vector3.zero;
vis.transform.localRotation = Quaternion.identity;
vis.transform.localScale = new Vector3(wallThickness, wallHeight, length);
vis.isStatic = false;
// 移除 Cube 自带的 Collider,避免双重碰撞
var cubeCol = vis.GetComponent<Collider>();
if (cubeCol != null) DestroyImmediate(cubeCol);
}
segIndex++;
}
}
}
// ============================================================
// Douglas-Peucker 路径简化(与 GroundBuilder 一致)
// ============================================================
List<Vector3> SimplifyClosedPath(List<Vector3> points, float tolerance)
{
if (points.Count < 4) return points;
// 找离 points[0] 最远的点
float maxDist = 0f;
int farIdx = 1;
for (int i = 1; i < points.Count; i++)
{
float d = Vector3.Distance(points[0], points[i]);
if (d > maxDist) { maxDist = d; farIdx = i; }
}
var arc1 = new List<Vector3>();
var arc2 = new List<Vector3>();
for (int i = 0; i <= farIdx; i++) arc1.Add(points[i]);
for (int i = farIdx; i < points.Count; i++) arc2.Add(points[i]);
arc2.Add(points[0]);
arc1 = DouglasPeucker(arc1, tolerance);
arc2 = DouglasPeucker(arc2, tolerance);
var result = new List<Vector3>(arc1);
for (int i = 1; i < arc2.Count - 1; i++)
result.Add(arc2[i]);
return result;
}
List<Vector3> DouglasPeucker(List<Vector3> points, float tolerance)
{
if (points.Count < 3) return points;
float maxDist = 0f;
int maxIdx = 0;
Vector3 a = points[0];
Vector3 b = points[points.Count - 1];
for (int i = 1; i < points.Count - 1; i++)
{
float d = PerpDistance(points[i], a, b);
if (d > maxDist) { maxDist = d; maxIdx = i; }
}
if (maxDist > tolerance)
{
var left = DouglasPeucker(points.GetRange(0, maxIdx + 1), tolerance);
var right = DouglasPeucker(points.GetRange(maxIdx, points.Count - maxIdx), tolerance);
var result = new List<Vector3>(left);
result.AddRange(right.GetRange(1, right.Count - 1));
return result;
}
else
{
return new List<Vector3> { a, b };
}
}
float PerpDistance(Vector3 p, Vector3 lineA, Vector3 lineB)
{
Vector3 ab = lineB - lineA;
float lenSq = ab.sqrMagnitude;
if (lenSq < 1e-8f) return Vector3.Distance(p, lineA);
Vector3 ap = p - lineA;
float t = Vector3.Dot(ap, ab) / lenSq;
t = Mathf.Clamp01(t);
Vector3 proj = lineA + t * ab;
return Vector3.Distance(p, proj);
}
// ============================================================
// Gizmos —— 在 Scene 视图中预览碰撞边界
// ============================================================
private void OnDrawGizmosSelected()
{
var paths = GetBoundaryPaths();
if (paths.Count == 0) return;
Gizmos.color = new Color(1f, 0.4f, 0.2f, 0.8f); // 橙色,区别于 GroundBuilder 的蓝色
foreach (var path in paths)
{
for (int i = 0; i < path.Count; i++)
{
Vector3 cur = path[i];
Vector3 next = path[(i + 1) % path.Count];
// 底部线
Gizmos.DrawLine(cur, next);
// 顶部线
Vector3 curTop = cur + new Vector3(0, wallHeight, 0);
Vector3 nextTop = next + new Vector3(0, wallHeight, 0);
Gizmos.DrawLine(curTop, nextTop);
// 垂直连接线
Gizmos.DrawLine(cur, curTop);
Gizmos.DrawSphere(cur, 0.1f);
}
}
}
}
}