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.mfbz.cn/a/702930.html

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

相关文章

Solr 日志系统7.4.0部署和迁移到本地,Core Admin 添加新的core报错

文章目录 Solr部署Docker部署二进制部署 Tips:Solr设置账号密码方法1&#xff1a;(不使用)方法2&#xff1a; Core Admin 添加新的core报错Solr数据迁移 Solr部署 Docker部署 docker run -d -p 8983:8983 --name solr solr:latest docker run -d -p 8983:8983 -v /opt/solr:/…

操作系统入门系列-MIT6.828(操作系统工程)学习笔记(五)---- 操作系统的组织结构(OS design)

系列文章目录 操作系统入门系列-MIT6.S081&#xff08;操作系统&#xff09;学习笔记&#xff08;一&#xff09;---- 操作系统介绍与接口示例 操作系统入门系列-MIT6.828&#xff08;操作系统工程&#xff09;学习笔记&#xff08;二&#xff09;----课程实验环境搭建&#x…

网工内推 | 深信服、中软国际技术支持工程师,最高13k*13薪

01 深信服 &#x1f537;招聘岗位&#xff1a;远程技术支持工程师 &#x1f537;任职要求&#xff1a; 一、专业能力和行业经验&#xff1a; ①具备友商同岗位工作经验1.5年以上&#xff0c;具备良好的分析和判断能力&#xff0c;有独立问题处理思路&#xff0c;具备常见协…

python中魔术方法__str__与__repr__的区别

在Python中&#xff0c;__str__和__repr__是两个常见的魔法方法&#xff08;也称为双下方法或dunder方法&#xff09;&#xff0c;它们用于定义对象的字符串表示形式。它们的主要区别在于它们的用途和使用场景。 __str__ 用途&#xff1a;__str__方法用于为用户提供一个易读的…

【嵌入式DIY实例】-Nokia 5110显示DHT11/DHT22传感器数据

Nokia 5110显示DHT11/DHT22传感器数据 文章目录 Nokia 5110显示DHT11/DHT22传感器数据1、硬件准备2、代码实现2.1 显示DHT11数据2.2 显示DHT22数据本文介绍如何将 ESP8266 NodeMCU 开发板 (ESP-12E) 与 DHT11 数字湿度和温度传感器以及诺基亚 5110 LCD 连接。 NodeMCU 从 DHT11…

.NET Core 服务注册步骤总结

总结一下 .NET Core 服务注册的步骤&#xff1a; .NET Core Web Api 项目服务注册步骤&#xff1a; 创建一个接口&#xff0c;和实现类 比如&#xff1a;IMyService, CnService 在 Program.cs 的 var app builder.Build(); 语句之前加上&#xff1a; var builder WebApplic…

【面经总结】 Java基础 - 异常

异常 介绍一下 Java 的异常体系 Java 的异常体系是由 Throwable 类及其子类构成的。 Throwable 包含两个子类&#xff1a;Error&#xff08;错误&#xff09;和 Exception&#xff08;异常&#xff09; Error 表示错误&#xff0c;通常不需要程序员处理&#xff0c;如内存溢…

python中的turtle

turtle个别指令 初始箭头默认指向为东&#xff08;右&#xff09; 往前&#xff08;右&#xff09;三个格&#xff1a;turtle.forward(3) 往后&#xff08;左&#xff09;三个格&#xff1a;turtle.backward(3) 往左转90度&#xff1a;turtle.left(90) 往右转90度&#xf…

Attention与轻量级ResNet融合,低资源消耗下实现效率和性能完美平衡

注意力机制通过让模型关注图像关键区域提升了识别精度&#xff0c;而轻量级残差网络通过减少参数和计算量&#xff0c;实现了在低资源消耗下的优秀性能。 结合注意力机制与轻量级残差网络&#xff0c;既能让模型能够更高效地关注输入数据中的关键信息&#xff0c;提升模型处理…

vs调试时无法找到文件-chromium源码编译

一直跟着教程走结果报错了&#xff0c;找了半天的教程无法解决&#xff0c;于是乎只好重来&#xff0c;因为这个是属于项目调试&#xff0c;报错了可以重新编译项目就好。在重新做的过程中发现路径写错了

人工智能的等价形式

经典的人工智能&#xff0c;采用“梯度下降法”&#xff0c;运算量很大&#xff0c;约是esp2。其中e是epoch&#xff0c;训练的周期数&#xff1b;s是sample&#xff0c;训练样本的数量&#xff1b;p是parameter&#xff0c;参数的数量。 人工智能有等价形式&#xff0c;它不需…

DPI简析

DPI简析 一、DPI与PPI二、硬件设备的DPI2.1打印机DPI2.2显示器DPI2.2.1显示器DPI计算2.2.2显示器分辨率与系统分辨率2.2.3常见分辨率 2.3鼠标DPI 三、图片DPI3.1图片DPI与打印尺寸3.1.1图片打印尺寸计算3.1.2常用的照片尺寸及DPI 3.2图片DPI与屏幕显示3.3修改图片DPI 参考文档 …

Windos10上Podman安装运行mysql8

记录以下在windows10系统上Podman v5.1.1安装MySQL8全过程。 目录 一、拉取mysql8镜像二、创建宿主目录三、创建 my.cnf文件四、创建Mysql8容器五、windows上Podman安装运行mysql8失败问题描述 解决办法① 通过PowerShell进入wsl② 修改wsl系统配置③ 重启wsl&#xff0c;Podma…

3个月搞定计算机二级C语言!高效刷题系列进行中

文章目录 前言备考计算机二级C语言为什么考二级C语言&#xff1f;刷题总结后发布系列文章后记免责声明 前言 大家好&#xff0c;我是梁国庆。 计算机二级应该是每一位大学生的必修课&#xff0c;相信很多同学的大学flag中都会有它的身影。 我在大学里也不止一次的想要考计算…

【运维知识大神篇】运维界的超神器Kubernetes教程14(RBAC三种认证方式详解:基于用户+基于用户组+基于服务账号)

本篇文章继续给大家介绍Kubernetes&#xff0c;内容依旧烧脑&#xff0c;不过内容也已经过了一大半了&#xff0c;如果你把我Kubernetes前面的教程都看懂了的话&#xff0c;那么你已经很厉害了&#xff0c;坚持下去&#xff01;本篇文章主要介绍RBAC的三种认证方式&#xff0c;…

Python使用tkinter库设置背景图片、label显示位置和label设置显示图片

tkinter 设置背景图片 label显示位置 label设置显示图片 from tkinter import * import tkinter as tk from PIL import ImageTk from PIL import Imagedef get_img(filename, width, height):im Image.open(filename).resize((width, height))im ImageTk.PhotoImage(im)…

Java的集合框架总结

Map接口和Collection接口是所有集合框架的父接口&#xff1a; Collection接口的子接口包括&#xff1a;Set接口和List接口 Map接口的实现类主要有&#xff1a;HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 Set接口的实现类主要有&#xff1a;HashSet、Tr…

【CH32V305FBP6】USBD HS 描述符修改

文章目录 前言设备描述符完整描述符配置描述符CDC 描述符接口关联描述符接口描述符功能描述符端点描述符接口描述符端点描述符 HID 描述符接口描述符 新增一个 HID 设备 前言 USB HS 复合设备&#xff0c;CDCHID 功能&#xff1a;串口、DAP、CAN-HID、RS485 设备描述符 htt…

远程抄表系统方案

一、远程抄表系统简述 远程抄表系统&#xff0c;是一种智能化的自动化数据收集技术性&#xff0c;主要运用于电力工程、供水公司、天然气等公共服务行业&#xff0c;完成对消费者表计数据的远程、即时、智能采集。这类系统不但降低了成本&#xff0c;降低了人力抄表错误&#…

手部出现这4种症状,可能是糖尿病先兆!务必及时监测血糖

糖尿病来袭&#xff0c;手部或先知&#xff01;出现这 4 种症状&#xff0c;务必尽快监测血糖 第一&#xff0c;手指麻木感明显&#xff0c;当持续性的高血糖损害手部血管与神经末梢时&#xff0c;便会导致手指产生麻木以及刺痛之感。而且随着血糖不断升高&#xff0c;受损的血…