更新空气墙
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
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" +
|
||||
"请指定 groundSprite(Sprite 需有 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 对齐 dir,up=Y 保持朝上)
|
||||
segObj.transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
|
||||
|
||||
// 添加 BoxCollider:X=厚度, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Ci4ZvSr8Vy1K7YVdL2e+1SXH9+0+0MevgfqHwYx9g0KL0FOBKdnKSOQ=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user