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

news2024/11/18 3:31:02

放置物体功能

效果:

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

功能:

  • 自定义物体占用区域的大小
  • 一键调整占用区域
  • 调整旋转度数,分四个挡位:
    • 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.coloradmin.cn/o/1792356.html

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

相关文章

Vue03-HelloWord

一、Hello World 1-1、示例1 1、现有html容器&#xff1b; 2、再有vue实例。 new Vue({});中的{}是配置对象。配置对象是&#xff1a;key&#xff1a;value的格式。 el&#xff1a;element元素。id对应#&#xff0c;class对应. 把容器中变化的数据&#xff0c;交给Vue实例去保…

嵌入式软件跳槽求指导?

嵌入式软件行业的跳槽确实需要一些特定的策略和技巧。我这里有一套嵌入式入门教程&#xff0c;不仅包含了详细的视频讲解&#xff0c;项目实战。如果你渴望学习嵌入式&#xff0c;不妨点个关注&#xff0c;给个评论222&#xff0c;私信22&#xff0c;我在后台发给你。 因为这个…

[Algorithm][动态规划][两个数组的DP][最长公共子序列][不相交的线][不同的子序列][通配符匹配]详细讲解

目录 1.最长公共子序列1.题目链接2.算法原理详解3.代码实现 2.不相交的线1.题目链接2.算法原理详解3.代码实现 3.不同的子序列1.题目链接2.算法原理详解3.代码实现 4.通配符匹配1.题目链接2.算法原理详解3.代码实现 1.最长公共子序列 1.题目链接 最长公共子序列 2.算法原理详…

Linux网络编程:数据链路层协议

目录 前言&#xff1a; 1.以太网 1.1.以太网帧格式 1.2.MTU&#xff08;最大传输单元&#xff09; 1.2.1.IP协议和MTU 1.2.2.UDP协议和MTU 1.2.3.TCP协议和MTU 2.ARP协议&#xff08;地址解析协议&#xff09; 2.1.ARP在局域网通信的角色 2.2.ARP报文格式 2.3.ARP报文…

SpringBoot高手之路02-全局异常处理器

RestControllerAdvice 可以将响应数据返回json格式然后响应 那么开始做全局异常处理器 首先先定义一个类 package com.healer.exception;import com.healer.common.Result; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.we…

AIGC专栏11——EasyAnimateV2结构详解与Lora训练 最大支持768x768 144帧视频生成

AIGC专栏11——EasyAnimateV2结构详解与Lora训练 最大支持768x768 144帧视频生成 学习前言源码下载地址EasyAnimate V2简介技术储备Diffusion Transformer (DiT)Motion ModuleU-VITLora 算法细节算法组成视频VAE视频DIT 数据处理视频分割视频筛选视频描述 模型训练视频VAE视频D…

Leetcode3168. 候诊室中的最少椅子数

Every day a Leetcode 题目来源&#xff1a;3168. 候诊室中的最少椅子数 解法1&#xff1a;模拟 代码&#xff1a; /** lc appleetcode.cn id3168 langcpp** [3168] 候诊室中的最少椅子数*/// lc codestart class Solution { public:int minimumChairs(string s){int chair…

前缀树的实现

前缀树的实现 何谓前缀树&#xff1f;实现前缀树节点的选择插入查找查找前缀完整代码 何谓前缀树&#xff1f; 前缀树&#xff08;字典树&#xff09;&#xff0c;使用树状的数据结构存储一个字典的所有单词。前缀树是一个多叉树&#xff0c;一个节点可能有多个子节点&#xf…

SpringMVC:转发和重定向

1. 请求转发和重定向简介 参考该链接第9点 2. forward 返回下一个资源路径&#xff0c;请求转发固定格式&#xff1a;return "forward:资源路径"如 return "forward:/b" 此时为一次请求返回逻辑视图名称 返回逻辑视图不指定方式时都会默认使用请求转发in…

优化你的WordPress网站:内链建设与Link Whisper Pro插件的利用

文章目录 内链的重要性WordPress SEO插件&#xff1a;Link Whisper Pro主要功能使用指南下载与安装 结语 在数字营销和网站管理领域&#xff0c;SEO内部优化是提升网站排名、增加流量和提高用户参与度的核心策略。在众多SEO技巧中&#xff0c;内链建设是构建良好网站结构和提升…

使用Django JWT实现身份验证

文章目录 安装依赖配置Django设置创建API生成和验证Token总结与展望 在现代Web应用程序中&#xff0c;安全性和身份验证是至关重要的。JSON Web Token&#xff08;JWT&#xff09;是一种流行的身份验证方法&#xff0c;它允许在客户端和服务器之间安全地传输信息。Django是一个…

docker实战流程:

Docker-compose是docker官方的开源项目&#xff0c;负责实现对docker容器的集群的快速编排&#xff08;通过yaml文件docker-compose.yml管理写好容器之间的调用关系只需一个命令就能实现容器的通识开启或关闭&#xff09;。 类比spring容器&#xff0c;spring管理的是bean而do…

【乐吾乐3D可视化组态编辑器】3D场景与大屏通信

乐吾乐大屏可视化可以实现大屏页面与内嵌2d/3d场景相互通信&#xff0c;底层原理是利用了iframe通过postMessage发送消息。 3D组态编辑器地址&#xff1a;3D可视化组态 - 乐吾乐Le5le 大屏组态编辑器地址&#xff1a;大屏可视化设计器 - 乐吾乐Le5le 下面以3d场景为例&#…

C# 判断字符串不等于空的示例

在C#中&#xff0c;要判断一个字符串是否不等于空&#xff08;即它既不是null也不是空字符串""&#xff09;&#xff0c;方法有如下几种&#xff0c;如下。 方法1 使用逻辑运算符和string.IsNullOrEmpty方法 string myString "123"; // 假设要检查的字…

Python第二语言(三、Python函数def)

目录 1. Python函数&#xff08;def 函数名():&#xff09; 1.1 sorted对容器进行排序&#xff1a;无法指定排序规则 1.2 sort对容器自定义排序&#xff1a;可以指定排序规则 1.3 获取变量长度函数&#xff08;len&#xff09; 1.4 函数的定义 1.5 函数-传参定义 1.6 函…

JFinal学习06 控制器——getPara()接收数据

JFinal学习06 控制器——getPara()接收数据 视频来源https://www.bilibili.com/video/BV1Bt411H7J9/?spm_id_from333.337.search-card.all.click 文章目录 JFinal学习06 控制器——getPara()接收数据零、JFinal数据提交的三种方式一、get提交二、post提交三、url参数化提交四、…

基于spring boot+vue的校园新闻管理系统

随着网络不断的普及发展&#xff0c;校园新闻网站依靠网络技术的支持得到了快速的发展&#xff0c; 首先要从用户的实际需求出发&#xff0c; 通过了解用户的需求开发出具有针对性的首页、校园 新闻、 论坛交流、 留言反馈、 个人中心、 后台管理功能&#xff0c; 利用目前网络…

MotionEditor_ 通过内容感知扩散编辑视频运动

图1. MotionEditor&#xff1a;一种基于扩散的视频编辑方法&#xff0c;旨在将参考视频的运动转移到源视频中。 摘要 现有的基于扩散的视频编辑模型在随时间编辑源视频的属性方面取得了显著进展&#xff0c;但在修改运动信息的同时保持原始主角的外观和背景方面存在困难。为…

计算机网络 期末复习(谢希仁版本)第2章

物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输媒体。4 个特性&#xff1a; 机械特性&#xff1a;指明接口所用接线器的形状和尺寸、引线数目和排列、固定和锁定装置等。 电气特性&#xff1a;指明在接口电缆的各条线上出现…

gitlabcicd-k8s部署gitlab

一.安装准备环境 存储使用nfs挂载持久化 k8s环境 helm安装 建议helm 3 二.部署gitlab-deploy.yaml nfs的ip是192.168.110.190 挂载目录是/data/data 注意所需要的目录需要创建&#xff1a;/data/data/gitlab/config &#xff0c;/data/data/gitlab/logs &#xff0c;/dat…