You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Unity网格建造系统的旋转预制件与网格对齐问题的通用数学解决方案咨询

基于Unity网格建造系统的旋转预制件与网格对齐问题的通用数学解决方案咨询

问题描述

我正在Unity中开发一个基于网格的放置/建造系统,预制件有不同的形状和尺寸(比如1x3、2x4的非对称结构),每个预制件通过PrefabGridData定义自己的网格占用mask。目前高亮系统和网格逻辑已经正常工作,但旋转预制件(90°步长)时,预制件的模型总是和高亮的网格Quad不对齐——0°时没问题,但90°/180°/270°时,尤其是非对称预制件,模型和高亮区域会偏移。

我的核心需求是:不管预制件的原始形状、尺寸、pivot位置如何,旋转后都能让模型完美居中在高亮的网格mask中心,和所有Quad完全重合,不需要针对每个旋转角度写hack,用纯数学方法实现通用对齐。

现有代码

GridManager.cs(网格管理类)

public class GridManager : MonoBehaviour {
    public int width = 20;
    public int height = 20;
    public float cellSize = 1f;
    private bool[,] occupied;

    void Awake() {
        occupied = new bool[width, height];
    }

    public Vector2Int WorldToGrid(Vector3 pos) {
        int x = Mathf.FloorToInt(pos.x / cellSize);
        int y = Mathf.FloorToInt(pos.z / cellSize);
        return new Vector2Int(x, y);
    }

    public Vector3 GridToWorld(int x, int y) {
        // 返回网格单元格的左下角世界位置
        return new Vector3(x * cellSize, 0, y * cellSize);
    }

    public bool IsInBounds(int x, int y) {
        return x >= 0 && x < width && y >= 0 && y < height;
    }

    public bool CanPlaceObject(int x, int y) {
        return IsInBounds(x, y) && !occupied[x, y];
    }

    public void OccupyCell(int x, int y) {
        if (IsInBounds(x, y)) occupied[x, y] = true;
    }

    #if UNITY_EDITOR
    private void OnDrawGizmos() {
        Gizmos.color = Color.gray;
        for (int x = 0; x < width; x++)
            for (int y = 0; y < height; y++) {
                Vector3 pos = GridToWorld(x, y);
                Gizmos.DrawWireCube(pos + new Vector3(cellSize / 2, 0, cellSize / 2), new Vector3(cellSize, 0.02f, cellSize));
            }
    }
    #endif
}

PrefabGridData.cs(预制件网格数据类)

public class PrefabGridData : MonoBehaviour {
    [Tooltip("网格占用mask,每个Vector2Int表示相对于模型左下角的网格偏移")]
    public List<Vector2Int> occupiedCells = new List<Vector2Int>() { new Vector2Int(0,0) };
    public Vector3 gridOffset;
    public int maskWidth = 3;
    public int maskHeight = 3;

    #if UNITY_EDITOR
    private void OnDrawGizmosSelected() {
        float cellSize = 1f;
        Vector3 basePos = transform.position + gridOffset;
        Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.2f);
        foreach (var cell in occupiedCells) {
            Vector3 cellCenter = basePos + new Vector3((cell.x + 0.5f) * cellSize, 0.1f, (cell.y + 0.5f) * cellSize);
            Gizmos.DrawCube(cellCenter, new Vector3(cellSize, 0.1f, cellSize));
            Gizmos.color = Color.blue;
            Gizmos.DrawWireCube(cellCenter, new Vector3(cellSize, 0.1f, cellSize));
            Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.2f);
        }
    }

    [CustomEditor(typeof(PrefabGridData))]
    public class MaskEditor : Editor {
        public override void OnInspectorGUI() {
            DrawDefaultInspector();
            PrefabGridData data = (PrefabGridData)target;

            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Visual Grid Mask Editor", EditorStyles.boldLabel);

            // 临时mask缓冲区
            bool[,] tempMask = new bool[data.maskWidth, data.maskHeight];
            foreach (var cell in data.occupiedCells)
                if (cell.x >= 0 && cell.x < data.maskWidth && cell.y >= 0 && cell.y < data.maskHeight)
                    tempMask[cell.x, cell.y] = true;

            bool changed = false;
            for (int y = data.maskHeight - 1; y >= 0; y--) {
                EditorGUILayout.BeginHorizontal();
                for (int x = 0; x < data.maskWidth; x++) {
                    bool occupied = tempMask[x, y];
                    GUIStyle style = new GUIStyle(GUI.skin.button);
                    style.normal.textColor = occupied ? Color.green : Color.red;
                    style.fontStyle = occupied ? FontStyle.Bold : FontStyle.Normal;
                    string label = occupied ? "■" : "□";

                    if (GUILayout.Button(label, style, GUILayout.Width(30), GUILayout.Height(30))) {
                        Undo.RecordObject(data, "Grid Mask Change");
                        tempMask[x, y] = !tempMask[x, y];
                        changed = true;
                    }
                }
                EditorGUILayout.EndHorizontal();
            }

            // 实时更新mask
            if (changed) {
                data.occupiedCells.Clear();
                for (int x = 0; x < data.maskWidth; x++)
                    for (int y = 0; y < data.maskHeight; y++)
                        if (tempMask[x, y])
                            data.occupiedCells.Add(new Vector2Int(x, y));
                EditorUtility.SetDirty(data);
            }
        }
    }
    #endif
}

BuildingSystem.cs(建造系统类,基础框架)

public class BuildingSystem : MonoBehaviour {
    // 处理预制件预览和放置逻辑
    public GridManager gridManager;
    public GameObject currentPreviewPrefab;
    private int currentRotationSteps = 0; // 0=0°,1=90°,2=180°,3=270°

    // 下文补充对齐逻辑
}

通用数学解决方案

核心思路

问题的本质是:旋转后,预制件的几何中心需要和网格mask的中心完全对齐,不管预制件的原始pivot在哪里。我们通过以下纯数学步骤实现通用对齐:

  1. 对当前旋转角度,生成旋转后的网格mask
  2. 计算旋转后网格mask的包围盒中心(世界空间)
  3. 计算预制件模型的本地几何中心(忽略原始pivot)
  4. 将本地几何中心旋转到当前旋转角度的世界空间方向
  5. 调整预制件的世界位置,让模型几何中心与网格mask中心重合

具体代码实现

在BuildingSystem中添加以下方法,实现通用对齐:

// 根据当前旋转角度,生成旋转后的网格mask(顺时针每步90°)
private List<Vector2Int> GetRotatedGridMask(PrefabGridData originalData, int rotationSteps) {
    List<Vector2Int> rotatedMask = new List<Vector2Int>();
    int currentWidth = originalData.maskWidth;
    int currentHeight = originalData.maskHeight;

    foreach (var cell in originalData.occupiedCells) {
        Vector2Int rotatedCell = cell;
        int tempWidth = currentWidth;
        int tempHeight = currentHeight;

        // 按步骤逐次旋转
        for (int i = 0; i < rotationSteps; i++) {
            // 90°顺时针旋转公式:(x,y) → (y, tempWidth - 1 - x)
            rotatedCell = new Vector2Int(rotatedCell.y, tempWidth - 1 - rotatedCell.x);
            // 旋转后宽高互换
            (tempWidth, tempHeight) = (tempHeight, tempWidth);
        }
        rotatedMask.Add(rotatedCell);
    }
    return rotatedMask;
}

// 计算网格mask的世界空间中心
private Vector3 CalculateMaskWorldCenter(List<Vector2Int> rotatedMask, Vector2Int targetGridPos) {
    // 计算旋转后mask的网格包围盒边界
    int minX = int.MaxValue, maxX = int.MinValue;
    int minY = int.MaxValue, maxY = int.MinValue;
    foreach (var cell in rotatedMask) {
        int gridX = targetGridPos.x + cell.x;
        int gridY = targetGridPos.y + cell.y;
        minX = Mathf.Min(minX, gridX);
        maxX = Mathf.Max(maxX, gridX);
        minY = Mathf.Min(minY, gridY);
        maxY = Mathf.Max(maxY, gridY);
    }

    // 计算网格包围盒的中心坐标(支持非整数中心,比如1.5网格单位)
    float centerGridX = (minX + maxX) / 2f;
    float centerGridY = (minY + maxY) / 2f;

    // 转换为世界空间中心:网格左下角 + 中心偏移量
    Vector3 centerWorldPos = gridManager.GridToWorld(Mathf.FloorToInt(centerGridX), Mathf.FloorToInt(centerGridY));
    centerWorldPos += new Vector3(
        (centerGridX % 1) * gridManager.cellSize,
        0,
        (centerGridY % 1) * gridManager.cellSize
    );
    return centerWorldPos;
}

// 获取预制件的本地几何中心(忽略原始pivot,取模型整体包围盒中心)
private Vector3 GetPrefabLocalCenter(GameObject prefab) {
    Renderer[] renderers = prefab.GetComponentsInChildren<Renderer>();
    if (renderers.Length == 0) return Vector3.zero;

    // 合并所有子物体的包围盒,得到整体模型的包围盒
    Bounds combinedBounds = renderers[0].bounds;
    foreach (Renderer r in renderers) {
        combinedBounds.Encapsulate(r.bounds);
    }

    // 返回相对于预制件transform的本地空间中心
    return prefab.transform.InverseTransformPoint(combinedBounds.center);
}

// 核心对齐方法:将预制件完美对齐到当前网格mask
public void AlignPrefabToGrid(GameObject prefab, Vector2Int targetGridPos) {
    PrefabGridData prefabData = prefab.GetComponent<PrefabGridData>();
    if (prefabData == null) return;

    // 1. 获取当前旋转角度的Quaternion
    float rotationY = currentRotationSteps * 90f;
    Quaternion targetRotation = Quaternion.Euler(0, rotationY, 0);

    // 2. 生成旋转后的网格mask
    List<Vector2Int> rotatedMask = GetRotatedGridMask(prefabData, currentRotationSteps);

    // 3. 计算网格mask的世界空间中心
    Vector3 maskWorldCenter = CalculateMaskWorldCenter(rotatedMask, targetGridPos);

    // 4. 获取预制件的本地几何中心
    Vector3 localModelCenter = GetPrefabLocalCenter(prefab);

    // 5. 将本地中心旋转到当前旋转方向,得到世界空间偏移量
    Vector3 rotatedLocalCenter = targetRotation * localModelCenter;

    // 6. 最终位置:mask中心减去旋转后的本地中心偏移,让模型几何中心与mask中心重合
    prefab.transform.position = maskWorldCenter - rotatedLocalCenter;
    prefab.transform.rotation = targetRotation;
}

关键说明

  1. 旋转网格mask:使用纯数学公式旋转网格坐标,支持任意90°倍数的旋转,无硬编码判断。
  2. 几何中心计算:合并所有子模型的包围盒,确保即使预制件有多个子物体,也能获取准确的整体几何中心,完全不受原始pivot位置影响。
  3. 矩阵旋转偏移:用Quaternion直接旋转本地几何中心,得到旋转后的世界空间偏移,确保旋转角度变化时计算始终准确。
  4. 精确对齐:通过maskWorldCenter - rotatedLocalCenter的计算,让模型的几何中心精确落在网格mask的中心,从而与所有高亮Quad完全重合。

使用方法

在预制件预览(比如鼠标悬停时)和放置时,调用AlignPrefabToGrid(currentPreviewPrefab, targetGridPos)即可实现实时对齐:

// 示例:鼠标移动时更新预览位置
private void Update() {
    if (currentPreviewPrefab == null) return;

    // 获取鼠标对应的网格位置
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    if (Physics.Raycast(ray, out RaycastHit hit)) {
        Vector2Int gridPos = gridManager.WorldToGrid(hit.point);
        AlignPrefabToGrid(currentPreviewPrefab, gridPos);
    }

    // 按R键切换旋转角度
    if (Input.GetKeyDown(KeyCode.R)) {
        currentRotationSteps = (currentRotationSteps + 1) % 4;
        AlignPrefabToGrid(currentPreviewPrefab, gridManager.WorldToGrid(currentPreviewPrefab.transform.position));
    }
}

这个方案完全通用,不管预制件是对称还是非对称、原始pivot在角落还是边缘,旋转后都能完美匹配高亮网格,不需要任何针对特定旋转角度的hack。

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 06:19:48