Files
aibis-dream/Assets/Scripts/MiniGame/BlockPuzzle/Class/PlacementValidator.cs
T
2025-12-01 20:44:20 +08:00

318 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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); // 吸附后的根网格坐标
}
}