318 lines
13 KiB
C#
318 lines
13 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace AibisDream
|
||
{
|
||
/// <summary>
|
||
/// 放置验证器实现
|
||
/// 负责验证形状是否可以放置在指定位置
|
||
/// </summary>
|
||
public static class PlacementValidator
|
||
{
|
||
/// <summary>
|
||
/// 检查形状是否可以放置在指定位置
|
||
/// </summary>
|
||
/// <param name="grid">网格系统</param>
|
||
/// <param name="shape">要放置的形状</param>
|
||
/// <param name="gridX">网格X坐标(形状左下角位置)</param>
|
||
/// <param name="gridY">网格Y坐标(形状左下角位置)</param>
|
||
/// <returns>放置验证结果</returns>
|
||
public static PlacementResult CanPlace(IBlockPuzzleGrid grid, IBlockShape shape, int gridX, int gridY)
|
||
{
|
||
if (grid == null || shape == null)
|
||
{
|
||
return PlacementResult.InvalidPosition;
|
||
}
|
||
|
||
// 获取形状的所有格子位置(相对坐标,基于形状的本地坐标系)
|
||
Vector2Int[] blockPositions = shape.GetBlockPositions();
|
||
if (blockPositions == null || blockPositions.Length == 0)
|
||
{
|
||
return PlacementResult.InvalidPosition;
|
||
}
|
||
|
||
// 形状的坐标系统:
|
||
// - GetBlockPositions() 返回的坐标中,(0, 0) 是形状的左下角
|
||
// - pattern[i, j] 中,i 是 x(从左到右),j 是 y(从下到上,j=0 是底部)
|
||
// - gridX, gridY 是形状左下角在网格中的位置
|
||
// - 需要将形状的相对坐标转换为网格坐标
|
||
foreach (var blockPos in blockPositions)
|
||
{
|
||
// 将形状的相对坐标转换为网格坐标
|
||
// 形状的 (0, 0) 对应网格的 (gridX, gridY)
|
||
// 形状的 (i, j) 对应网格的 (gridX + i, gridY + j)
|
||
int gridCellX = gridX + blockPos.x;
|
||
int gridCellY = gridY + blockPos.y;
|
||
|
||
// 检查是否超出边界
|
||
if (!grid.IsValidPosition(gridCellX, gridCellY))
|
||
{
|
||
return PlacementResult.OutOfBounds;
|
||
}
|
||
|
||
// 检查是否与已有块重叠
|
||
if (!grid.IsCellEmpty(gridCellX, gridCellY))
|
||
{
|
||
return PlacementResult.Overlap;
|
||
}
|
||
}
|
||
|
||
return PlacementResult.Success;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查形状是否可以放置在指定位置(详细版本)
|
||
/// </summary>
|
||
/// <param name="grid">网格系统</param>
|
||
/// <param name="shape">要放置的形状</param>
|
||
/// <param name="gridX">网格X坐标(形状左下角位置)</param>
|
||
/// <param name="gridY">网格Y坐标(形状左下角位置)</param>
|
||
/// <param name="failureReason">失败原因(如果返回false)</param>
|
||
/// <returns>是否可以放置</returns>
|
||
public static bool CanPlaceDetailed(IBlockPuzzleGrid grid, IBlockShape shape, int gridX, int gridY, out PlacementResult failureReason)
|
||
{
|
||
failureReason = CanPlace(grid, shape, gridX, gridY);
|
||
return failureReason == PlacementResult.Success;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取形状在指定位置的所有占用格子坐标
|
||
/// </summary>
|
||
/// <param name="shape">形状</param>
|
||
/// <param name="gridX">网格X坐标(形状左下角位置)</param>
|
||
/// <param name="gridY">网格Y坐标(形状左下角位置)</param>
|
||
/// <returns>占用格子的网格坐标数组</returns>
|
||
public static (int x, int y)[] GetOccupiedCells(IBlockShape shape, int gridX, int gridY)
|
||
{
|
||
if (shape == null)
|
||
{
|
||
return new (int x, int y)[0];
|
||
}
|
||
|
||
Vector2Int[] blockPositions = shape.GetBlockPositions();
|
||
if (blockPositions == null || blockPositions.Length == 0)
|
||
{
|
||
return new (int x, int y)[0];
|
||
}
|
||
|
||
List<(int x, int y)> occupiedCells = new List<(int x, int y)>();
|
||
|
||
foreach (var blockPos in blockPositions)
|
||
{
|
||
// 将形状的相对坐标转换为网格坐标(左下角为基准)
|
||
// 形状的 (0, 0) 对应网格的 (gridX, gridY)
|
||
// 形状的 (i, j) 对应网格的 (gridX + i, gridY + j)
|
||
int gridCellX = gridX + blockPos.x;
|
||
int gridCellY = gridY + blockPos.y;
|
||
occupiedCells.Add((gridCellX, gridCellY));
|
||
}
|
||
|
||
return occupiedCells.ToArray();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 将形状的单个相对坐标转换为网格坐标
|
||
/// </summary>
|
||
/// <param name="shapeLocalX">形状内的X坐标(相对坐标,相对于形状左下角)</param>
|
||
/// <param name="shapeLocalY">形状内的Y坐标(相对坐标,相对于形状左下角)</param>
|
||
/// <param name="gridX">网格X坐标(形状左下角位置)</param>
|
||
/// <param name="gridY">网格Y坐标(形状左下角位置)</param>
|
||
/// <returns>对应的网格坐标</returns>
|
||
public static Vector2Int ConvertShapeLocalToGrid(int shapeLocalX, int shapeLocalY, int gridX, int gridY)
|
||
{
|
||
return new Vector2Int(gridX + shapeLocalX, gridY + shapeLocalY);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查世界坐标是否可以吸附到网格
|
||
/// </summary>
|
||
/// <param name="grid">网格系统</param>
|
||
/// <param name="worldPos">世界坐标位置</param>
|
||
/// <param name="snapDistance">吸附距离阈值</param>
|
||
/// <returns>吸附检测结果</returns>
|
||
public static SnapResult CheckSnapToGrid(IBlockPuzzleGrid grid, Vector3 worldPos, float snapDistance)
|
||
{
|
||
SnapResult result = new SnapResult
|
||
{
|
||
canSnap = false,
|
||
snapPosition = worldPos,
|
||
gridX = 0,
|
||
gridY = 0,
|
||
distance = float.MaxValue
|
||
};
|
||
|
||
if (grid == null)
|
||
{
|
||
return result;
|
||
}
|
||
|
||
// 将世界坐标转换为网格坐标
|
||
// 注意:WorldToGrid 需要世界坐标在网格范围内才能返回true
|
||
// 但我们需要检查所有附近的网格单元格,不仅仅是当前所在的单元格
|
||
// 所以先尝试直接转换,如果失败则查找最近的网格单元格
|
||
|
||
int tempGridX, tempGridY;
|
||
bool inGrid = grid.WorldToGrid(worldPos, out tempGridX, out tempGridY);
|
||
|
||
if (!inGrid)
|
||
{
|
||
// 如果不在网格内,尝试查找最近的网格单元格
|
||
// 计算最近的网格坐标
|
||
Vector3 localPos = worldPos - grid.Origin;
|
||
tempGridX = Mathf.FloorToInt(localPos.x / grid.CellSize);
|
||
tempGridY = Mathf.FloorToInt(localPos.y / grid.CellSize);
|
||
|
||
// 检查是否在有效范围内
|
||
if (!grid.IsValidPosition(tempGridX, tempGridY))
|
||
{
|
||
return result;
|
||
}
|
||
}
|
||
|
||
// 获取网格单元格中心的世界坐标
|
||
Vector3 gridCenter = grid.GridToWorld(tempGridX, tempGridY);
|
||
|
||
// 计算距离
|
||
float distance = Vector3.Distance(worldPos, gridCenter);
|
||
|
||
if (distance <= snapDistance)
|
||
{
|
||
// 计算形状应该放置的网格位置
|
||
// 形状左下角对齐到网格单元格左下角
|
||
result.gridX = tempGridX;
|
||
result.gridY = tempGridY;
|
||
|
||
// 计算形状左下角的世界坐标
|
||
// 网格单元格左下角的世界坐标 = Origin + (gridX * CellSize, gridY * CellSize)
|
||
Vector3 gridBottomLeft = grid.Origin + new Vector3(
|
||
tempGridX * grid.CellSize,
|
||
tempGridY * grid.CellSize,
|
||
0
|
||
);
|
||
|
||
result.snapPosition = gridBottomLeft;
|
||
result.distance = distance;
|
||
result.canSnap = true;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查世界坐标是否可以吸附到网格(简化版本,只返回是否可以吸附和网格坐标)
|
||
/// </summary>
|
||
/// <param name="grid">网格系统</param>
|
||
/// <param name="worldPos">世界坐标位置</param>
|
||
/// <param name="snapDistance">吸附距离阈值</param>
|
||
/// <param name="gridX">吸附到的网格X坐标(形状左下角位置)</param>
|
||
/// <param name="gridY">吸附到的网格Y坐标(形状左下角位置)</param>
|
||
/// <returns>是否可以吸附</returns>
|
||
public static bool CheckSnapToGrid(IBlockPuzzleGrid grid, Vector3 worldPos, float snapDistance, out int gridX, out int gridY)
|
||
{
|
||
gridX = 0;
|
||
gridY = 0;
|
||
|
||
SnapResult result = CheckSnapToGrid(grid, worldPos, snapDistance);
|
||
if (result.canSnap)
|
||
{
|
||
gridX = result.gridX;
|
||
gridY = result.gridY;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查世界坐标是否可以吸附到网格(完整版本,返回吸附位置和网格坐标)
|
||
/// </summary>
|
||
/// <param name="grid">网格系统</param>
|
||
/// <param name="worldPos">世界坐标位置</param>
|
||
/// <param name="snapDistance">吸附距离阈值</param>
|
||
/// <param name="snapPos">吸附后的世界坐标位置</param>
|
||
/// <param name="gridX">吸附到的网格X坐标(形状左下角位置)</param>
|
||
/// <param name="gridY">吸附到的网格Y坐标(形状左下角位置)</param>
|
||
/// <returns>是否可以吸附</returns>
|
||
public static bool CheckSnapToGrid(IBlockPuzzleGrid grid, Vector3 worldPos, float snapDistance, out Vector3 snapPos, out int gridX, out int gridY)
|
||
{
|
||
snapPos = worldPos;
|
||
gridX = 0;
|
||
gridY = 0;
|
||
|
||
SnapResult result = CheckSnapToGrid(grid, worldPos, snapDistance);
|
||
if (result.canSnap)
|
||
{
|
||
snapPos = result.snapPosition;
|
||
gridX = result.gridX;
|
||
gridY = result.gridY;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查找最近的网格单元格(即使不在吸附距离内)
|
||
/// </summary>
|
||
/// <param name="grid">网格系统</param>
|
||
/// <param name="worldPos">世界坐标位置</param>
|
||
/// <param name="gridX">最近的网格X坐标</param>
|
||
/// <param name="gridY">最近的网格Y坐标</param>
|
||
/// <returns>是否找到有效的网格单元格</returns>
|
||
public static bool FindNearestGridCell(IBlockPuzzleGrid grid, Vector3 worldPos, out int gridX, out int gridY)
|
||
{
|
||
gridX = 0;
|
||
gridY = 0;
|
||
|
||
if (grid == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 先尝试直接转换
|
||
if (grid.WorldToGrid(worldPos, out gridX, out gridY))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// 如果不在网格内,计算最近的网格坐标
|
||
Vector3 localPos = worldPos - grid.Origin;
|
||
gridX = Mathf.FloorToInt(localPos.x / grid.CellSize);
|
||
gridY = Mathf.FloorToInt(localPos.y / grid.CellSize);
|
||
|
||
// 限制在有效范围内
|
||
gridX = Mathf.Clamp(gridX, 0, grid.Width - 1);
|
||
gridY = Mathf.Clamp(gridY, 0, grid.Height - 1);
|
||
|
||
return grid.IsValidPosition(gridX, gridY);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 放置验证结果
|
||
/// </summary>
|
||
public enum PlacementResult
|
||
{
|
||
Success, // 可以放置
|
||
OutOfBounds, // 超出边界
|
||
Overlap, // 与已有块重叠
|
||
InvalidPosition // 无效位置
|
||
}
|
||
|
||
/// <summary>
|
||
/// 吸附检测结果
|
||
/// </summary>
|
||
public struct SnapResult
|
||
{
|
||
public bool canSnap; // 是否可以吸附
|
||
public Vector3 snapPosition; // 吸附后的世界坐标位置
|
||
public int gridX; // 吸附到的网格X坐标(形状左下角位置)
|
||
public int gridY; // 吸附到的网格Y坐标(形状左下角位置)
|
||
public float distance; // 到网格单元格的距离
|
||
public Vector2Int[] gridPositions; // 吸附后
|
||
public Vector2Int RootCell => new Vector2Int(gridX, gridY); // 吸附后的根网格坐标
|
||
}
|
||
}
|
||
|