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

自定义房间区域功能

效果:

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


文章中MultiMeshAreaCalculator的具体功能参考物体占用的区域及放置点自动化


功能:

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

核心功能——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) , localLeftDirection , roomRefFrame.HandlesSize , AcapFunction , 1f));
     roomRefFrame.rightLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.rightLocation) , localRightDirection , roomRefFrame.HandlesSize , AcapFunction , 1f));
     roomRefFrame.topLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.topLocation) , localUpDirection , roomRefFrame.HandlesSize , AcapFunction , 1f));
     roomRefFrame.bottomLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.bottomLocation) , localDownDirection , roomRefFrame.HandlesSize , AcapFunction , 1f));
     roomRefFrame.frontLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.frontLocation ) , localForwardDirection , roomRefFrame.HandlesSize , AcapFunction , 1f)) ;
     roomRefFrame.backLocation=trans.InverseTransformPoint(Handles.Slider(trans.TransformPoint(roomRefFrame.backLocation ) , localBackDirection , roomRefFrame.HandlesSize , AcapFunction , 1f)) ;

     // 设置控制点的颜色
     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来了~

### 回答1: 好的,我可以回答这个问题。在 Unity 中,你可以使用以下代码来定义一个自定义姓名变量: public string customName; 然后,你可以在代码中使用这个变量来存储和访问玩家的自定义姓名。例如: Debug.Log("玩家的姓名是:" + customName); 这样就可以在控制台中输出玩家的自定义姓名了。当然,你也可以在游戏中的 UI 中显示玩家的姓名,或者在其他需要使用姓名的地方使用这个变量。 ### 回答2: 在Unity中,我们可以通过以下步骤来自定义一个姓名变量: 1. 创建一个新的C#脚本,可以命名为"Player"或者任何你喜欢的名字。 2. 在脚本中创建一个公共的字符串变量,用于存储姓名。例如: ```csharp public string playerName; ``` 3. 在Unity编辑器中选择你希望添加姓名变量的游戏对象。 4. 将脚本拖放到该游戏对象上,或者在添加组件的窗口中找到这个脚本并添加到游戏对象上。 5. 在Unity编辑器中找到该游戏对象的对应脚本组件,可以在Inspector面板中看到"Player Name"或者你在第2步中定义的变量名。 6. 在"Player Name"输入字段中,输入你想要的姓名。 7. 在脚本中,可以通过访问这个变量来获取或者修改玩家的姓名。例如,在Start方法中可以写下以下代码来获取玩家姓名并输出到控制台上: ```csharp void Start() { Debug.Log("玩家姓名是:" + playerName); } ``` 通过上述步骤,你就可以在Unity自定义一个玩家姓名变量了。当你在编辑器中修改姓名后,脚本将会读取和更新这个值,并可以在游戏中使用。 ### 回答3: 在Unity中,我们可以通过定义自定义变量来存储和管理玩家姓名。要实现这一点,我们可以遵循以下步骤: 1. 首先,我们需要在脚本中创建一个公共字符串变量,用于存储玩家的姓名。可以在任何脚本中创建这个变量,例如在角色控制脚本中或专门用于管理玩家信息的脚本中。 2. 使用以下代码在脚本中创建玩家姓名变量: ```csharp public string playerName; ``` 3. 在Unity编辑器中,在脚本挂载的对象上可以看到这个玩家姓名变量。可以直接在编辑器中输入并保存玩家的姓名。这个变量也可以在脚本中动态修改。 4. 在需要使用玩家姓名的地方,我们可以通过访问此变量来获取玩家的姓名。例如,我们可以将玩家姓名显示在游戏界面上的一个文本框中: ```csharp public Text playerNameText; void Update() { playerNameText.text = "Player Name: " + playerName; } ``` 这样,无论我们在脚本中的哪个位置修改了玩家姓名变量,它都会反映到这个文本框上。 通过上述步骤,我们就可以在Unity自定义一个姓名变量,并将其应用于游戏中的任何需要使用玩家姓名的地方。这样,我们可以通过访问和修改这个变量来管理和更新玩家的姓名信息。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唐沢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值