Unity 自定义房间布局系统 设计与实现一个灵活的房间放置系统 ——物体占用的区域及放置点自动化

放置物体功能

效果:

在这里插入图片描述
在这里插入图片描述

功能:

  • 自定义物体占用区域的大小
  • 一键调整占用区域
  • 调整旋转度数,分四个挡位:
    • NoRotation:该物体不能调整旋转。
    • MaximumAngle:每次转动90°。
    • NormalAngle:每次转动45°,效果最好。
    • MinimumAngle:每次转动22.5°,很细的调整,如果想要转动到某一个想要的角度比较花时间,但也不是不行。
  • 禁止垂直旋转(比如花盆,只能进行水平旋转,放置在地上时不能倒立在地上对吧~)
  • 当物体放置到区域内可自动调整吸附点(六个方位使用的吸附点不同,保证不会出穿模)
  • 单独设置物体的吸附点(比如凳子,可以调整为只能吸附在下方,不能放置在墙上或者天花板)
  • 可单独管理一个物体或者管理一组物体,互不影响
  • 直观可调整的视觉效果

文章中RoomReferenceFrame 的具体功能参考自定义房间区域功能


核心功能——MultiMeshAreaCalculator

计算和处理多个网格的区域,用于房间系统的几何管理

我们要在编辑器模式下存储这个物体占用区域的数据,而且至少要保存一个区域数据

  • 得到Renderer的bounds
  • 获得bounds的区域并存储
  • 后续手动调整区域大小,包括旋转矩阵变换
    在这里插入图片描述
初始化并保存区域数据

如果构造函数的AUTO 为True,就是自动调整区域数据,renderer的bounds数据是以世界坐标得到的,无论当前物体的旋转是什么,我们都要变为默认旋转再保存数据,然后再把旋转重置为最开始的样子,既保存了标准数据又不影响物体数据🤪

 [System.Serializable]public class AreaData{public Vector3[] corners = new Vector3[8];// 构造函数,根据 overallBounds 初始化八个角的位置public AreaData(Renderer renderer , bool AUTO = true){Quaternion _Rotation = Quaternion.identity;if (AUTO){_Rotation = renderer.transform.rotation;renderer.transform.rotation = Quaternion.identity;}Vector3 boundsMin = renderer.bounds.min;Vector3 boundsMax = renderer.bounds.max;// 计算八个角的位置信息corners[0] = new Vector3(boundsMin.x , boundsMin.y , boundsMin.z);corners[1] = new Vector3(boundsMin.x , boundsMin.y , boundsMax.z);corners[2] = new Vector3(boundsMin.x , boundsMax.y , boundsMin.z);corners[3] = new Vector3(boundsMin.x , boundsMax.y , boundsMax.z);corners[4] = new Vector3(boundsMax.x , boundsMin.y , boundsMin.z);corners[5] = new Vector3(boundsMax.x , boundsMin.y , boundsMax.z);corners[6] = new Vector3(boundsMax.x , boundsMax.y , boundsMin.z);corners[7] = new Vector3(boundsMax.x , boundsMax.y , boundsMax.z);if (AUTO)renderer.transform.rotation = _Rotation;}a}
旋转或缩放时要进行矩阵变换,使用一个数据来保存偏移量并在构造时更新数据,当物体移动或旋转时调用UpdateAreaData
[HideInInspector]
public Vector3[] cornersInverseTransformPoint = new Vector3[8];
[HideInInspector]
public Transform location;
[HideInInspector]
public Renderer renderer;public AreaData(Renderer renderer , bool AUTO = true)
{//省略this.renderer = renderer;this.location = renderer.transform;for (int i = 0; i < cornersInverseTransformPoint.Length; i++)cornersInverseTransformPoint[i] = location.InverseTransformPoint(corners[i]);UpdateAreaData();
}
public void UpdateAreaData()
{if (renderer == null)return;// 获取对象的旋转和缩放Quaternion rotation = location.rotation;Vector3 scale = location.lossyScale;  // 使用 lossyScale 获取物体在世界空间的缩放for (int i = 0; i < corners.Length; i++){// 对角度偏移应用对象的缩放、旋转和位置Vector3 scaledOffset = Vector3.Scale(cornersInverseTransformPoint[i] , scale);  // 应用世界空间的缩放corners[i] = ( location.position + rotation * scaledOffset );}
}
OK了,我们只需要在MultiMeshAreaCalculator中保存数据,在需要的时候赋值就行了
[HideInInspector]
public List<AreaData> ListOverallBounds = new List<AreaData>();
[HideInInspector]
public Renderer[] renderers;//渲染器数组。表示当前子物体有多少物体可以占用空间public void AutoFindChildData(bool AUTO = true)
{renderers = GetComponentsInChildren<Renderer>();UpdateChildAreaData(AUTO);UpdateData();
}
void UpdateChildAreaData(bool AUTO = true)
{if (renderers.Length > 0){ListOverallBounds.Clear();foreach (Renderer renderer in renderers){if (renderer.GetComponentInParent<MultiMeshAreaCalculator>() == this)ListOverallBounds.Add(new AreaData(renderer , AUTO));}}
}
void UpdateData()
{foreach (var item in ListOverallBounds){item.UpdateAreaData();}
}

有了区域,我们要找到区域的最边缘的六个点(相对于某个坐标系的上下左右前后)当作放置点,保证放置时不会穿模

UpdateAreaData() 方法更新当前 MultiMeshAreaCalculator 实例(以及递归更新所有子 MultiMeshAreaCalculator 实例)的区域数据。这主要是通过更新各个 AreaData 实例的数据来实现的,包括根据当前的物体变换(位置、旋转、缩放)重新计算对象的边界框角点位置。聚合了所有子对象边界点之后,调用 UpdateBoundary 方法更新整个系统的边界信息。使用所有子对象的边界点计算一个总的边界框。
在这里插入图片描述

这样就实现了无论是放置一个或者是一组物体都不影响边缘点的更新
[HideInInspector]
public RoomReferenceFrame roomReferenceFrame;//房间参考框架
[HideInInspector]
public MultiMeshAreaCalculator[] childData = null;//子区域数据。子物体是否包含此组件
[HideInInspector]
public bool IsMainArea = false;//是否为主区域。如果父物体有此组件,那么当前组件将不参与管理。
private Vector3 front, back, right, left, top, bottom, center, lastLocation = Vector3.zero, lastDir = Vector3.zero;void Initialize()
{IsMainArea = GetComponentsInParent<MultiMeshAreaCalculator>().Length < 2;if (IsMainArea){childData = GetComponentsInChildren<MultiMeshAreaCalculator>();childData = System.Array.FindAll(childData , r => r.gameObject != gameObject);}
}void UpdateAreaData()
{List<Vector3> centerList = new List<Vector3>();foreach (var multiMeshAreaCalculator in GetComponentsInChildren<MultiMeshAreaCalculator>()){foreach (var item in multiMeshAreaCalculator.ListOverallBounds){centerList.AddRange(item.GetCorners());}}if (centerList.Count > 0&& roomReferenceFrame)UpdateBoundary(roomReferenceFrame.transform , centerList.ToArray());
}
void UpdateBoundary(Transform angle , Vector3[] corners)
{// 初始化累加器Vector3 sum = Vector3.zero;// 遍历 corners 数组foreach (Vector3 corner in corners){// 将每个角落点的坐标添加到累加器sum += corner;}// 计算平均值,即 corners 数组的中心点Vector3 center = sum / corners.Length;// 获取 angle 的正前方方向Vector3 forwardDirection = angle.forward;Vector3 rightDirection = angle.right;Vector3 upDirection = angle.up;Vector3 front = Vector3.zero;Vector3 back = Vector3.zero;// 初始化最远和最近的投影点Vector3 right = Vector3.zero;Vector3 left = Vector3.zero;Vector3 top = Vector3.zero;Vector3 bottom = Vector3.zero;// 初始化投影长度float maxProjectionLengthFront = float.MinValue;float minProjectionLengthFront = float.MaxValue;float maxProjectionLengthRight = float.MinValue;float minProjectionLengthRight = float.MaxValue;float maxProjectionLengthUp = float.MinValue;float minProjectionLengthUp = float.MaxValue;// 遍历 corners 数组foreach (Vector3 corner in corners){// 将角落点投影到 angle.right 方向线上Vector3 projectedFrontPoint = ProjectPointOntoLine(center , forwardDirection , corner);float projectionLengthFront = Vector3.Dot(projectedFrontPoint - center , forwardDirection);// 更新最远和最近的右投影点if (projectionLengthFront > maxProjectionLengthFront){maxProjectionLengthFront = projectionLengthFront;front = projectedFrontPoint;}if (projectionLengthFront < minProjectionLengthFront){minProjectionLengthFront = projectionLengthFront;back = projectedFrontPoint;}// 将角落点投影到 angle.right 方向线上Vector3 projectedRightPoint = ProjectPointOntoLine(center , rightDirection , corner);float projectionLengthRight = Vector3.Dot(projectedRightPoint - center , rightDirection);// 更新最远和最近的右投影点if (projectionLengthRight > maxProjectionLengthRight){maxProjectionLengthRight = projectionLengthRight;right = projectedRightPoint;}if (projectionLengthRight < minProjectionLengthRight){minProjectionLengthRight = projectionLengthRight;left = projectedRightPoint;}// 将角落点投影到 angle.up 方向线上Vector3 projectedUpPoint = ProjectPointOntoLine(center , upDirection , corner);float projectionLengthUp = Vector3.Dot(projectedUpPoint - center , upDirection);// 更新最远和最近的上投影点if (projectionLengthUp > maxProjectionLengthUp){maxProjectionLengthUp = projectionLengthUp;top = projectedUpPoint;}if (projectionLengthUp < minProjectionLengthUp){minProjectionLengthUp = projectionLengthUp;bottom = projectedUpPoint;}}SetLocalCoordinates(front , back , right , left , top , bottom , center);
}void SetLocalCoordinates(Vector3 front , Vector3 back , Vector3 right , Vector3 left , Vector3 top , Vector3 bottom , Vector3 center)
{this.front = front;this.back = back;this.right = right;this.left = left;this.top = top;this.bottom = bottom;this.center = center;
}
Vector3 ProjectPointOntoLine(Vector3 origin , Vector3 direction , Vector3 point)
{// 计算方向向量的归一化向量Vector3 normalizedDirection = direction.normalized;// 计算点与线起点之间的向量Vector3 toPoint = point - origin;// 计算点在方向上的投影长度float projectionLength = Vector3.Dot(toPoint , normalizedDirection);// 计算投影点的位置return origin + projectionLength * normalizedDirection;
}

接下来我们要存储此物体可被使用的方向的功能,此功能是要在编辑器模式下设定并保存,我准备存到字典中,但是字典无法被序列化,只能自己写一个序列化字典的功能。

  • 将字典数据存储到字符串中
  • 序列化这个字符串
  • 使用字典时读取字符串数据并转化为字典
    在这里插入图片描述

🤨可真是个好主意!

定义基本属性

默认状态下,物体的六个方向都是允许被使用的

[HideInInspector]
public string UsableDirectionJson = "";//可用方向的JSON字符串。主要用于序列化字典
public Dictionary<SnapDirection , bool> UsableDirection = new Dictionary<SnapDirection , bool>{{ SnapDirection.Top , true } ,{ SnapDirection.Bottom , true } ,{ SnapDirection.Front , true } ,{ SnapDirection.Back , true } ,{ SnapDirection.Left , true } ,{ SnapDirection.Right , true }};
序列化字典
[System.Serializable]
public class SerializableDictionary<TKey, TValue>
{public List<TKey> keys = new List<TKey>();public List<TValue> values = new List<TValue>();public SerializableDictionary(Dictionary<TKey , TValue> dict){foreach (var kvp in dict){keys.Add(kvp.Key);values.Add(kvp.Value);}}public Dictionary<TKey , TValue> ToDictionary(){Dictionary<TKey , TValue> dict = new Dictionary<TKey , TValue>();for (int i = 0; i < keys.Count; i++){dict.Add(keys[i] , values[i]);}return dict;}
}
使用这个功能
字典序列化为JSON
UsableDirectionJson = JsonUtility.ToJson(new SerializableDictionary<SnapDirection , bool>(UsableDirection));JSON转化为字典
SerializableDictionary<SnapDirection , bool> serializedDict = JsonUtility.FromJson<SerializableDictionary<SnapDirection , bool>>(UsableDirectionJson);
if (serializedDict != null)UsableDirection = serializedDict.ToDictionary();

有了区域数据、边缘数据,有了可使用的方向,我们还缺少放置物体的功能

根据位置和方向向量,结合当前可用的吸附方向,计算并更新位置。
  • 计算目标方向以此推断出要使用哪个方向的吸附点
  • 检查可用方向
  • 定位和吸附
    在这里插入图片描述
[HideInInspector]
public SnapDirection currentSnap = SnapDirection.None;//当前吸附方向。
private Vector3 front, back, right, left, top, bottom, center, lastLocation = Vector3.zero, lastDir = Vector3.zero;
public void SetLocation(Vector3 location , Vector3 dir)
{lastLocation = location;lastDir = dir;UpdateData();Vector3 Dir = CalculateDirection(dir);if (!CheckUsableDirection())return;location = roomReferenceFrame.SnapToGrid(location , currentSnap);transform.position = location + ( transform.position - Dir );
}
public enum SnapDirection
{None,Top,Bottom,Front,Back,Left,Right
}
CalculateDirection - 利用传入的方向向量,CalculateDirection确定对象应当吸附的方向(前、后、左、右、上、下之一)。它通过计算传入向量与每个预设方向之间的余弦相似度,选出相似度最高的方向作为目标方向,并更新currentSnap至该方向。
Vector3 CalculateDirection(Vector3 dir)
{// 使用 CalculateCosineSimilarity 方法计算 dir 与每个方向向量之间的余弦相似度float similarityFront = CalculateCosineSimilarity(front - center , dir);float similarityBack = CalculateCosineSimilarity(back - center , dir);float similarityRight = CalculateCosineSimilarity(right - center , dir);float similarityLeft = CalculateCosineSimilarity(left - center , dir);float similarityTop = CalculateCosineSimilarity(top - center , dir);float similarityBottom = CalculateCosineSimilarity(bottom - center , dir);// 初始化最高相似度和目标方向float maxSimilarity = similarityFront;Vector3 targetDirection = back;currentSnap = SnapDirection.Back;// 找到与 dir 最相似的方向if (similarityBack > maxSimilarity){maxSimilarity = similarityBack;targetDirection = front;currentSnap = SnapDirection.Front;}if (similarityRight > maxSimilarity){maxSimilarity = similarityRight;targetDirection = left;currentSnap = SnapDirection.Left;}if (similarityLeft > maxSimilarity){maxSimilarity = similarityLeft;targetDirection = right;currentSnap = SnapDirection.Right;}if (similarityTop > maxSimilarity){maxSimilarity = similarityTop;targetDirection = bottom;currentSnap = SnapDirection.Bottom;}if (similarityBottom > maxSimilarity){maxSimilarity = similarityBottom;targetDirection = top;currentSnap = SnapDirection.Top;}return targetDirection;
}
float CalculateCosineSimilarity(Vector3 vectorA , Vector3 vectorB)
{float dotProduct = Vector3.Dot(vectorA , vectorB); // 计算两个向量的点积float magnitudeA = vectorA.magnitude; // 计算向量 A 的欧几里德范数(长度)float magnitudeB = vectorB.magnitude; // 计算向量 B 的欧几里德范数(长度)// 计算余弦相似度float cosineSimilarity = dotProduct / ( magnitudeA * magnitudeB );return cosineSimilarity;
}
CheckUsableDirection - 确定当前的吸附方向(currentSnap)是否在UsableDirection字典中标记为true(即可用)。
bool CheckUsableDirection()
{bool IsUsable = false;foreach (KeyValuePair<SnapDirection , bool> pair in UsableDirection){if (pair.Key == currentSnap && pair.Value){return true;}}return IsUsable;
}

接下来是旋转,要保证每次旋转都会重新更新边缘位置并重新吸附在正确的位置

思考:

一个物体有三个旋转轴,操作者应该如何高效的旋转一个物体?
想实现将物体旋转到任意角度的话就要控制三个轴,如何既实现这个功能而且把操作简化?

这让我想起了一个名字叫“天”的游戏:塞尔达传说-王国之泪。

游戏里使用究极手来操作物体旋转,可以说是想旋转到什么角度就能旋转到什么角度,而且使用两个轴就可以了。
我决定复刻究极手的旋转功能!
在这里插入图片描述

通过枚举RotateDirection接受传入的旋转指令,然后基于预设的旋转角度更新对象的旋转状态。完成旋转后,调用SetLocation设置对象位置。
public enum RotateDirection
{Reset,Top,Bottom,Left,Right
}public bool DisableYAxisRotation = false;//禁用垂直旋转。public void SetRotate(RotateDirection rotateDirection)
{if (rotateDirection == RotateDirection.Reset)transform.rotation = roomReferenceFrame.transform.rotation;else{if (DisableYAxisRotation && ( rotateDirection == RotateDirection.Top || rotateDirection == RotateDirection.Bottom ) || rotationAngle == RotationAngle.NoRotation)return;int rotateInterval = 4;switch (rotationAngle){case RotationAngle.MaximumAngle:rotateInterval *= 1;break;case RotationAngle.NormalAngle:rotateInterval *= 2;break;case RotationAngle.MinimumAngle:rotateInterval *= 4;break;}// 计算旋转中心点Vector3 pivot = roomReferenceFrame.transform.position;//根据这个坐标轴,如果是VR模式就是手柄或者人物的坐标轴// 根据不同的方向进行旋转switch (rotateDirection){case RotateDirection.Top:transform.RotateAround(pivot , roomReferenceFrame.transform.right , -360 / rotateInterval);break;case RotateDirection.Bottom:transform.RotateAround(pivot , roomReferenceFrame.transform.right , 360 / rotateInterval);break;case RotateDirection.Left:transform.RotateAround(pivot , roomReferenceFrame.transform.up , -360 / rotateInterval);break;case RotateDirection.Right:transform.RotateAround(pivot , roomReferenceFrame.transform.up , 360 / rotateInterval);break;}SetLocation(lastLocation , lastDir);}
}
这样是可以实现旋转,但是由于旋转的轴是自身的,而定位的位置却不是自身的,看起来会卡一下,稍微优化一下

在旋转前隐藏自己,在下一帧显示,这样就没问题了,这个解决方法有点奇怪🤔,以后再改进吧。
在这里插入图片描述

public void SetRotate(RotateDirection rotateDirection)
{if (rotateDirection == RotateDirection.Reset)transform.rotation = roomReferenceFrame.transform.rotation;else{// 隐藏物体foreach (var item in GetItemData()){item.renderer.enabled = false;}StartCoroutine(DelaySetLocation());}
}
IEnumerator DelaySetLocation()
{// 等待直到下一帧yield return null;// 在下一帧执行SetLocation,并恢复显示SetLocation(lastLocation , lastDir);foreach (var item in GetItemData()){item.renderer.enabled = true;}
}
public List<AreaData> GetItemData()
{List<AreaData> AreaDates = new List<AreaData>();if (ListOverallBounds.Count != 0){AreaDates.AddRange(ListOverallBounds);}if (childData != null){foreach (var item in childData){if (item.ListOverallBounds.Count != 0){AreaDates.AddRange(item.ListOverallBounds);}}}return AreaDates;
}

基本功能已经完成了,目前已经实现基本需求,不过到应用还差很多


视觉效果——网格显示

根据数据画线就行了
在这里插入图片描述

private void OnDrawGizmosSelected()
{if (renderers == null)return;UpdateData();if (IsMainArea)DrawCenter();foreach (var item in ListOverallBounds){DrawChildArea(item.GetCorners());}
}
void DrawCenter()//绘制哪个方向可以被放置
{Gizmos.color = adsorptionLocation;//绘制吸附点边框resolution = resolution < 1 ? 1 : resolution;if (UsableDirection[SnapDirection.Front])DrawDirPeripheral(front);if (UsableDirection[SnapDirection.Back])DrawDirPeripheral(back);if (UsableDirection[SnapDirection.Right])DrawDirPeripheral(right);if (UsableDirection[SnapDirection.Left])DrawDirPeripheral(left);if (UsableDirection[SnapDirection.Top])DrawDirPeripheral(top);if (UsableDirection[SnapDirection.Bottom])DrawDirPeripheral(bottom);
}
void DrawDirPeripheral(Vector3 dir)
{Gizmos.DrawLine(dir , center);Gizmos.DrawSphere(dir , 0.02f);DrawPeripheralPoint(dir , dir - center , boundarySize , resolution);
}
void DrawPeripheralPoint(Vector3 origin , Vector3 normal , float radius , int resolution)
{normal.Normalize();Vector3 reference;reference = Mathf.Abs(Vector3.Dot(normal , Vector3.up)) > 0.999f ? Vector3.forward : Vector3.up;Vector3 right = Vector3.Cross(normal , reference).normalized;Vector3 up = Vector3.Cross(right , normal).normalized;Vector3[] circleVertices = new Vector3[resolution];// 修改角度计算方式float angleIncrement = 360f / resolution;for (int i = 0; i < resolution; i++){float angle = i * angleIncrement * Mathf.Deg2Rad; // 弧度制float x = Mathf.Cos(angle) * radius;float y = Mathf.Sin(angle) * radius;circleVertices[i] = origin + right * x + up * y;}// 绘制连接顶点的线来组成圆for (int i = 0; i < resolution; i++){int nextIndex = ( i + 1 ) % resolution;Gizmos.DrawLine(circleVertices[i] , circleVertices[nextIndex]);}
}void DrawChildArea(Vector3[] corners)//绘制子区域
{if (corners.Length != 8)return;// 设置绘制颜色(可根据需求调整颜色)Gizmos.color = areaColor;// 绘制前面正方形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]);
}

视觉效果——Editor_MultiMeshAreaCalculator

Editor_MultiMeshAreaCalculator 是一个自定义编辑器类,用于在 Unity 编辑器中扩展 MultiMeshAreaCalculator 组件的功能,使其更易于在场景中调整和可视化。

场景视图中绘制控制柄

直观的可视化工具,使用户可以在场景视图中通过拖动控制柄来调整区域的大小和位置。
在这里插入图片描述

private void OnSceneGUI()
{if (multiMeshAreaCalculator.useHandle){Color color = multiMeshAreaCalculator.areaColor;color.a = 0.5f;Handles.color = color;Handles.CapFunction capFunction = Handles.ConeHandleCap;for (int i = 0; i < multiMeshAreaCalculator.ListOverallBounds.Count; i++){AreaData areaDate = multiMeshAreaCalculator.ListOverallBounds[i];Vector3[] faceCenters = areaDate.GetFaceCenters();for (int j = 0; j < faceCenters.Length; j++){Vector3 SliderOffset = Vector3.Normalize(faceCenters[j] - areaDate.GetCorner()) * (multiMeshAreaCalculator.handleSize / 2);Vector3 newFaceCenter = Handles.Slider(faceCenters[j] + SliderOffset, faceCenters[j] - areaDate.GetCorner(), multiMeshAreaCalculator.handleSize, capFunction, 1f) - SliderOffset;if (newFaceCenter != faceCenters[j]){Vector3 offset = newFaceCenter - faceCenters[j];UpdateCorners(areaDate, j, offset);}}}}
}
void UpdateCorners(AreaData areaDate, int faceIndex, Vector3 offset)
{Vector3[] corners = areaDate.GetCorners();switch (faceIndex){case 0: // 前面corners[0] += offset;corners[1] += offset;corners[2] += offset;corners[3] += offset;break;case 1: // 后面corners[4] += offset;corners[5] += offset;corners[6] += offset;corners[7] += offset;break;case 2: // 左面corners[0] += offset;corners[2] += offset;corners[4] += offset;corners[6] += offset;break;case 3: // 右面corners[1] += offset;corners[3] += offset;corners[5] += offset;corners[7] += offset;break;case 4: // 顶面corners[2] += offset;corners[3] += offset;corners[6] += offset;corners[7] += offset;break;case 5: // 底面corners[0] += offset;corners[1] += offset;corners[4] += offset;corners[5] += offset;break;}areaDate.SetCorners(corners);
}

AreaData类要添加对应的功能

得到点位,设置点位等功能

public void SetCorners(Vector3[] temp)
{if (temp.Length != cornersInverseTransformPoint.Length){Debug.LogError("新的角点数组长度必须与原始角点数组长度相同!");return;}Vector3[] newCornersInverseTransformPoint = new Vector3[temp.Length];// 获取对象的旋转的逆矩阵Quaternion rotationInverse = Quaternion.Inverse(location.rotation);Vector3 scale = location.lossyScale;for (int i = 0; i < temp.Length; i++){// 对应角点位置的偏移Vector3 offsetAdjusted = temp[i] + offset - location.position;// 逆向应用对象的缩放、旋转和位置Vector3 scaledOffset = new Vector3(offsetAdjusted.x / scale.x , offsetAdjusted.y / scale.y , offsetAdjusted.z / scale.z);newCornersInverseTransformPoint[i] = rotationInverse * scaledOffset;}cornersInverseTransformPoint = newCornersInverseTransformPoint;UpdateAreaData();
}
/// <summary>
/// 得到六个面的中心点
/// </summary>
/// <returns></returns>public Vector3[] GetCorners()
{return corners;
}
public Vector3 GetCorner()
{Vector3 Corner = Vector3.zero;foreach (var item in GetCorners()){Corner += item;}return Corner/ GetCorners().Length;
}
public Vector3[] GetFaceCenters()
{Vector3[] faceCenters = new Vector3[6];faceCenters[0] = ( corners[0] + corners[1] + corners[2] + corners[3] ) / 4;faceCenters[1] = ( corners[4] + corners[5] + corners[6] + corners[7] ) / 4;faceCenters[2] = ( corners[0] + corners[2] + corners[4] + corners[6] ) / 4;faceCenters[3] = ( corners[1] + corners[3] + corners[5] + corners[7] ) / 4;faceCenters[4] = ( corners[2] + corners[3] + corners[6] + corners[7] ) / 4;faceCenters[5] = ( corners[0] + corners[1] + corners[4] + corners[5] ) / 4;return faceCenters;
}

美化面板

  • 自动查找子物体功能: 通过按钮触发,自动查找并保存子物体的渲染器数据。
  • 手动调整区域功能: 提供按钮和滑块来调整区域的尺寸和位置。
  • 视觉效果调整: 提供调整颜色、吸附点分辨率和尺寸的选项。
  • 旋转控制: 允许用户设置旋转角度和锁定 Y 轴旋转。
  • 吸附方向设置: 提供界面来设置和保存吸附方向。
  • 区域重置和测试功能: 提供按钮来重置区域和添加/移除测试区域。
    在这里插入图片描述
public override void OnInspectorGUI()
{GUILayout.Space(5);if (GUILayout.Button(new GUIContent("AUTO", "寻找子物体的Render并保存边框数据\n自动情况下的原理是将Render物体的旋转重置,保存数据后恢复旋转"), GUILayout.Width(255), GUILayout.Height(50))){multiMeshAreaCalculator.AutoFindChildData();}GUILayout.Space(5);EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(260));GUILayout.BeginHorizontal();string btnName = multiMeshAreaCalculator.useHandle ? "关闭" : "手动调整占用区域";if (GUILayout.Button(new GUIContent(btnName, "点击打开调整面板"), GUILayout.Width(255), GUILayout.Height(25))){multiMeshAreaCalculator.useHandle = !multiMeshAreaCalculator.useHandle;}GUILayout.EndHorizontal();if (multiMeshAreaCalculator.useHandle){GUILayout.BeginHorizontal();GUILayout.Label("手柄尺寸", GUILayout.Width(75));multiMeshAreaCalculator.handleSize = EditorGUILayout.Slider(multiMeshAreaCalculator.handleSize, 0, 1, GUILayout.Width(150));GUILayout.EndHorizontal();if (GUILayout.Button(new GUIContent("与世界坐标对齐", "寻找子物体的Render并保存边框数据\n自动调整对不齐的情况使用,调整Render的边框直至到合适的边框大小"), GUILayout.Width(250))){multiMeshAreaCalculator.AutoFindChildData(false);}}EditorGUILayout.EndVertical();GUILayout.Space(15);EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(260));if (ChangeControl){if (GUILayout.Button("隐藏", "prebutton"))ChangeControl = !ChangeControl;}else{if (GUILayout.Button("调整视觉效果", "prebutton"))ChangeControl = !ChangeControl;}if (ChangeControl){GUILayout.Space(10);GUILayout.BeginHorizontal();GUILayout.Label("吸附位置", GUILayout.Width(55));multiMeshAreaCalculator.adsorptionLocation = EditorGUILayout.ColorField(multiMeshAreaCalculator.adsorptionLocation, GUILayout.Width(50));GUILayout.Space(10);GUILayout.Label("占用区域", GUILayout.Width(55));multiMeshAreaCalculator.areaColor = EditorGUILayout.ColorField(multiMeshAreaCalculator.areaColor, GUILayout.Width(50));GUILayout.EndHorizontal();GUILayout.Space(10);GUILayout.BeginHorizontal();GUILayout.Label("吸附点分辨率", GUILayout.Width(75));multiMeshAreaCalculator.resolution = EditorGUILayout.IntSlider(multiMeshAreaCalculator.resolution, 0, 15, GUILayout.Width(150));GUILayout.EndHorizontal();GUILayout.BeginHorizontal();GUILayout.Label("吸附点尺寸", GUILayout.Width(75));multiMeshAreaCalculator.boundarySize = EditorGUILayout.Slider(multiMeshAreaCalculator.boundarySize, 0, 1, GUILayout.Width(150));GUILayout.EndHorizontal();}EditorGUILayout.EndVertical();GUILayout.Space(10);GUILayout.BeginHorizontal();GUILayout.Label(new GUIContent("旋转调节", "分为四个挡位:" +"\nNoRotation:该物体不能调整旋转。" +"\nMaximumAngle:每次转动90°。" +"\nNormalAngle:每次转动45°,效果最好。" +"\nMinimumAngle:每次转动22.5°,很细的调整,如果想要转动到某一个想要的角度比较花时间,但也不是不行。"), GUILayout.Width(55));multiMeshAreaCalculator.rotationAngle = (RotationAngle)EditorGUILayout.EnumPopup(multiMeshAreaCalculator.rotationAngle, GUILayout.Width(100));GUILayout.Label("   锁定Y轴", GUILayout.Width(60));multiMeshAreaCalculator.DisableYAxisRotation = GUILayout.Toggle(multiMeshAreaCalculator.DisableYAxisRotation, "", GUILayout.Width(70));GUILayout.EndHorizontal();GUILayout.Space(10);EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(260));SerializableDictionary<SnapDirection, bool> serializedDict = JsonUtility.FromJson<SerializableDictionary<SnapDirection, bool>>(multiMeshAreaCalculator.UsableDirectionJson);if (serializedDict != null)multiMeshAreaCalculator.UsableDirection = serializedDict.ToDictionary();GUILayout.BeginHorizontal();GUILayout.Space(100);GUILayout.Label("Top", GUILayout.Width(25));multiMeshAreaCalculator.UsableDirection[SnapDirection.Top] = GUILayout.Toggle(multiMeshAreaCalculator.UsableDirection[SnapDirection.Top], "", GUILayout.Width(70));GUILayout.EndHorizontal();GUILayout.BeginHorizontal();GUILayout.Space(40);GUILayout.Label("Front", GUILayout.Width(35));multiMeshAreaCalculator.UsableDirection[SnapDirection.Front] = GUILayout.Toggle(multiMeshAreaCalculator.UsableDirection[SnapDirection.Front], "", GUILayout.Width(37));GUILayout.Label("|", GUILayout.Width(5));GUILayout.Label("Back", GUILayout.Width(35));multiMeshAreaCalculator.UsableDirection[SnapDirection.Back] = GUILayout.Toggle(multiMeshAreaCalculator.UsableDirection[SnapDirection.Back], "", GUILayout.Width(37));GUILayout.EndHorizontal();GUILayout.BeginHorizontal();GUILayout.Label("Left", GUILayout.Width(40));multiMeshAreaCalculator.UsableDirection[SnapDirection.Left] = GUILayout.Toggle(multiMeshAreaCalculator.UsableDirection[SnapDirection.Left], "", GUILayout.Width(40));GUILayout.Label("|", GUILayout.Width(5));GUILayout.Label("Right", GUILayout.Width(40));multiMeshAreaCalculator.UsableDirection[SnapDirection.Right] = GUILayout.Toggle(multiMeshAreaCalculator.UsableDirection[SnapDirection.Right], "", GUILayout.Width(40));GUILayout.EndHorizontal();GUILayout.BeginHorizontal();GUILayout.Space(100);GUILayout.Label("Bottom", GUILayout.Width(45));multiMeshAreaCalculator.UsableDirection[SnapDirection.Bottom] = GUILayout.Toggle(multiMeshAreaCalculator.UsableDirection[SnapDirection.Bottom], "", GUILayout.Width(70));GUILayout.EndHorizontal();if (GUILayout.Button("保存设置", GUILayout.Width(255))){SerializableDictionary<SnapDirection, bool> serializableDictionary = new SerializableDictionary<SnapDirection, bool>(multiMeshAreaCalculator.UsableDirection);multiMeshAreaCalculator.UsableDirectionJson = JsonUtility.ToJson(serializableDictionary);}EditorGUILayout.EndVertical();GUILayout.Space(20);if (GUILayout.Button("Reset Area", GUILayout.Width(255))){multiMeshAreaCalculator.ResetArea();}GUILayout.Space(20);GUILayout.BeginHorizontal();if (GUILayout.Button("TEST AddArea", GUILayout.Width(125))){multiMeshAreaCalculator.AddArea();}if (GUILayout.Button("TEST RemoveArea", GUILayout.Width(125))){multiMeshAreaCalculator.RemoveArea();}GUILayout.EndHorizontal();GUILayout.Space(20);DrawDefaultInspector();
}

MultiMeshAreaCalculator要添加对应的属性

[HideInInspector]
public TransformChangedEvent transformChangedEvent;//监听变换事件的组件。当物体移动或者旋转时触发
public Color adsorptionLocation = Color.green;//吸附位置颜色。-编辑器扩展使用
public Color areaColor = Color.black;//区域颜色。-编辑器扩展使用
[Range(1 , 36)]
public int resolution = 4;//边缘显示框分辨率。-编辑器扩展使用
public float boundarySize = 0.5f;//边界边缘显示框尺寸。-编辑器扩展使用
public float handleSize = 0.5f;//调整区域控制柄的尺寸。-编辑器扩展使用
public bool useHandle = false;//是否使用控制柄。
public RotationAngle rotationAngle = RotationAngle.NormalAngle;//每次旋转角度。

为什么这么设计?

  • 用户友好性: 提供直观的 GUI 界面,使得用户可以轻松调整和配置 MultiMeshAreaCalculator 组件。
  • 高效开发: 自动化查找和保存子物体的渲染器数据,减少手动操作,提高开发效率。
  • 丰富的功能: 提供多种调整选项,包括手柄调整、视觉效果设置、吸附方向设置等,满足不同的需求。
  • 可维护性强: 代码结构清晰,逻辑分明,便于维护和扩展。
  • 实时反馈: 在场景视图中提供控制柄,使用户能够实时预览调整效果,提升用户体验。

如何使用这个功能

做一个案例来测试

public LayerMask raycastLayers; // 存储要检测的层
public RoomReferenceFrame roomReferenceFrame;
MultiMeshAreaCalculator multiMeshAreaCalculator;
bool activate = false;private void OnValidate()
{roomReferenceFrame=roomReferenceFrame==null ? FindObjectOfType<RoomReferenceFrame>() : roomReferenceFrame;
}void Update()
{// 检查鼠标左键是否被按下if(Input.GetMouseButton(0)){Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);RaycastHit hit;// 使用 LayerMask 来指定要检测的层if(Physics.Raycast(ray , out hit , Mathf.Infinity , raycastLayers)){activate=true;if(multiMeshAreaCalculator!=null){multiMeshAreaCalculator.SetLocation(hit.point , hit.normal);}}else{// 如果没有击中任何物体,则输出射线未命中Debug.Log("射线未命中");}}if(Input.GetKeyDown(KeyCode.A)){if(multiMeshAreaCalculator)multiMeshAreaCalculator.SetRotate(RotateDirection.Left);}if(Input.GetKeyDown(KeyCode.D)){if(multiMeshAreaCalculator)multiMeshAreaCalculator.SetRotate(RotateDirection.Right);}if(Input.GetKeyDown(KeyCode.W)){if(multiMeshAreaCalculator)multiMeshAreaCalculator.SetRotate(RotateDirection.Top);}if(Input.GetKeyDown(KeyCode.S)){if(multiMeshAreaCalculator)multiMeshAreaCalculator.SetRotate(RotateDirection.Bottom);}if(Input.GetKeyDown(KeyCode.Space)){if(multiMeshAreaCalculator&&activate){if(multiMeshAreaCalculator.Place()){multiMeshAreaCalculator=null;activate=false;}}}if(Input.GetKeyDown(KeyCode.Alpha1)){CreateRoomItem("一盆花");}if(Input.GetKeyDown(KeyCode.Alpha2)){CreateRoomItem("凳子");}if(Input.GetKeyDown(KeyCode.Alpha3)){CreateRoomItem("吊灯");}if(Input.GetKeyDown(KeyCode.Alpha4)){CreateRoomItem("微波炉");}if(Input.GetKeyDown(KeyCode.Alpha5)){CreateRoomItem("毛巾");}if(Input.GetKeyDown(KeyCode.Alpha6)){CreateRoomItem("电视");}if(Input.GetKeyDown(KeyCode.Alpha7)){CreateRoomItem("家具组1");}
}void CreateRoomItem(string path)
{if(multiMeshAreaCalculator)return;GameObject item = Resources.Load<GameObject>(path);multiMeshAreaCalculator=Instantiate(item).GetComponent<MultiMeshAreaCalculator>();multiMeshAreaCalculator.SetRotate( RotateDirection.Reset);
}

MultiMeshAreaCalculator添加对应的功能

public bool Place()
{bool IsPlace = roomReferenceFrame.IsOverlapping(this , out List<Renderer> renders);if (IsPlace)roomReferenceFrame.AddRoomItem(this);return IsPlace;
}

嚯!~ 到底了!

量太大了?

没事哥们,慢慢消化

点击下载👉Demo~

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

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

相关文章

根据租户id切换数据源

花了半天时间&#xff0c;使用spring-boot实现动态数据源&#xff0c;切换自如 在一个项目中使用多个数据源的情况很多&#xff0c;所以动态切换数据源是项目中标配的功能&#xff0c;当然网上有相关的依赖可以使用&#xff0c;比如动态数据源&#xff0c;其依赖为&#xff0c;…

探索营销系统业务架构的设计与应用

随着市场竞争的日益激烈和消费者需求的不断变化&#xff0c;营销系统作为企业营销管理的重要组成部分&#xff0c;扮演着至关重要的角色。本文将深入探讨营销系统业务架构的设计与应用&#xff0c;从客户关系管理、营销活动管理、数据分析和智能化服务等方面进行全面解析&#…

Innodb Buffer Pool缓存机制(四)预读与Mysql改进的LRU策略

一、什么是预读 InnoDB提供了预读(read ahead)。所谓预读&#xff0c;就是InnoDB认为执行当前的请求可能之后会读取某些页面&#xff0c;就预先把它们加载到Buffer Pool中。根据触发方式的不同&#xff0c;预读又可以细分为下边两种&#xff1a; 1.1 线性预读 InnoDB提供了一…

掘金AI商战宝典-高阶班:如何用AI制作视频(11节视频课)

课程下载&#xff1a;掘金AI商战宝典-高阶班&#xff1a;如何用AI制作视频(11节视频课)-课程网盘链接提取码下载.txt资源-CSDN文库 更多资源下载&#xff1a;关注我。 课程目录&#xff1a; 1-第一讲用AI自动做视频(上)_1.mp4 2-第二讲用AI自动做视频(中)_1.mp4 3-第四讲A…

阿里云邮件推送服务配置教程:怎么做批发?

阿里云邮件推送的API配置步骤&#xff1f;配置教程有哪些步骤&#xff1f; 阿里云邮件推送服务凭借其高并发、稳定性强和安全性高等特点&#xff0c;成为众多企业的首选。Aok将详细介绍如何使用阿里云邮件推送服务进行批发配置&#xff0c;并简要提及AokSend的优势。 阿里云邮…

UE4_环境_材质函数

学习笔记&#xff0c;不喜勿喷&#xff0c;欢迎指正&#xff0c;侵权立删&#xff01; 1、建立材质函数Distance_Fun&#xff0c;勾选公开到库。 2、添加函数输入节点FunctionInput&#xff0c; 这个输入我们想作为混合材质属性BlendMaterialAttributes的alpha输入节点&#x…

手撸 串口交互命令行 及 AT应用层协议解析框架

在嵌入式系统开发中&#xff0c;命令行接口&#xff08;CLI&#xff09;和AT命令解析是常见的需求。CLI提供了方便的调试接口&#xff0c;而AT命令则常用于模块间的通信控制。本文将介绍如何手动实现一个串口交互的命令行及AT应用层协议解析框架&#xff0c;适用于FreeRTOS系统…

06Docker-Compose和微服务部署

Docker-Compose 概述 Docker Compose通过一个单独的docker-compose.yml模板文件来定义一组相关联的应用容器&#xff0c;帮助我们实现多个相互关联的Docker容器的快速部署 一般一个docker-compose.yml对应完整的项目,项目中的服务和中间件对应不同的容器 Compose文件实质就…

锂电池寿命预测 | Matlab基于SSA-SVR麻雀优化支持向量回归的锂离子电池剩余寿命预测

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 【锂电池剩余寿命RUL预测案例】 锂电池寿命预测 | Matlab基于SSA-SVR麻雀优化支持向量回归的锂离子电池剩余寿命预测&#xff08;完整源码和数据&#xff09; 1、提取NASA数据集的电池容量&#xff0c;以历史容量作…

【C++课程学习】:类和对象(上)(类的基础详细讲解)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 &#x1f35f;1.1类的引出&#xff1a; &#x1f35f;1.2类的结构&#xff1a; &#x1f35f;1.3类的…

LeetCode-82. 删除排序链表中的重复元素 II【链表 双指针】

LeetCode-82. 删除排序链表中的重复元素 II【链表 双指针】 题目描述&#xff1a;解题思路一&#xff1a;用一个cur即可实现去重cur.next cur.next.next背诵版&#xff1a;解题思路三&#xff1a;0 题目描述&#xff1a; 给定一个已排序的链表的头 head &#xff0c; 删除原始…

十大排序-冒泡排序

算法原理如下&#xff1a; 给出一组数据&#xff1b;比较相邻的元素。如果第一个比第二个大&#xff0c;互换两个值。对每一组相邻元素同样方式比较&#xff0c;从开始的第一组到结束的最后一组。最后的元素会是最大数。除了排列好的最大数&#xff0c;针对所有元素重复以上步…

前端应用开发实验:组件应用

目录 实验目的相关知识点实验内容及要求代码实现效果 实验目的 &#xff08;1&#xff09;掌握组件的创建方法&#xff08;全局组件、局部组件&#xff09;&#xff1b; &#xff08;2&#xff09;重点学会组件之间的数据传递&#xff08;prop传值、自定义事件&#xff09;&am…

SAP 用事务码SQVI 制作简单的ALV报表

我们在项目实施和运维的过程中经常会接到用户的很多需求&#xff0c;有很大的一部分需求可能都是一些报表的需求&#xff0c;有些报表的需求需要开发人员使用ABAP编写&#xff0c;但是有些报表仅仅只是两个或者多个报表的表关联就可以实现。这个时候我们就可以用SQVI这个事物代…

揭秘!宠物空气净化器对抗猫毛过敏,效果真的超乎想象?

猫毛过敏困扰着不少爱猫人士。尽管网络上充斥着各种缓解策略&#xff0c;但究竟哪种方法效果最佳&#xff1f;作为一位经验丰富的宠物主人&#xff0c;我搜集了大量信息&#xff0c;对比了几种主流的猫毛过敏应对策略&#xff0c;比如药物治疗、日常清洁和宠物空气净化器的使用…

阿里云私有CA使用教程

点击免费生成 根CA详情 启用根CA -----BEGIN CERTIFICATE----- MIIDpzCCAogAwIBAgISBZ2QPcfDqvfI8fqoPkOq6AoMA0GCSqGSIb3DQEBCwUA MFwxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdiZWlqaW5nMRAwDgYDVQQHDAdiZWlq aW5nMQ0wCwYDVQQKDARDU0REMQ0wCwYDVQQLDARDU0REMQswCQYDVQQDDAJDTjA…

单列集合--ArryList、LinkedList、Set

使用IDEA进入某个类之后&#xff0c;按ctrlF12,或者alt数字7&#xff0c;可查看该实现类的大纲。 package exercise;import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.function.Consumer;public class Demo3 {public static void…

开放式耳机哪个牌子好?2024年度热门机型推荐榜单分享!

随着音乐技术的不断革新&#xff0c;开放式耳机已成为音乐发烧友们的首选。从最初的简单音质&#xff0c;到如今的高清解析&#xff0c;开放式耳机不断进化。音质纯净&#xff0c;佩戴舒适&#xff0c;无论是街头漫步还是家中细细静听&#xff0c;都能带给你身临其境的音乐体验…

iOS18 新变化提前了解,除了AI还有这些变化

iOS 18即将在不久的将来与广大iPhone用户见面&#xff0c;这次更新被普遍认为是苹果历史上最重要的软件更新之一。据多方报道和泄露的消息&#xff0c;iOS 18将带来一系列全新的功能和改进&#xff0c;包括在人工智能领域的重大突破、全新的设计元素以及增强的性能和安全性。现…

AI教我变得厉害的思维模式01 - 成长型思维模式

今天和AI一起思考如何培养自己的成长性思维。 一一核对&#xff0c;自己哪里里做到&#xff0c;哪里没有做到&#xff0c;让AI来微调训练我自己。 成长性思维的介绍 成长性思维&#xff08;Growth Mindset&#xff09;是由斯坦福大学心理学教授卡罗尔德韦克&#xff08;Carol…