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//ConeHandleCapHandles.CapFunction BcapFunction = Handles.RectangleHandleCap;//CircleHandleCap//RectangleHandleCapVector3 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.mzph.cn/pingmian/20670.shtml

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

相关文章

云原生架构模式

本文主要介绍了云原生架构的主要设计模式&#xff0c;讨论了这些模式的优缺点及其适用场景&#xff0c;并探讨了在云计算环境中的应用和挑战。原文: Cloud-Native Architecture Patterns (Part 1)&#xff0c;Cloud-Native Architecture Patterns (Part 2) Bernard Hermant Uns…

【产品经理】总篇章

引言: 在最近频繁的产品职位面试中&#xff0c;我深刻体会到了作为产品需要的不仅仅是对市场和技术的敏锐洞察&#xff0c;更多的是在复杂多变的环境中&#xff0c;如何运用沟通、领导力和决策能力来引导产品从概念走向市场。这一系列博客将分享我多年经历和所学到的所以知识&a…

java —— 集合

一、集合的概念 集合可以看做是一个存储对象的容器&#xff0c;与数组不同的是集合可以存储不同类型的对象&#xff0c;但开发中一般不这样做。集合不能存储基本类型的对象&#xff0c;如果存储则需要将其转化为对应的包装类。 二、集合的分类 集合分为 Collection 和 Map 两…

AI技术:探索未来智能的无限可能

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术已经成为我们这个时代最引人瞩目的科技力量。从简单的自动化任务到复杂的决策支持系统&#xff0c;AI技术正在以前所未有的速度改变着我们的世界。本文将深入探讨AI技术的定义、发展历程、当前应用、面临…

JavaScript第四讲:函数,作用域,运算符

前言 在JavaScript的广阔天地中&#xff0c;函数、作用域、算术运算符和逻辑运算符是构成代码世界的基石。它们各自扮演着不同的角色&#xff0c;却又紧密相连&#xff0c;共同编织出丰富多彩的程序逻辑。无论是编写一个简单的网页交互&#xff0c;还是构建一个复杂的应用程序…

React-表单受控绑定

概念&#xff1a;使用React组件的状态&#xff08;useState&#xff09;控制表单的状态 1.准备一个React状态值 2.通过value属性绑定状态&#xff0c;通过onChange属性绑定状态同步的函数

js或者es6 已知id为222的对象数据,如何查询并返回该数据中parentId: 7对应的对象父节点数据的对象

要查询并返回id为222的对象数据中parentId为7对应的对象的父节点数据&#xff0c;我们需要遍历整个data数组&#xff0c;找到id为222的对象&#xff0c;并从其父节点中提取信息。由于您提供的data数组中的对象格式存在问题&#xff08;例如&#xff0c;对象的键值对应该用花括号…

2024年5月总结及随笔之快乐五一

1. 回头看 日更坚持了517天。 读《天才与算法&#xff1a;人脑与AI的数学思维》更新完成 2023年至2024年5月底累计码字1177253字&#xff0c;累计日均码字2277字。 2024年5月码字95875字&#xff0c;同比增长66.7%&#xff0c;环比增长9.3%&#xff0c;日均码字数3092字&am…

jenkins快速入门

Jenkins 是啥&#xff1f; Jenkins 是一个超级能干的自动化助手&#xff0c;它的主要任务是帮你自动构建项目、测试代码、部署应用等等&#xff0c;就像你告诉它&#xff1a;“嘿&#xff0c;Jenkins&#xff0c;我改了代码&#xff0c;你帮我看看能不能正常运行&#xff0c;没…

STM32 定时器与PWM的LED控制

学习目标&#xff1a; 1. 使用定时器的某一个通道控制LED周期性亮灭&#xff1b; 2. 采用定时器PWM模式&#xff0c;让 LED 以呼吸灯方式渐亮渐灭。 一、定时器 1、STM32定时器介绍 STMicroelectronics是STM32微控制器中的重要块&#xff0c;具有丰富的外设和功能&#xff0…

Dijkstra求最短路篇一(全网最详细讲解两种方法,适合小白)(python,其他语言也适用)

前言&#xff1a; Dijkstra算法博客讲解分为两篇讲解&#xff0c;这两篇博客对所有有难点的问题都会讲解&#xff0c;小白也能很好理解。看完这两篇博客后保证收获满满。 本篇博客讲解朴素Dijkstra算法&#xff0c;第二篇博客讲解堆优化Dijkstra算法Dijkstra求最短路篇二(全网…

从C++示例理解开闭原则

开闭原则要求我们在编写代码时&#xff0c;尽量不去修改原先的代码&#xff0c;当出现新的业务需求时&#xff0c;应该通过增加新代码的形式扩展业务而不是对原代码进行修改。 假如我们现在有一批产品&#xff0c;每个产品都具有颜色和大小&#xff0c;产品其定义如下&#xf…

父子进程概述

父子进程概述 总结了两篇博客&#xff0c;对父子进程涉及的问题进行了简要总结&#xff08;参考博客在文章末尾&#xff09; 创建进程的目的一般有两个&#xff1a; 一是父进程希望生成一份自己的副本&#xff0c;执行同一个程序中不同的代码片段。二是让子进程执行不同的程序…

python with 和 上下文管理器

with with操作写法简单又安全 文件操作使用with会自动调用关闭文件操作&#xff0c;即使出现异常也会自动调用文件关闭操作 上下文管理器 with语句强大的根本是由上下文管理器支持的 通过open打开的的文件&#xff0c;赋值给的一个变量file&#xff0c;file就是文件对象&am…

linux docker常用命令记录

一、防火墙 1. 开启防火墙 systemctl start firewalld 2.查看防火墙状态 systemctl status firewalld 二、docker 1.启动docker systemctl start docker 2.关闭docker systemctl stop docker 3.重启docker systemctl restart docker4.查看docker 运行状态 systemc…

Kotlin 函数

文章目录 函数的定义函数的返回值参数默认值 & 调用时参数指定函数作用域Lambda 表达式匿名函数内联函数扩展函数中缀函数递归函数 & 尾递归函数 函数的定义 函数可以理解成一个小小的加工厂&#xff0c;给入特定的原材料&#xff0c;它就会给出特定的产品。 fun [接…

知识运维概述

文章目录 知识运维研究现状技术发展趋势 知识运维 由于构建全量的行业知识图谱成本很高&#xff0c;在真实的场景落地过程中&#xff0c;一般遵循小步快走、快速迭代的原则进行知识图谱的构建和逐步演化。知识运维是指在知识图谱初次构建完成之后&#xff0c;根据用户的使用反馈…

小白跟做江科大32单片机之对射式红外传感器计次

原理部分 1中断示意图&#xff0c;中断会打断主函数的执行&#xff0c;终端执行完成之后再返回主函数继续执行 2.STM32中断 这些灰色的是内核中断 这些白色的是普通中断 3.NVIC统一管理中断&#xff0c;每个中断通道都拥有16个可编程的优先等级&#xff0c;可对优先级进行分组…

独孤思维:10个T的赚钱资料,要不要

01 今天有一个通过网站引流过来的粉丝。 问我&#xff0c;为啥网站不设置付费&#xff0c;这样直接转化成网站vip。 我说&#xff0c;我想把用户沉淀到私域。 其实这个问题&#xff0c;独孤在早年做网站的时候也思考过。 前端给资料&#xff0c;是为了后端引流加个人号&am…

java-this关键字

Java 中的 this 关键字是一个特殊的引用&#xff0c;它代表当前对象。在 Java 中&#xff0c;this 关键字可以在类的构造函数、方法、块和初始化语句中使用。this 关键字的主要作用是&#xff1a; 1. 引用当前对象的属性&#xff08;Field&#xff09;&#xff1a;使用 this 关…