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

自定义房间区域功能

效果:

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

功能:

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

核心功能——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来了~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL(二)-基础操作

一、约束 有时候&#xff0c;数据库中数据是有约束的&#xff0c;比如 性别列&#xff0c;你不能填一些奇奇怪怪的数据~ 如果靠人为的来对数据进行检索约束的话&#xff0c;肯定是不行的&#xff0c;人肯定会犯错~因此就需要让计算机对插入的数据进行约束要求&#xff01; 约…

下载HF AutoTrain 模型的配置文件

下载HF AutoTrain 模型的配置文件 一.在huggingface上创建AutoTrain项目二.通过HF用户名和autotrain项目名,拼接以下url,下载模型列表(json格式)到指定目录三.解析上面的json文件、去重、批量下载模型配置文件(权重以外的文件) 一.在huggingface上创建AutoTrain项目 二.通过HF用…

电脑没电关机,wsl和docker又挂了,附解决过程

如题&#xff0c;开了个会没带笔记本电源&#xff0c;点啊弄关机后docker打不开&#xff0c;我以为是docker坏了&#xff0c;结果docker报错&#xff1a; An unexpected error occurred while executing a WSL command. Either shut down WSL down with wsl --shutdown, and/or…

植被变化趋势线性回归以及可视化

目录 植被变化线性回归ee.Reducer.linearFit().reduce()案例:天水市2004-2023年EVI线性回归趋势在该图中,使用了从红色到蓝色的渐变来表示负趋势到正趋势。红色代表在某段时间中,植被覆盖减少,绿色表示持平,蓝色表示植被覆盖增加。 植被变化线性回归 该部分参考Google…

Java(十一)---String类型

文章目录 前言1.String类的重要性2.常用方法2.1.字符串的创建2.2.字符串的比较2.2.1.比较是否引用同一个对象2.2.2.boolean equals(Object anObject) 方法&#xff1a;2.2.3.int CompareTo(String s)2.2.4.int compareToIgnoreCase(String str) 方法&#xff1a; 2.3.字符串的查…

单调栈原理+练习

首先用一道题引出单调栈 码蹄集 (matiji.net) 首先画一个图演示山的情况&#xff1a; 最暴力的做法自然是O(n方)的双循环遍历&#xff0c;这么做的思想是求出当前山右侧有多少座比它小的山&#xff0c;遇见第一个高度大于等于它的就停止。 但是对于我们所求的答案数&#xff…

能不能接受这些坑?买电车前一定要看

图片来源&#xff1a;汽车之家 文 | Auto芯球 作者 | 雷慢 刚有个朋友告诉我&#xff0c;买了电车后感觉被骗了&#xff0c; 很多“坑”都是他买车后才知道的。 不提前研究&#xff0c;不做功课&#xff0c;放着我这个老司机不请教&#xff0c; 这个大冤种他不当谁当&…

C# WinForm —— 25 ProgressBar 介绍与使用

1. 简介 用于显示某个操作的进度 2. 常用属性 属性解释(Name)控件ID&#xff0c;在代码里引用的时候会用到,一般以 pbar 开头ContextMenuStrip右键菜单Enabled控件是否可用ForeColor用于显示进度的颜色MarqueeAnimationSpeed进度条动画更新的速度&#xff0c;以毫秒为单位M…

【Python】解决Python错误报错:IndexError: tuple index out of range

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

什么是PLAB?

接上文PLAB---》 可以看到和TLAB很像&#xff0c;PLAB即 Promotion Local Allocation Buffers。用在年轻代对象晋升到老年代时。 在多线程并行执行YGC时&#xff0c;可能有很多对象需要晋升到老年代&#xff0c;此时老年代的指针就"热"起来了&#xff0c;于是搞了个…

新游启航 失落的方舟台服注册指南 一文教会你方舟台服注册

新游启航&#xff01;失落的方舟台服注册指南&#xff01;一文教会你方舟台服注册 失落的方舟作为本月最受期待游戏之一&#xff0c;在上线之际许多玩家已经有点急不可待了。这款游戏是由开发商Smile gate开发的一款MMORPG类型游戏&#xff0c;这款游戏的基本玩法与其他MMORPG…

44-1 waf绕过 - WAF的分类

一、云 WAF 通常包含在 CDN 中的 WAF。在配置云 WAF 时&#xff0c;DNS 需要解析到 CDN 的 IP 上。请求 URL 时&#xff0c;数据包会先经过云 WAF 进行检测&#xff0c;如果通过检测&#xff0c;再将数据包流向主机。 二、硬件IPS/IDS防护、硬件WAF 硬件IPS/IDS防护&#xff…

归并排序C++代码详解,思路流程+代码注释,带你完全学会归并排序

归并排序 归并排序是一种经典的排序算法&#xff0c;属于分治算法的一种。其核心思想是将一个大问题分解成若干个较小的子问题来解决&#xff0c;然后根据子问题的解构建原问题的解。在排序问题中&#xff0c;归并排序将数组分成两半&#xff0c;分别对这两半进行排序&#xf…

0基础认识C语言(理论+实操3)

所有籍籍无名的日子里 我从未看轻自己半分 小伙伴们&#xff0c;一起开始我们今天的话题吧 一、算法操作符 1.双目操作符 为何叫双目操作符呢&#xff1f;其实是因为我们进行加减乘除的时候&#xff0c;至少得需要两个数字进行这些运算&#xff0c;而这个数字就被称为操作数…

Java对sqlserver表的image字段图片读取和输出本地

Java代码实现对sqlserver数据库表的image字段图片的读取&#xff0c;和输出存储到本地 由于表image字段图片存的内容是二进制值&#xff0c;如何输出保存到本地&#xff1a; 代码示例&#xff1a;&#xff08;注&#xff1a;连接sqlserver数据库需配置其驱动文件&#xff09; …

基础—SQL—DCL(数据控制语言)之用户管理

一、引言 分类全称描述DCLData Control Language&#xff08;数据控制语言&#xff09;用来创建和管理数据库用户以及控制数据库的访问权限 1、图解 右边的是我们的 MySQL 的数据库服务器&#xff0c;左边是假设的两个用户 1、 DCL 主要控制的就是有哪些用户可以来访问这台 My…

英语翻译程序,可以对用户自己建立的词汇表进行增删查改

⑴ 自行建立一个包含若干英文单词的词汇表文件&#xff0c;系统初始化时导入内存&#xff0c;用于进行句子翻译。 ⑵ 用户可以输入单词或者句子&#xff0c;在屏幕上显示对应翻译结果。 ⑶ 用户可对词汇表进行添加和删除&#xff0c;并能将更新的词汇表存储到文件中。 #defi…

RDD实战:排序算子 - sortBy()

在本实战案例中&#xff0c;我们将使用Apache Spark的sortBy()算子来对一个包含学生信息的RDD进行排序操作。 排序规则如下&#xff1a; 首先按照性别升序排列。在性别相同的情况下&#xff0c;按照年龄降序排列。 步骤1&#xff1a;创建学生信息列表 首先&#xff0c;我们创…

《计算机工程与应用》最新投稿经验2024年5月

研二下第一次投稿&#xff0c;深度学习长时间序列预测方向&#xff0c;选择了《计算机工程与应用》期刊&#xff0c;是CSCD扩展刊北大核心&#xff0c;且在24年被EI收录等等。4.10交稿到最后5.31收到录用通知&#xff0c;历时不到2个月&#xff0c;总的来说编辑部效率确实高。 …

基于强化学习的控制率参数自主寻优

1.介绍 针对控制建模与设计场景中控制参数难以确定的普遍问题&#xff0c;提出了一种基于强化学习的控制律参数自主优化解决方案。该方案以客户设计的控制律模型为基础&#xff0c;根据自定义的控制性能指标&#xff0c;自主搜索并确定最优的、可状态依赖的控制参数组合。 可…