Unity 自定义房间布局系统 设计与实现一个灵活的房间放置系统 ——自定义房间区域功能

news2024/10/6 1:52:19

自定义房间区域功能

效果:

请添加图片描述
请添加图片描述

功能:

  • 能够自定义房间的大小
  • 一键生成放置区域
  • 可控的放置网格点
  • 当物体放置到区域内可自动吸附
  • 物体是否可放置,放置时如果与其他物体交叉则不可放置(纯算法计算)
  • 管理房间内的物体,能够添加或删除房间内的物体
  • 直观可调整的视觉效果

核心功能——RoomReferenceFrame

管理房间的边界信息和相关操作:

1.1 属性和字段

这些字段定义了房间的显示属性和调整手柄的参数,比如网格颜色、手柄大小和网格间隔等。
在这里插入图片描述

public bool showWorldArea = true;
public bool showForbidArea = true;
public Color gizmosColor = Color.black;
public Color gizmosXColor = new Color(1 , 0 , 0 , 0.2f);
public Color gizmosYColor = new Color(0 , 1 , 0 , 0.2f);
public Color gizmosZColor = new Color(0 , 0 , 1 , 0.2f);
public float HandlesSize = 0.5f;
public Vector2Int LeftAndRightInterval = Vector2Int.one*5;
public Vector2Int TopAndBottomInterval = Vector2Int.one*5;
public Vector2Int FrontAndBackInterval = Vector2Int.one*5;
1.2 初始位置定义
Vector3 worldLeftLocation, worldRightLocation, worldTopLocation, worldBottomLocation, worldFrontLocation, worldBackLocation;
[HideInInspector]
public Vector3 leftLocation = new Vector3(-5 , 0 , 0);
[HideInInspector]
public Vector3 rightLocation = new Vector3(5 , 0 , 0);
[HideInInspector]
public Vector3 topLocation = new Vector3(0 , 5 , 0);
[HideInInspector]
public Vector3 bottomLocation = new Vector3(0 , -5 , 0);
[HideInInspector]
public Vector3 frontLocation = new Vector3(0 , 0 , 5);
[HideInInspector]
public Vector3 backLocation = new Vector3(0 , 0 , -5);
public int CornerContagion = 1;

这些字段定义了房间各个边界的位置,使用HideInInspector属性隐藏在Inspector中,不直接显示给用户。

1.3 网格对齐方法
public Vector3 SnapToGrid(Vector3 point , SnapDirection snapDirection)
{
    Vector3 localPoint = transform.InverseTransformPoint(point);
    Vector3 localStart;
    Vector3 localEnd;
    int totalIntervals;

    switch(snapDirection)
    {
        case SnapDirection.Front:
        case SnapDirection.Back:
            localStart=transform.InverseTransformPoint(worldLeftLocation);
            localEnd=transform.InverseTransformPoint(worldRightLocation);
            totalIntervals=FrontAndBackInterval.x-1;
            localPoint.x=SnapCoordinate(localPoint.x , localStart.x , localEnd.x , totalIntervals);

            localStart=transform.InverseTransformPoint(worldBottomLocation);
            localEnd=transform.InverseTransformPoint(worldTopLocation);
            totalIntervals=FrontAndBackInterval.y-1;
            localPoint.y=SnapCoordinate(localPoint.y , localStart.y , localEnd.y , totalIntervals);

            // Z轴坐标保持不变
            break;

        case SnapDirection.Left:
        case SnapDirection.Right:
            localStart=transform.InverseTransformPoint(worldFrontLocation);
            localEnd=transform.InverseTransformPoint(worldBackLocation);
            totalIntervals=LeftAndRightInterval.x-1;
            localPoint.z=SnapCoordinate(localPoint.z , localStart.z , localEnd.z , totalIntervals);

            // 与Front/Back情况相同的Y轴处理
            localStart=transform.InverseTransformPoint(worldBottomLocation);
            localEnd=transform.InverseTransformPoint(worldTopLocation);
            totalIntervals=LeftAndRightInterval.y-1;
            localPoint.y=SnapCoordinate(localPoint.y , localStart.y , localEnd.y , totalIntervals);

            // X轴坐标保持不变
            break;

        case SnapDirection.Top:
        case SnapDirection.Bottom:
            localStart=transform.InverseTransformPoint(worldLeftLocation);
            localEnd=transform.InverseTransformPoint(worldRightLocation);
            totalIntervals=TopAndBottomInterval.x-1;
            localPoint.x=SnapCoordinate(localPoint.x , localStart.x , localEnd.x , totalIntervals);

            localStart=transform.InverseTransformPoint(worldFrontLocation);
            localEnd=transform.InverseTransformPoint(worldBackLocation);
            totalIntervals=TopAndBottomInterval.y-1;
            localPoint.z=SnapCoordinate(localPoint.z , localStart.z , localEnd.z , totalIntervals);

            // Y轴坐标保持不变
            break;
    }

    return transform.TransformPoint(localPoint);
}

private float SnapCoordinate(float coordinate , float start , float end , int intervals)
{
    float relativePos = (coordinate-start)/(end-start);
    int intervalIndex = Mathf.RoundToInt(relativePos*intervals);
    if(intervalIndex<CornerContagion)
        intervalIndex=CornerContagion;
    if(intervalIndex>intervals-(CornerContagion))
        intervalIndex=intervals-(CornerContagion);
    float lerpValue = (float)intervalIndex/intervals;
    return Mathf.Lerp(start , end , lerpValue);
}
SnapToGrid方法用于将一个点对齐到网格节点上,使得物体在特定的方向上沿网格对齐。这在房间布局和物体摆放中非常有用,有助于保持场景中的物体排列整齐。

请添加图片描述

1.坐标转换:将输入点point转换到局部坐标系localPoint。
2.根据对齐方向处理:

  • Front/Back方向:对局部x和y坐标进行对齐。
  • Left/Right方向:对局部z和y坐标进行对齐。
  • Top/Bottom方向:对局部x和z坐标进行对齐。

3.调用SnapCoordinate方法:计算并返回对齐后的坐标。
4.坐标还原:将对齐后的局部坐标转换回世界坐标。

SnapCoordinate方法用于将单个坐标值对齐到最近的网格节点,具体步骤如下:

1.计算相对位置:将坐标coordinate标准化到0到1范围内。
2.确定区间索引:根据相对位置和总区间数计算所在区间索引。
3.调整区间索引:确保区间索引不超出范围,避免靠近边界的物体超出区域。
4.计算对齐坐标:使用线性插值计算最终的对齐坐标。

优点
  • 可维护性强:将对齐逻辑封装在SnapToGrid和SnapCoordinate方法中,便于代 码的维护和扩展。
  • 灵活性高:通过SnapDirection参数指定对齐方向,适应不同场景需求。
  • 防止超出边界:CornerContagion参数控制边界区域,确保对齐后的坐标不会超出预设范围。
  • 简化复杂计算:使用线性插值和标准化简化坐标计算,保证精度和效率。
    这些好处和技巧使得该方法在实现房间物体的网格对齐时既简洁高效,又具备高度的灵活性和可控性。
1.4 其他辅助方法

ResetArea(): 重置房间边界位置。
请添加图片描述

public void ResetArea()
{
    leftLocation=new Vector3(-1 , 0 , 0);
    rightLocation=new Vector3(1 , 0 , 0);
    topLocation=new Vector3(0 , 1 , 0);
    bottomLocation=new Vector3(0 , -1 , 0);
    frontLocation=new Vector3(0 , 0 , 1);
    backLocation=new Vector3(0 , 0 , -1);
    transform.rotation=Quaternion.identity;
}

AddRoomItem(MultiMeshAreaCalculator roomItem): 添加房间内的物体。

List<MultiMeshAreaCalculator> RoomItems = new List<MultiMeshAreaCalculator>();
List<AreaData> AreaDatas = new List<AreaData>();//AreaData存储空间数据,一般为八个Vector3数据,一个方块的8个点位

public void AddRoomItem(MultiMeshAreaCalculator roomItem)
{
    RoomItems.Add(roomItem);
    AddItemData(roomItem);
}
void AddItemData(MultiMeshAreaCalculator roomItem)
{
    AreaDatas.AddRange(roomItem.GetItemData());//roomItem.GetItemData()提供物体占用区域数据
}

RemoveAddRoomItem(MultiMeshAreaCalculator roomItem): 删除房间内的物体。

public void RemoveAddRoomItem(MultiMeshAreaCalculator roomItem)
{
    RoomItems.Remove(roomItem);
    AreaDatas.Clear();
    foreach(var item in RoomItems)
    {
        AddItemData(item);
    }
}

AutoAdjustEdge 方法用于自动调整房间边界的位置。这是通过在每个方向上投射射线(Ray)并检测与碰撞体的交点来实现的。
请添加图片描述

public void AutoAdjustEdge()
{
    RaycastHit hit;
    Ray ray = new Ray(transform.position , transform.up);
    if (Physics.Raycast(ray , out hit))
        topLocation = transform.InverseTransformPoint(hit.point);
    ray = new Ray(transform.position , -transform.up);
    if (Physics.Raycast(ray , out hit))
        bottomLocation = transform.InverseTransformPoint(hit.point);
    ray = new Ray(transform.position , transform.right);
    if (Physics.Raycast(ray , out hit))
        rightLocation = transform.InverseTransformPoint(hit.point);
    ray = new Ray(transform.position , -transform.right);
    if (Physics.Raycast(ray , out hit))
        leftLocation = transform.InverseTransformPoint(hit.point);
    ray = new Ray(transform.position , transform.forward);
    if (Physics.Raycast(ray , out hit))
        frontLocation = transform.InverseTransformPoint(hit.point);
    ray = new Ray(transform.position , -transform.forward);
    if (Physics.Raycast(ray , out hit))
        backLocation = transform.InverseTransformPoint(hit.point);
}
射线投射 (Raycasting)
  • 使用 Physics.Raycast 方法在各个方向上投射射线,检测与其他物体的碰撞。
  • 通过射线投射,可以精确地确定房间边界的实际位置,而不必手动调整。
坐标转换
topLocation = transform.InverseTransformPoint(hit.point);
  • 使用 transform.InverseTransformPoint 方法将世界坐标系下的碰撞点转换为本地坐标系。
  • 这样做可以确保边界位置与房间物体的本地坐标系一致,便于后续的计算和调整。
自适应性
  • 通过自动调整边界位置,AutoAdjustEdge 方法使得房间能够自适应环境中的其他物体,自动调整其尺寸和位置。
  • 这种自适应性对于动态环境中的房间管理和布局调整非常有用。

准备好!上硬菜🥦

IsOverlapping 方法用于检查一个 roomItem 是否与现有房间的任意部分重叠,并返回重叠的 Renderer 列表。

public bool IsOverlapping(MultiMeshAreaCalculator roomItem , out List<Renderer> renderers)
{
    bool returnValue = true;
    List<AreaData> Temp = new List<AreaData>();
    renderers=new List<Renderer>();
    if(roomItem.ListOverallBounds.Count!=0)
    {
        Temp.AddRange(roomItem.ListOverallBounds);
    }
    if(roomItem.childData!=null)
    {
        foreach(var item in roomItem.childData)
        {
            if(item.ListOverallBounds.Count!=0)
            {
                Temp.AddRange(item.ListOverallBounds);
            }
        }
    }
    foreach(var item in Temp)
    {
        foreach(var AreaDate in AreaDatas)
        {
            if(AreCubesOverlapping(item.GetCorners() , AreaDate.GetCorners()))
            {
                renderers.Add(item.renderer);
                returnValue=false;
            }
        }
    }
    return returnValue;
}

判断房间物体是否重叠——分离轴定理(Separating Axis Theorem, SAT)

这里不详细解释SAT的原理,我们只学怎么利用这个算法

AreCubesOverlapping 方法

这个方法运用了多种技术和算法,特别是基于分离轴定理(Separating Axis Theorem, SAT)的碰撞检测算法。
这些方法共同作用,管理房间物体和边界的逻辑。

  1. 获取所有可能的分离轴
    从 corners1 和 corners2 中获取本地轴。
    通过两组顶点的边计算交叉乘积,得到法向量。
  2. 检查每个轴上的分离
    对每个轴调用 OverlapOnAxis 方法,检查两个立方体在该轴上的投影是否重叠。
bool AreCubesOverlapping(Vector3[] corners1 , Vector3[] corners2)
{
    // 获取所有可能的分离轴
    List<Vector3> axes = new List<Vector3>();

    // 添加立方体1的本地轴
    axes.AddRange(GetLocalAxes(corners1));
    // 添加立方体2的本地轴
    axes.AddRange(GetLocalAxes(corners2));

    // 对每一对边计算交叉乘积,得到立方体之间边的法向量
    for(int i = 0; i<corners1.Length; i+=4)
    {
        for(int j = 0; j<corners2.Length; j+=4)
        {
            Vector3 edge1 = corners1[i+1]-corners1[i];
            Vector3 edge2 = corners2[j+1]-corners2[j];
            Vector3 axis = Vector3.Cross(edge1 , edge2).normalized;
            if(axis!=Vector3.zero)
            {
                axes.Add(axis);
            }
        }
    }

    // 检查每一个轴,查看是否在该轴上存在分离
    foreach(Vector3 axis in axes)
    {
        if(!OverlapOnAxis(corners1 , corners2 , axis))
        {
            return false; // 在这个轴上找到分离,说明不相交
        }
    }

    return true; // 所有可能的分离轴都没有找到分离,说明相交
}
bool OverlapOnAxis(Vector3[] vertices1 , Vector3[] vertices2 , Vector3 axis)
{
    float min1 = float.MaxValue;
    float max1 = float.MinValue;
    float min2 = float.MaxValue;
    float max2 = float.MinValue;

    // 计算顶点在立方体1坐标轴上的投影
    foreach(Vector3 vertex in vertices1)
    {
        float projection = Vector3.Dot(vertex , axis);
        min1=Mathf.Min(min1 , projection);
        max1=Mathf.Max(max1 , projection);
    }

    // 计算顶点在立方体2坐标轴上的投影
    foreach(Vector3 vertex in vertices2)
    {
        float projection = Vector3.Dot(vertex , axis);
        min2=Mathf.Min(min2 , projection);
        max2=Mathf.Max(max2 , projection);
    }

    // 检查投影是否重叠
    if(max1<min2||max2<min1)
    {
        return false; // 在这个轴上没有重叠
    }

    return true; // 重叠存在于这个轴上
}
GetLocalAxes 方法

获取立方体的本地轴。

List<Vector3> GetLocalAxes(Vector3[] corners)
{
    List<Vector3> axes = new List<Vector3>();

    Vector3 edge1 = corners[1] - corners[0];
    Vector3 edge2 = corners[2] - corners[0];
    Vector3 edge3 = corners[4] - corners[0];

    if (edge1 != Vector3.zero) axes.Add(edge1.normalized);
    if (edge2 != Vector3.zero) axes.Add(edge2.normalized);
    if (edge3 != Vector3.zero) axes.Add(edge3.normalized);

    return axes;
}

核心技术分离轴定理 (SAT):

请添加图片描述

  • 利用 SAT 检测两个凸多边形(这里是立方体)的碰撞。通过检查投影在所有可能的分离轴上是否重叠来判断碰撞。
  • 这种方法非常高效,可以处理复杂的三维碰撞检测。

局部轴计算:

  • GetLocalAxes 方法提取了立方体的本地轴,这些轴是分离轴的一部分。
  • 通过计算边之间的交叉乘积,生成了额外的分离轴,提高了检测的准确性。

投影计算:

  • OverlapOnAxis 方法通过将立方体的顶点投影到分离轴上,计算最小和最大投影值。
  • 检查投影是否重叠,决定了两个立方体在该轴上是否分离。

代码封装与复用:

  • 通过将复杂的碰撞检测逻辑分解为多个方法(如 AreCubesOverlapping、GetLocalAxes 和 OverlapOnAxis),提高了代码的可读性和可维护性。
  • 这种封装方式使得每个方法的职责单一,便于调试和扩展。
这些技术亮点和技巧使得 IsOverlapping 方法能够高效、准确地检测房间物体之间的重叠情况,对于复杂的房间布局和物体摆放场景具有重要意义。

视觉效果

以下代码主要用于在Unity编辑器中通过Gizmos绘制房间区域和禁区区域,以便开发者在编辑模式下直观地看到这些区域。具体代码包含OnDrawGizmos、OnDrawGizmosSelected、DrawChildArea、DrawMesh和DrawInterval等方法。
在这里插入图片描述

OnDrawGizmos
  • 当在编辑器中绘制Gizmos时调用。
  • showWorldArea:是否一直显示在Scene视图,不选中也会显示网格
  • isSelect:为了避免OnDrawGizmos与OnDrawGizmosSelected渲染两次网格
  • showForbidArea:显示禁区区域,在编辑器模式下更加直观的观察物体的放置区域
bool isSelect = false;

private void OnDrawGizmos()
{
    if (showWorldArea && !isSelect)
        DrawMesh();
    isSelect = false;
    if (showForbidArea)
    {
        foreach (var item in AreaDatas)
        {
            DrawChildArea(item.GetCorners());
        }
    }
}
OnDrawGizmosSelected
  • 当在编辑器中选择对象时调用。
  • isSelect:为了避免OnDrawGizmos与OnDrawGizmosSelected渲染两次网格
  • DrawMesh:只要选中就一定会渲染网格。
  • 绘制左右、上下、前后边界线,后面使用手柄作为连接。
private void OnDrawGizmosSelected()
{
    isSelect = true;
    DrawMesh();
    Gizmos.color = gizmosColor;
    Gizmos.DrawLine(worldLeftLocation, worldRightLocation);
    Gizmos.DrawLine(worldTopLocation, worldBottomLocation);
    Gizmos.DrawLine(worldFrontLocation, worldBackLocation);
}
DrawChildArea
  • 绘制禁止区域的立方体。
  • 依次绘制立方体的前面、后面以及连接前后面的线。
void DrawChildArea(Vector3[] corners)
{
    if (corners.Length != 8)
        return;
    Gizmos.color = gizmosColor;
    Gizmos.DrawLine(corners[0], corners[1]);
    Gizmos.DrawLine(corners[1], corners[3]);
    Gizmos.DrawLine(corners[3], corners[2]);
    Gizmos.DrawLine(corners[2], corners[0]);
    Gizmos.DrawLine(corners[4], corners[5]);
    Gizmos.DrawLine(corners[5], corners[7]);
    Gizmos.DrawLine(corners[7], corners[6]);
    Gizmos.DrawLine(corners[6], corners[4]);
    Gizmos.DrawLine(corners[0], corners[4]);
    Gizmos.DrawLine(corners[1], corners[5]);
    Gizmos.DrawLine(corners[2], corners[6]);
    Gizmos.DrawLine(corners[3], corners[7]);
}

DrawMesh
  • 绘制网格
  • 调用DrawInterval方法绘制网格的各个面。
void DrawMesh()
{
    Gizmos.color = gizmosColor;
    worldLeftLocation = transform.TransformPoint(leftLocation);
    worldRightLocation = transform.TransformPoint(rightLocation);
    worldTopLocation = transform.TransformPoint(topLocation);
    worldBottomLocation = transform.TransformPoint(bottomLocation);
    worldFrontLocation = transform.TransformPoint(frontLocation);
    worldBackLocation = transform.TransformPoint(backLocation);

    DrawInterval(FindOppositePoint(worldLeftLocation, transform.position, worldBottomLocation),
                 FindOppositePoint(worldRightLocation, transform.position, worldBottomLocation),
                 TopAndBottomInterval.x, worldFrontLocation, worldBackLocation, gizmosYColor);
    DrawInterval(FindOppositePoint(worldFrontLocation, transform.position, worldBottomLocation),
                 FindOppositePoint(worldBackLocation, transform.position, worldBottomLocation),
                 TopAndBottomInterval.y, worldLeftLocation, worldRightLocation, gizmosYColor);
    DrawInterval(FindOppositePoint(worldTopLocation, transform.position, worldRightLocation),
                 FindOppositePoint(worldBottomLocation, transform.position, worldRightLocation),
                 LeftAndRightInterval.y, worldFrontLocation, worldBackLocation, gizmosXColor);
    DrawInterval(FindOppositePoint(worldFrontLocation, transform.position, worldRightLocation),
                 FindOppositePoint(worldBackLocation, transform.position, worldRightLocation),
                 LeftAndRightInterval.x, worldTopLocation, worldBottomLocation, gizmosXColor);
    DrawInterval(FindOppositePoint(worldLeftLocation, transform.position, worldFrontLocation),
                 FindOppositePoint(worldRightLocation, transform.position, worldFrontLocation),
                 FrontAndBackInterval.x, worldTopLocation, worldBottomLocation, gizmosZColor);
    DrawInterval(FindOppositePoint(worldTopLocation, transform.position, worldFrontLocation),
                 FindOppositePoint(worldBottomLocation, transform.position, worldFrontLocation),
                 FrontAndBackInterval.y, worldLeftLocation, worldRightLocation, gizmosZColor);
}
void DrawInterval(Vector3 A, Vector3 B, int interval, Vector3 positiveDirection, Vector3 negativeDirection, Color gizmosColor)
{
    float a = gizmosColor.a;
    Color ProhibitedAreaColor = Color.black;
    ProhibitedAreaColor.a = a * 0.2f;
    Gizmos.color = gizmosColor;
    interval--;
    for (int i = 0; i <= interval; i++)
    {
        Gizmos.color = i < CornerContagion || i > interval - CornerContagion ? ProhibitedAreaColor : gizmosColor;
        if (i == 0)
        {
            Gizmos.DrawLine(A, FindOppositePoint(A, transform.position, positiveDirection));
            Gizmos.DrawLine(A, FindOppositePoint(A, transform.position, negativeDirection));
        }
        else if (i == interval)
        {
            Gizmos.DrawLine(B, FindOppositePoint(B, transform.position, positiveDirection));
            Gizmos.DrawLine(B, FindOppositePoint(B, transform.position, negativeDirection));
        }
        else
        {
            Vector3 target = Vector3.Lerp(A, B, (float)i / (float)interval);
            Gizmos.DrawLine(target, FindOppositePoint(target, transform.position, positiveDirection));
            Gizmos.DrawLine(target, FindOppositePoint(target, transform.position, negativeDirection));
        }
    }
}
Vector3 FindOppositePoint(Vector3 a, Vector3 b, Vector3 c)
{
    return a + c - b;
}

Gizmos绘制

  • 使用OnDrawGizmos和OnDrawGizmosSelected方法在Unity编辑器中绘制Gizmos,便于在编辑器中直观地查看对象及其区域。
  • 通过Gizmos可以在编辑器模式下提供视觉反馈,而不会影响运行时性能。

动态计算

  • 使用Vector3.Lerp方法在两个点之间插值计算,可以动态绘制复杂的网格结构。
  • 通过FindOppositePoint方法计算对立点,使得代码简洁高效。

参数控制

  • CornerContagion参数用于控制角落的禁区区域,使得禁区区域的绘制更加灵活。
这些代码通过利用Gizmos在Unity编辑器中绘制出房间的边界和禁区区域,为开发者提供了直观的视觉反馈,方便房间的布局和调整。通过射线投射、颜色区分和动态计算等技术,使得代码具备了很高的可维护性和可读性。

编辑器扩展——Editor_RoomReferenceFrame

用于在Unity编辑器中自定义房间边界的可视化和交互操作。

手动调整区域

该方法负责在Scene视图中绘制控制点和拖拽操作,允许用户通过控制点调整房间边界的位置。具体实现包括:

  • 使用Handles类绘制控制点(手柄)并允许用户拖拽。
  • 转换控制点的世界坐标和本地坐标。
  • 更新RoomReferenceFrame对象的边界位置。在这里插入图片描述
    (一个圆锥手柄不太好看,又增加一个方片手柄,嗯~舒服多了)
 private void OnSceneGUI()
 {
     RoomReferenceFrame roomRefFrame = (RoomReferenceFrame)target;
     Transform trans = roomRefFrame.transform;

     // 使用 Transform.TransformDirection 将方向向量转换为对象的本地空间
     Vector3 localLeftDirection = trans.TransformDirection(Vector3.left);
     Vector3 localRightDirection = trans.TransformDirection(Vector3.right);
     Vector3 localUpDirection = trans.TransformDirection(Vector3.up);
     Vector3 localDownDirection = trans.TransformDirection(Vector3.down);
     Vector3 localForwardDirection = trans.TransformDirection(Vector3.forward);
     Vector3 localBackDirection = trans.TransformDirection(Vector3.back);

     // 设置控制点的颜色
     Color color = roomRefFrame.gizmosColor;
     color.a=0.7f;
     Handles.color=color;

     // 在特定轴上绘制并拖拽控制点
     Handles.CapFunction AcapFunction = Handles.ConeHandleCap;//SphereHandleCap//CubeHandleCap//ConeHandleCap
     Handles.CapFunction BcapFunction = Handles.RectangleHandleCap;//CircleHandleCap//RectangleHandleCap
     Vector3 L_SliderOffset = Vector3.Normalize(roomRefFrame.leftLocation - roomRefFrame.transform.position) * ( roomRefFrame.HandlesSize / 2 );
     Vector3 R_SliderOffset = Vector3.Normalize(roomRefFrame.rightLocation - roomRefFrame.transform.position) * ( roomRefFrame.HandlesSize / 2 );
     Vector3 T_SliderOffset = Vector3.Normalize(roomRefFrame.topLocation - roomRefFrame.transform.position) * ( roomRefFrame.HandlesSize / 2 );
     Vector3 B_SliderOffset = Vector3.Normalize(roomRefFrame.bottomLocation - roomRefFrame.transform.position) * ( roomRefFrame.HandlesSize / 2 );
     Vector3 F_SliderOffset = Vector3.Normalize(roomRefFrame.frontLocation - roomRefFrame.transform.position) * ( roomRefFrame.HandlesSize / 2 );
     Vector3 E_SliderOffset = Vector3.Normalize(roomRefFrame.backLocation - roomRefFrame.transform.position) * ( roomRefFrame.HandlesSize / 2 );
     roomRefFrame.leftLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.leftLocation+ L_SliderOffset) , localLeftDirection , roomRefFrame.HandlesSize , AcapFunction , 1f)) - L_SliderOffset;
     roomRefFrame.rightLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.rightLocation+ R_SliderOffset) , localRightDirection , roomRefFrame.HandlesSize , AcapFunction , 1f))- R_SliderOffset;
     roomRefFrame.topLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.topLocation+ T_SliderOffset) , localUpDirection , roomRefFrame.HandlesSize , AcapFunction , 1f))- T_SliderOffset;
     roomRefFrame.bottomLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.bottomLocation+ B_SliderOffset) , localDownDirection , roomRefFrame.HandlesSize , AcapFunction , 1f))- B_SliderOffset;
     roomRefFrame.frontLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.frontLocation + F_SliderOffset) , localForwardDirection , roomRefFrame.HandlesSize , AcapFunction , 1f)) - F_SliderOffset;
     roomRefFrame.backLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.backLocation + E_SliderOffset) , localBackDirection , roomRefFrame.HandlesSize , AcapFunction , 1f)) - E_SliderOffset;

     // 设置控制点的颜色
     Handles.color=roomRefFrame.gizmosColor;
     roomRefFrame.leftLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.leftLocation) , localLeftDirection , roomRefFrame.HandlesSize , BcapFunction , 1f));
     roomRefFrame.rightLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.rightLocation) , localRightDirection , roomRefFrame.HandlesSize , BcapFunction , 1f));
     roomRefFrame.topLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.topLocation) , localUpDirection , roomRefFrame.HandlesSize , BcapFunction , 1f));
     roomRefFrame.bottomLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.bottomLocation) , localDownDirection , roomRefFrame.HandlesSize , BcapFunction , 1f));
     roomRefFrame.frontLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.frontLocation) , localForwardDirection , roomRefFrame.HandlesSize , BcapFunction , 1f));
     roomRefFrame.backLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.backLocation) , localBackDirection , roomRefFrame.HandlesSize , BcapFunction , 1f));

     // 如果位置发生变化,标记场景已经修改
     if (GUI.changed)
     {
         Undo.RecordObject(roomRefFrame , "Move control points");
         EditorUtility.SetDirty(roomRefFrame);
     }
 }

编辑器面板美化🌸

该方法负责在Inspector面板中绘制自定义的GUI,允许用户通过按钮和滑块调整房间属性。包括:

  • “一键调整区域”按钮用于一键调整区域和重置区域。
  • 调整手柄大小和颜色等。在这里插入图片描述
bool ChangeControl = false;

public override void OnInspectorGUI()
{
    //base.OnInspectorGUI();
    RoomReferenceFrame roomRefFrame = (RoomReferenceFrame)target;

    GUILayout.Space(5);
    if (GUILayout.Button("一键调整区域" , GUILayout.Height(50)))
    {
        roomRefFrame.AutoAdjustEdge();
    }
    GUILayout.Space(15);
    EditorGUILayout.BeginVertical("HelpBox");
    if(ChangeControl)
    {
        if(GUILayout.Button("隐藏" , "prebutton"))
            ChangeControl=!ChangeControl;
    }
    else
    {
        if(GUILayout.Button("调整视觉效果" , "prebutton"))
            ChangeControl=!ChangeControl;
    }
    if(ChangeControl)
    {
        GUILayout.Space(10);
        GUILayout.BeginHorizontal();
        roomRefFrame.showWorldArea = GUILayout.Toggle(roomRefFrame.showWorldArea , new GUIContent("显示网格" , "不选中此物体时也会显示网格") , GUILayout.Width(100));
        roomRefFrame.showForbidArea = GUILayout.Toggle(roomRefFrame.showForbidArea , new GUIContent("显示禁区" , "显示禁止放置的区域,一般情况下放置物体后才会显示") , GUILayout.Width(100));
        GUILayout.EndHorizontal();

        GUILayout.Space(10);
        GUILayout.BeginHorizontal();
        GUILayout.Label("手柄调整" , GUILayout.Width(70));
        roomRefFrame.HandlesSize=EditorGUILayout.Slider(roomRefFrame.HandlesSize , 0.1f , 3 , GUILayout.Width(150));
        roomRefFrame.gizmosColor=EditorGUILayout.ColorField(roomRefFrame.gizmosColor , GUILayout.Width(70));
        GUILayout.EndHorizontal();
        GUILayout.Space(10);
        GUILayout.BeginHorizontal();
        GUILayout.Label("网格颜色" , GUILayout.Width(70));
        roomRefFrame.gizmosXColor=EditorGUILayout.ColorField(roomRefFrame.gizmosXColor , GUILayout.Width(70));
        roomRefFrame.gizmosYColor=EditorGUILayout.ColorField(roomRefFrame.gizmosYColor , GUILayout.Width(70));
        roomRefFrame.gizmosZColor=EditorGUILayout.ColorField(roomRefFrame.gizmosZColor , GUILayout.Width(70));
        GUILayout.EndHorizontal();
    }
    EditorGUILayout.EndVertical();

    GUILayout.Space(10);
    GUILayout.Label("网格间隔调整:" , GUILayout.Width(100));

    GUILayout.BeginHorizontal();
    GUILayout.Label("左右" , GUILayout.Width(40));
    roomRefFrame.LeftAndRightInterval=EditorGUILayout.Vector2IntField("" , roomRefFrame.LeftAndRightInterval);
    GUILayout.EndHorizontal();

    GUILayout.BeginHorizontal();
    GUILayout.Label("前后" , GUILayout.Width(40));
    roomRefFrame.FrontAndBackInterval=EditorGUILayout.Vector2IntField("" , roomRefFrame.FrontAndBackInterval);
    GUILayout.EndHorizontal();

    GUILayout.BeginHorizontal();
    GUILayout.Label("上下" , GUILayout.Width(40));
    roomRefFrame.TopAndBottomInterval=EditorGUILayout.Vector2IntField("" , roomRefFrame.TopAndBottomInterval);
    GUILayout.EndHorizontal();
    GUILayout.Space(10);

    GUILayout.BeginHorizontal();
    GUILayout.Label("边缘禁止系数" , GUILayout.Width(80));
    roomRefFrame.CornerContagion=EditorGUILayout.IntSlider(roomRefFrame.CornerContagion , 0 , Mathf.CeilToInt(roomRefFrame.FindMinimumInterval()/2)-1 , GUILayout.Width(200));
    GUILayout.EndHorizontal();
    GUILayout.Space(20);
    if(GUILayout.Button("重置"))
    {
        if(EditorUtility.DisplayDialog("注意!" , "这将使放置区域恢复到开始状态。" , "是的" , "手滑了~"))
        {
            roomRefFrame.ResetArea();
        }
    }
    if(GUI.changed)
    {
        Undo.RecordObject(roomRefFrame , "Move control points");
        EditorUtility.SetDirty(roomRefFrame);
    }
}
通过了解RoomReferenceFrame和Editor_RoomReferenceFrame,我们应该理解自定义房间系统的实现逻辑和操作方法。前者负责定义和管理房间的边界信息和操作方法,后者则提供了在Unity编辑器中交互和可视化的支持。通过这两个脚本的配合,用户可以方便地在Unity中自定义和调整房间的边界和显示属性。

嚯!~ 到底了!

量太大了?

没事哥们,慢慢消化

这不,Demo来了~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1717293.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【实战JVM】-实战篇-05-内存泄漏及分析

【实战JVM】-实战篇-05-内存泄漏及分析 1 内存溢出和内存泄漏1.1 常见场景1.2 解决内存溢出的方法1.2.1 发现问题1.2.1.1 top1.2.1.2 ViusalVM1.2.1.3 arthas1.2.1.4 PrometheusGrafana 1.2.2 堆内存状况对比1.2.3 内存泄漏原因-代码中1.2.3.1 equals()-hashCode()1.2.3.2 内部…

相机等效焦距

1. 背景 物理焦距我们很熟悉,但是在接触实际的相机参数时,相机厂家会提到一个参数等效焦距,甚至有时候不提供物理焦距,这时候如果我们得到真实的物理焦距需要进行一定的转换.在介绍两者之间的转换关系前,先介绍一下等效焦距的由来. 如上图,假设在某一个镜头,其成像面会出现图…

Linux配置java,maven,marshalsec环境

文章目录 一. Linux配置java环境1.下载jdk文件2.解压tar.gz文件3.设置java环境变量4.验证是否成功 二. Linux配置maven环境1.下载压缩包2.解压tar.gz3. 配置环境变量 三. Linux配置marshalsec环境 一. Linux配置java环境 1.下载jdk文件 mkdir /opt/javawget https://repo.hua…

【设计模式深度剖析】【5】【结构型】【桥接模式】| 以电视和遥控器为例加深理解

&#x1f448;️上一篇:组合模式 | 下一篇:外观模式&#x1f449;️ 设计模式-专栏&#x1f448;️ 目 录 桥接模式(Bridge Pattern)定义英文原话是&#xff1a;直译理解 4个角色UML类图代码示例 应用优点缺点使用场景 示例解析&#xff1a;电视和遥控器UML类图 桥接模式…

【漏洞复现】DT-高清车牌识别摄像机 任意文件读取漏洞

0x01 产品简介 DT-高清 车牌识别摄像机是一款先进的安防设备&#xff0c;采用高清图像传感器和先进的识别算法&#xff0c;能够精准、快速地识别车牌信息。其高清晰该摄像机结合了智能识别技术&#xff0c;支持实时监宴图像质量确保在各种光照和天气条件下都能准确捕捉车牌信息…

【设计模式】JAVA Design Patterns——Factory Method(虚拟构造器模式)

&#x1f50d;目的 为创建一个对象定义一个接口&#xff0c;但是让子类决定实例化哪个类。工厂方法允许类将实例化延迟到子类 &#x1f50d;解释 真实世界例子 铁匠生产武器。精灵需要精灵武器&#xff0c;而兽人需要兽人武器。根据客户来召唤正确类型的铁匠。 通俗描述 它为类…

视频汇聚管理平台EasyCVR程序报错“create jwtSecret del server class:0xf98b6040”的原因排查与解决

国标GB28181协议EasyCVR安防视频监控平台可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级联、磁盘阵列存储、视频集中存储、云存储等丰富的视频能力&#xff0c;平台支持7*24小时实时高清视频监控&#xff0c;能同时播放多路监控视频流…

【学习笔记】Windows GDI绘图(八)画笔Pen与画刷Brush

文章目录 关于Pen改变Pen的宽度width和对齐方式Alignment带线帽的线段连接线条LineJoin自定义虚线用纹理填充线条 关于BrushHatchBrush阴影LinearGradientBrush线性渐变PathGradientBrush 详细示例Pen与Brush的属性与方法 关于Pen 改变Pen的宽度width和对齐方式Alignment 可以…

IntelliJ IDEA Ultimate 2024.1 Mac激活码 Java开发首选IDE

IntelliJ IDEA Ultimate 2024 搜Mac软件之家下载IDEA Mac中文版 IntelliJ IDEA Ultimate 2024是JetBrains公司推出的一款功能强大的集成开发环境&#xff08;IDE&#xff09;&#xff0c;专为专业开发者设计&#xff0c;支持多种编程语言和框架。它提供了一系列高级功能&…

【免费Web系列】JavaWeb实战项目案例五

这是Web第一天的课程大家可以传送过去学习 http://t.csdnimg.cn/K547r 新增员工 前面我们已经实现了员工信息的条件分页查询。 那今天我们要实现的是新增员工的功能实现&#xff0c;页面原型如下&#xff1a; ​ 首先我们先完成"新增员工"的功能开发&#xff0…

Linux--线程的分离、线程库的地址关系的理解、线程的简单封装(二)

线程系列&#xff1a; 线程的认识&#xff1a;讲解线程的概念和线程的基本控制 线程的分离 线程分离是指将一个线程从主线程中分离出来&#xff0c;使其能够独立运行。当一个线程被设置为分离状态时&#xff0c;它结束时系统会自动回收其资源&#xff0c;而不需要其他线程使用…

【喜报】科大睿智服务企业通过CMMI3级认证

​北京建投科信科技发展股份有限公司&#xff08;以下简称“北京建投科技” &#xff09;前身为北京银帝科技发展公司&#xff0c;成立于1993年&#xff0c;注册资本6,000万元&#xff0c;为中国建银投资有限责任公司&#xff08;简称“中国建投”&#xff09;的成员企业建投华…

ovs-vsctl错误:Port does not contain a column whoes name matches “--id“

出错的命令是: ovs-vsctl -- set Bridge br-int mirrors=@m -- --id=@snooper0 get Port snooper0\ -- --id=@patch-tun get Port patch-tun -- --id=@m create Mirror name=mymirror \ select

微软Edge浏览器深度解析:功能、同步、隐私与安全

微软Edge浏览器是微软公司开发的一款网页浏览器,它基于Chromium内核,提供了快速、安全和兼容性良好的网页浏览体验。以下是关于微软Edge浏览器的详细信息和使用指南: 微软Edge浏览器的主要特点: 1. 基于Chromium内核: 渲染引擎:Chromium内核是基于开源项目Blink的,它…

LNMP分布式搭建

一、准备三台主机 192.168.100.11 mysql 192.168.100.12 nginx 192.168.100.13 php 二、关闭防火墙及安全策略 systemctl stop firewalld setenforce 0 三、安装nginx&#xff08;192.168.100.11&#xff09; 1、添加nginx源 vim /etc/yum.repos.d/ng…

数据整理的Compact流程 (二)|OceanBase数据转储合并技术解读(二)

上篇文章《数据整理的Compact流程 &#xff08;一&#xff09;&#xff5c;OceanBase数据转储合并技术解读&#xff08;二&#xff09;》中&#xff0c;有讲解到&#xff0c;在OceanBase数据库中&#xff0c;当MemTable写满时&#xff0c;将其下刷到Mini SSTable的过程包含两个…

正邦科技(day4)

烧录 一、烧录固件二、 通讯模块升级1&#xff1a;USB的方式升级固件2&#xff1a;通过mqtt的方式升级固件3&#xff1a;切换环境 三、 烧录WiFi1&#xff1a;短接2&#xff1a;烧录脚本 设备注意事项&#xff1a; 第一种方式&#xff1a;通信模组和MCU都可以统一烧录BoodLoade…

数据结构---栈队列

栈和队列是我们数据结构中经常使用的数据结构&#xff0c;所以现在来了解一下栈和队列。 栈 特点&#xff1a; 栈是一种特殊的线性表&#xff0c;其中进行数据插入和弹出的部分叫做栈顶&#xff0c;另一端叫做栈底。 只允许数据从栈顶压入&#xff0c;从栈顶弹出即先进后出的…

Mac | Mac M 芯片应用意外退出问题

现象问题 电脑配置&#xff1a;MacBook Pro M1&#xff0c;系统 Sonoma 很多小伙伴新买了 M 芯片的 MacBook&#xff0c;在下载下应用后进行安装&#xff0c;安装成功后却无法打开&#xff0c;提示意外退出。报错如图 原因 部分应用过适配了 M 芯片&#xff0c;但还是有些应…

Windows配置共享文件夹

正文共&#xff1a;888 字 16 图&#xff0c;预估阅读时间&#xff1a;1 分钟 我们前面介绍了如果安装NAS工具&#xff08;废物利用&#xff0c;矿渣装个黑群晖。家庭小NAS搞起来&#xff01;&#xff09;&#xff0c;也介绍过如果配置远程桌面的多账号登录&#xff08;Windows…