方块拼图

This commit is contained in:
2025-11-27 18:14:24 +08:00
parent 76809a829e
commit 06a68cda8e
31 changed files with 2262 additions and 44 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0eb5cc55dcdc0f244a5cb635090c0bd3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -14,6 +14,13 @@ namespace AibisDream
[SerializeField] private float cellSize = 1f;
[SerializeField] private bool initializeOnStart = true;
[Header("编辑器预览设置")]
[SerializeField] private bool showGizmos = true;
[SerializeField] private Color gridLineColor = new Color(0.5f, 0.5f, 0.5f, 0.8f);
[SerializeField] private Color emptyCellColor = new Color(0.9f, 0.9f, 0.9f, 0.2f);
[SerializeField] private Color occupiedCellColor = new Color(0.2f, 0.6f, 1f, 0.5f);
[SerializeField] private bool showCellStates = true;
private GridCellState[,] _grid;
private bool _isInitialized = false;
@@ -157,6 +164,24 @@ namespace AibisDream
return new Vector3(worldX, worldY, Origin.z);
}
public Vector3[] GridVerticesToWorld(int x, int y)
{
if (!_isInitialized)
{
return new Vector3[4];
}
// 先获取网格中心位置
Vector3 center = GridToWorld(x, y);
// 然后获取网格四个顶点位置
Vector3[] vertices = new Vector3[4];
vertices[0] = center + new Vector3(-CellSize / 2, -CellSize / 2, 0);
vertices[1] = center + new Vector3(CellSize / 2, -CellSize / 2, 0);
vertices[2] = center + new Vector3(CellSize / 2, CellSize / 2, 0);
vertices[3] = center + new Vector3(-CellSize / 2, CellSize / 2, 0);
return vertices;
}
public void Clear()
{
if (!_isInitialized || _grid == null) return;
@@ -209,6 +234,93 @@ namespace AibisDream
height = Mathf.Max(1, height);
cellSize = Mathf.Max(0.1f, cellSize);
}
/// <summary>
/// 在编辑器中绘制网格预览
/// </summary>
private void OnDrawGizmos()
{
if (!showGizmos) return;
// 使用当前Inspector中的参数或已初始化的参数
int drawWidth = _isInitialized ? Width : width;
int drawHeight = _isInitialized ? Height : height;
float drawCellSize = _isInitialized ? CellSize : cellSize;
Vector3 drawOrigin = _isInitialized ? Origin : transform.position;
if (drawWidth <= 0 || drawHeight <= 0 || drawCellSize <= 0) return;
// 绘制网格线
Gizmos.color = gridLineColor;
for (int x = 0; x <= drawWidth; x++)
{
Vector3 start = drawOrigin + new Vector3(x * drawCellSize, 0, 0);
Vector3 end = drawOrigin + new Vector3(x * drawCellSize, drawHeight * drawCellSize, 0);
Gizmos.DrawLine(start, end);
}
for (int y = 0; y <= drawHeight; y++)
{
Vector3 start = drawOrigin + new Vector3(0, y * drawCellSize, 0);
Vector3 end = drawOrigin + new Vector3(drawWidth * drawCellSize, y * drawCellSize, 0);
Gizmos.DrawLine(start, end);
}
// 绘制单元格状态(如果已初始化且启用)
if (showCellStates && _isInitialized && _grid != null)
{
float cellDisplaySize = drawCellSize * 0.9f; // 稍微小一点,留出网格线空间
Vector3 cellSizeVec = new Vector3(cellDisplaySize, cellDisplaySize, 0.01f);
for (int x = 0; x < drawWidth; x++)
{
for (int y = 0; y < drawHeight; y++)
{
if (IsValidPosition(x, y))
{
Vector3 cellCenter = GridToWorld(x, y);
GridCellState state = GetCellState(x, y);
Gizmos.color = state == GridCellState.Occupied ? occupiedCellColor : emptyCellColor;
Gizmos.DrawCube(cellCenter, cellSizeVec);
}
}
}
}
}
/// <summary>
/// 选中时绘制更明显的网格边界
/// </summary>
private void OnDrawGizmosSelected()
{
if (!showGizmos) return;
// 使用当前Inspector中的参数或已初始化的参数
int drawWidth = _isInitialized ? Width : width;
int drawHeight = _isInitialized ? Height : height;
float drawCellSize = _isInitialized ? CellSize : cellSize;
Vector3 drawOrigin = _isInitialized ? Origin : transform.position;
if (drawWidth <= 0 || drawHeight <= 0 || drawCellSize <= 0) return;
// 绘制网格边界(更粗的线)
Gizmos.color = new Color(gridLineColor.r, gridLineColor.g, gridLineColor.b, 1f);
Vector3 bottomLeft = drawOrigin;
Vector3 bottomRight = drawOrigin + new Vector3(drawWidth * drawCellSize, 0, 0);
Vector3 topLeft = drawOrigin + new Vector3(0, drawHeight * drawCellSize, 0);
Vector3 topRight = drawOrigin + new Vector3(drawWidth * drawCellSize, drawHeight * drawCellSize, 0);
// 绘制边界框
Gizmos.DrawLine(bottomLeft, bottomRight);
Gizmos.DrawLine(bottomRight, topRight);
Gizmos.DrawLine(topRight, topLeft);
Gizmos.DrawLine(topLeft, bottomLeft);
// 绘制原点标记
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(drawOrigin, 0.1f);
}
}
}
@@ -0,0 +1,381 @@
using System.Collections.Generic;
using AibisDream.Utility;
using UnityEngine;
namespace AibisDream
{
public class BlockShape : MonoBehaviour, IBlockShape
{
private SpriteRenderer[,] _cellRenderers;
// 模块数据
[Header("模块数据")]
[SerializeField] private int shapeId;
[SerializeField] private string shapeName;
[SerializeField] private RotationAngle currentRotation;
[SerializeField] private int width;
[SerializeField] private int height;
[SerializeField] private float cellSize;
[SerializeField] private string originalPatternStr;
// 接口实现
public int ShapeId => shapeId;
public string ShapeName => shapeName;
public RotationAngle CurrentRotation => currentRotation;
public int Width => width;
public int Height => height;
public int BlockCount
{
get
{
var pattern = GetOriginalPattern();
if (pattern == null) return 0;
int count = 0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if (pattern[i, j] != 0)
{
count++;
}
}
}
return count;
}
}
public Vector2Int[] GetBlockPositions()
{
var pattern = GetOriginalPattern();
if (pattern == null) return new Vector2Int[0];
List<Vector2Int> positions = new List<Vector2Int>();
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if (pattern[i, j] != 0)
{
positions.Add(new Vector2Int(i, j));
}
}
}
return positions.ToArray();
}
/// <summary>
/// 获取单元格渲染器数组(供拖拽组件使用)
/// </summary>
public SpriteRenderer[,] GetCellRenderers()
{
return _cellRenderers;
}
public int[,] GetCurrentPattern()
{
throw new System.NotImplementedException();
}
public int[,] GetOriginalPattern()
{
return CommonUtil.DeserializeIntArray(originalPatternStr);
}
public bool HasBlockAt(int x, int y)
{
throw new System.NotImplementedException();
}
public void ResetRotation()
{
throw new System.NotImplementedException();
}
public void Rotate(RotationAngle angle)
{
throw new System.NotImplementedException();
}
public void RotateClockwise()
{
throw new System.NotImplementedException();
}
public void RotateCounterClockwise()
{
throw new System.NotImplementedException();
}
private Sprite CreateDefaultSprite()
{
Texture2D texture = new Texture2D(1, 1);
texture.SetPixel(0, 0, Color.red);
texture.Apply();
return Sprite.Create(texture, new Rect(0, 0, 1, 1), new Vector2(0.5f, 0.5f), 1f);
}
// 仅限编辑器下使用
public void DrawShape()
{
transform.DestroyAllChildren();
_cellRenderers = null;
// 根据Width和Height,绘制Cells,Cell为SpriteRenderer
_cellRenderers = new SpriteRenderer[width, height];
var defaultSprite = CreateDefaultSprite();
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
var cell = new GameObject($"Cell_{i}_{j}");
var cellRenderer = cell.AddComponent<SpriteRenderer>();
cellRenderer.sprite = defaultSprite;
_cellRenderers[i, j] = cellRenderer;
cell.transform.parent = transform;
// 计算Cell应处的位置
var cellPosX = i * cellSize;
var cellPosY = j * cellSize;
cell.transform.localPosition = new Vector3(cellPosX, cellPosY, 0);
}
}
// 反序列化并处理
var pattern = CommonUtil.DeserializeIntArray(originalPatternStr);
// 先判断pattern的维度是否与width和height一致
if (pattern.GetLength(0) != width || pattern.GetLength(1) != height)
{
// 如果不一致,就不处理
Debug.LogWarning($"Pattern dimension mismatch: {pattern.GetLength(0)}x{pattern.GetLength(1)} != {width}x{height}");
return;
}
// 如果一致,就读取pattern,并设置对应的cell的SpriteRenderer是否激活
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
_cellRenderers[i, j].enabled = pattern[i, j] != 0;
}
}
// 更新碰撞体形状
UpdatePolygonCollider();
}
public void SaveShape()
{
// 如果_cellRenderers不为空,则序列化并保存
if (_cellRenderers != null)
{
var pattern = new int[width, height];
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
pattern[i, j] = _cellRenderers[i, j].enabled ? 1 : 0;
}
}
Debug.Log($"SaveShape: {CommonUtil.SerializeIntArray(pattern)}");
originalPatternStr = CommonUtil.SerializeIntArray(pattern);
// 保存后更新碰撞体
UpdatePolygonCollider();
}
}
/// <summary>
/// 根据originalPattern更新PolygonCollider2D的形状
/// </summary>
public void UpdatePolygonCollider()
{
var pattern = GetOriginalPattern();
if (pattern == null) return;
// 获取或添加PolygonCollider2D组件
PolygonCollider2D polygonCollider = GetComponent<PolygonCollider2D>();
if (polygonCollider == null)
{
polygonCollider = gameObject.AddComponent<PolygonCollider2D>();
}
// 生成多边形顶点
List<Vector2> vertices = GeneratePolygonVertices(pattern);
if (vertices.Count > 0)
{
// 设置多边形路径
polygonCollider.pathCount = 1;
polygonCollider.SetPath(0, vertices.ToArray());
}
else
{
// 如果没有有效格子,清除路径
polygonCollider.pathCount = 0;
}
}
/// <summary>
/// 根据pattern生成多边形顶点
/// </summary>
private List<Vector2> GeneratePolygonVertices(int[,] pattern)
{
List<Vector2> vertices = new List<Vector2>();
if (pattern == null || width <= 0 || height <= 0 || cellSize <= 0)
return vertices;
// 生成精确的外轮廓(只包含边界点)
vertices = GenerateOutlineVertices(pattern);
return vertices;
}
/// <summary>
/// 生成精确的外轮廓顶点(只包含边界点)
/// 使用Marching Squares算法的简化版本
/// </summary>
private List<Vector2> GenerateOutlineVertices(int[,] pattern)
{
List<Vector2> outlineVertices = new List<Vector2>();
HashSet<Vector2Int> visitedPoints = new HashSet<Vector2Int>();
// 遍历所有格子,找到边界点
for (int i = 0; i <= width; i++)
{
for (int j = 0; j <= height; j++)
{
// 检查这个角点是否在边界上
bool isBoundaryCorner = IsBoundaryCorner(pattern, i, j);
if (isBoundaryCorner)
{
// 计算角点的本地坐标
// cornerX 和 cornerY 是角点索引(0到width/height
// 需要转换为格子的角点坐标
// 角点 (i, j) 对应格子 (i-0.5, j-0.5) 的角点位置
float x = (i - 0.5f) * cellSize;
float y = (j - 0.5f) * cellSize;
Vector2Int key = new Vector2Int(Mathf.RoundToInt(x * 100), Mathf.RoundToInt(y * 100));
if (!visitedPoints.Contains(key))
{
outlineVertices.Add(new Vector2(x, y));
visitedPoints.Add(key);
}
}
}
}
// 如果没有找到边界点,使用简单包围盒
if (outlineVertices.Count == 0)
{
return GenerateBoundingBox(pattern);
}
// 对顶点进行排序,形成闭合多边形
return OrderVerticesForPolygon(outlineVertices);
}
/// <summary>
/// 检查角点是否在边界上
/// </summary>
private bool IsBoundaryCorner(int[,] pattern, int cornerX, int cornerY)
{
// 角点位于四个格子的交汇处
// 检查周围四个格子的状态
bool topLeft = (cornerX > 0 && cornerY > 0) ? pattern[cornerX - 1, cornerY - 1] != 0 : false;
bool topRight = (cornerX < width && cornerY > 0) ? pattern[cornerX, cornerY - 1] != 0 : false;
bool bottomLeft = (cornerX > 0 && cornerY < height) ? pattern[cornerX - 1, cornerY] != 0 : false;
bool bottomRight = (cornerX < width && cornerY < height) ? pattern[cornerX, cornerY] != 0 : false;
// 如果四个格子状态不一致,说明这个角点在边界上
int filledCount = (topLeft ? 1 : 0) + (topRight ? 1 : 0) +
(bottomLeft ? 1 : 0) + (bottomRight ? 1 : 0);
// 边界角点:不是全部填充也不是全部空
return filledCount > 0 && filledCount < 4;
}
/// <summary>
/// 生成简单的包围盒(备用方法)
/// </summary>
private List<Vector2> GenerateBoundingBox(int[,] pattern)
{
List<Vector2> vertices = new List<Vector2>();
// 找到有效格子的边界
int minX = width, maxX = -1, minY = height, maxY = -1;
bool hasValidCell = false;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if (pattern[i, j] != 0)
{
hasValidCell = true;
minX = Mathf.Min(minX, i);
maxX = Mathf.Max(maxX, i);
minY = Mathf.Min(minY, j);
maxY = Mathf.Max(maxY, j);
}
}
}
if (!hasValidCell)
return vertices;
// 计算包围盒的四个角点(本地坐标)
float halfSize = cellSize / 2f;
float minXWorld = minX * cellSize - width * cellSize / 2f - halfSize;
float maxXWorld = (maxX + 1) * cellSize - width * cellSize / 2f + halfSize;
float minYWorld = minY * cellSize - height * cellSize / 2f - halfSize;
float maxYWorld = (maxY + 1) * cellSize - height * cellSize / 2f + halfSize;
// 按逆时针顺序添加顶点(Unity的PolygonCollider2D要求)
vertices.Add(new Vector2(minXWorld, minYWorld)); // 左下
vertices.Add(new Vector2(maxXWorld, minYWorld)); // 右下
vertices.Add(new Vector2(maxXWorld, maxYWorld)); // 右上
vertices.Add(new Vector2(minXWorld, maxYWorld)); // 左上
return vertices;
}
/// <summary>
/// 对顶点进行排序,形成闭合多边形
/// </summary>
private List<Vector2> OrderVerticesForPolygon(List<Vector2> vertices)
{
if (vertices.Count <= 2)
return vertices;
// 找到中心点
Vector2 center = Vector2.zero;
foreach (var v in vertices)
{
center += v;
}
center /= vertices.Count;
// 按角度排序(相对于中心点)
vertices.Sort((a, b) =>
{
float angleA = Mathf.Atan2(a.y - center.y, a.x - center.x);
float angleB = Mathf.Atan2(b.y - center.y, b.x - center.x);
if (angleA.CompareTo(angleB) != 0)
{
return angleA.CompareTo(angleB);
}
return (a - center).sqrMagnitude.CompareTo((b - center).sqrMagnitude);
});
return vertices;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4be33b2e5baf96447a7df0d10cb22064
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,405 @@
using System;
using AibisDream.Framework;
using UnityEngine;
namespace AibisDream
{
/// <summary>
/// 形状拖拽组件
/// 实现IBlockShapeDragger接口,处理形状的拖拽、吸附和放置逻辑
/// </summary>
[RequireComponent(typeof(BlockShape))]
[RequireComponent(typeof(EventTriggerEx))]
public class BlockShapeDragger : MonoBehaviour, IBlockShapeDragger
{
[Header("拖拽设置")]
[SerializeField] private bool isDraggable = true;
[SerializeField] private float snapDistance = 0.5f;
[SerializeField] private BlockPuzzleGrid targetGridComponent; // Unity组件引用
[Header("视觉反馈")]
[SerializeField] private Color snapColor = new Color(0f, 1f, 0f, 0.7f);
[SerializeField] private Color invalidColor = new Color(1f, 0f, 0f, 0.7f);
[SerializeField] private Color normalColor = Color.white;
[SerializeField] private float dragScale = 1.1f;
// 拖拽状态
private DragState _currentState = DragState.Idle;
private bool _isDragging = false;
private Vector3 _dragOffset;
private Vector3 _originalPosition;
private Vector3 _originalScale;
private int _originalSortingOrder;
private bool _isSnapping = false;
private int _snapGridX;
private int _snapGridY;
// 组件引用
private BlockShape _blockShape;
private SpriteRenderer _spriteRenderer;
private Collider2D _collider2D;
// 事件
public event Action<IBlockShape> OnDragStart;
public event Action<IBlockShape, Vector3> OnDragUpdate;
public event Action<IBlockShape, bool> OnDragEnd;
public event Action<IBlockShape, Vector3, int, int> OnSnap;
public event Action<IBlockShape> OnUnsnap;
// 接口实现
public DragState CurrentState => _currentState;
public bool IsDraggable { get => isDraggable; set => isDraggable = value; }
public IBlockPuzzleGrid TargetGrid
{
get => targetGridComponent;
set => targetGridComponent = value as BlockPuzzleGrid;
}
public float SnapDistance { get => snapDistance; set => snapDistance = value; }
// 内部使用的网格引用
private IBlockPuzzleGrid targetGrid => targetGridComponent;
private void Awake()
{
_blockShape = GetComponent<BlockShape>();
_spriteRenderer = GetComponent<SpriteRenderer>();
}
public void StartDrag()
{
if (!isDraggable || _isDragging) return;
_isDragging = true;
_currentState = DragState.Dragging;
_originalPosition = transform.position;
_originalScale = transform.localScale;
// 计算拖拽偏移量
Vector3 mouseWorldPos = GetMouseWorldPosition();
_dragOffset = transform.position - mouseWorldPos;
// 提升层级和缩放
if (_spriteRenderer != null)
{
_originalSortingOrder = _spriteRenderer.sortingOrder;
_spriteRenderer.sortingOrder = 100;
}
transform.localScale = _originalScale * dragScale;
OnDragStart?.Invoke(_blockShape);
}
public void UpdateDrag()
{
if (!_isDragging) return;
Vector3 mouseWorldPos = GetMouseWorldPosition();
Vector3 targetPosition = mouseWorldPos + _dragOffset;
// 检测是否靠近网格
if (targetGrid != null)
{
bool canSnap = CheckSnapToGrid(targetPosition, out Vector3 snapPos, out int gridX, out int gridY);
if (canSnap)
{
// 吸附到网格
transform.position = snapPos;
_isSnapping = true;
_snapGridX = gridX;
_snapGridY = gridY;
_currentState = DragState.Snapping;
// 重要:更新拖拽偏移量,使鼠标继续移动时能正确计算位置
_dragOffset = snapPos - mouseWorldPos;
// 检查是否可以放置
bool canPlace = CanPlaceAt(gridX, gridY);
ShowSnapPreview(true, canPlace);
OnSnap?.Invoke(_blockShape, snapPos, gridX, gridY);
}
else
{
// 自由拖拽
transform.position = targetPosition;
if (_isSnapping)
{
_isSnapping = false;
ShowSnapPreview(false, false);
OnUnsnap?.Invoke(_blockShape);
}
_currentState = DragState.Dragging;
}
}
else
{
// 没有网格,自由拖拽
transform.position = targetPosition;
_currentState = DragState.Dragging;
}
OnDragUpdate?.Invoke(_blockShape, transform.position);
}
public void EndDrag()
{
if (!_isDragging) return;
bool placed = false;
if (_isSnapping && targetGrid != null)
{
// 尝试放置到网格
if (CanPlaceAt(_snapGridX, _snapGridY))
{
if (TryPlaceOnGrid(_snapGridX, _snapGridY))
{
placed = true;
_currentState = DragState.Placed;
OnShapePlaced();
}
}
}
if (!placed)
{
// 返回原位置
transform.position = _originalPosition;
_currentState = DragState.Idle;
}
// 恢复原始状态
transform.localScale = _originalScale;
if (_spriteRenderer != null)
{
_spriteRenderer.sortingOrder = _originalSortingOrder;
}
ShowSnapPreview(false, false);
_isDragging = false;
_isSnapping = false;
OnDragEnd?.Invoke(_blockShape, placed);
}
public void CancelDrag()
{
if (!_isDragging) return;
transform.position = _originalPosition;
transform.localScale = _originalScale;
if (_spriteRenderer != null)
{
_spriteRenderer.sortingOrder = _originalSortingOrder;
}
ShowSnapPreview(false, false);
_isDragging = false;
_isSnapping = false;
_currentState = DragState.Idle;
OnDragEnd?.Invoke(_blockShape, false);
}
public bool CanPlaceAt(int gridX, int gridY)
{
if (targetGrid == null || _blockShape == null) return false;
Vector2Int[] blockPositions = _blockShape.GetBlockPositions();
if (blockPositions == null || blockPositions.Length == 0) return false;
// 形状的坐标系统:
// - GetBlockPositions() 返回的坐标中,(0, 0) 是形状的左上角
// - gridX, gridY 是形状左下角在网格中的位置
// - 需要将形状的相对坐标转换为网格坐标
foreach (var blockPos in blockPositions)
{
// 将形状的相对坐标转换为网格坐标(左下角为基准)
// 形状的 (0, 0) 对应网格的 (gridX, gridY + shape.Height - 1)
// 形状的 (i, j) 对应网格的 (gridX + i, gridY + shape.Height - 1 - j)
int checkX = gridX + blockPos.x;
int checkY = gridY + _blockShape.Height - 1 - blockPos.y;
if (!targetGrid.IsValidPosition(checkX, checkY) ||
!targetGrid.IsCellEmpty(checkX, checkY))
{
return false;
}
}
return true;
}
public bool GetSnapGridPosition(out int gridX, out int gridY)
{
if (_isSnapping)
{
gridX = _snapGridX;
gridY = _snapGridY;
return true;
}
gridX = 0;
gridY = 0;
return false;
}
/// <summary>
/// 检查是否可以吸附到网格
/// </summary>
private bool CheckSnapToGrid(Vector3 worldPos, out Vector3 snapPos, out int gridX, out int gridY)
{
snapPos = worldPos;
gridX = 0;
gridY = 0;
if (targetGrid == null) return false;
// 将世界坐标转换为网格坐标
// 注意:WorldToGrid 需要世界坐标在网格范围内才能返回true
// 但我们需要检查所有附近的网格单元格,不仅仅是当前所在的单元格
// 所以先尝试直接转换,如果失败则查找最近的网格单元格
int tempGridX, tempGridY;
bool inGrid = targetGrid.WorldToGrid(worldPos, out tempGridX, out tempGridY);
if (!inGrid)
{
// 如果不在网格内,尝试查找最近的网格单元格
// 计算最近的网格坐标
Vector3 localPos = worldPos - targetGrid.Origin;
tempGridX = Mathf.FloorToInt(localPos.x / targetGrid.CellSize);
tempGridY = Mathf.FloorToInt(localPos.y / targetGrid.CellSize);
// 检查是否在有效范围内
if (!targetGrid.IsValidPosition(tempGridX, tempGridY))
{
return false;
}
}
// 获取网格单元格中心的世界坐标
Vector3 gridCenter = targetGrid.GridToWorld(tempGridX, tempGridY);
// 计算距离
float distance = Vector3.Distance(worldPos, gridCenter);
if (distance <= snapDistance)
{
// 计算形状应该放置的网格位置
// 形状左下角对齐到网格单元格左下角
gridX = tempGridX;
gridY = tempGridY;
// 计算形状左下角的世界坐标
// 网格单元格左下角的世界坐标 = Origin + (gridX * CellSize, gridY * CellSize)
Vector3 gridBottomLeft = targetGrid.Origin + new Vector3(
gridX * targetGrid.CellSize,
gridY * targetGrid.CellSize,
0
);
// 计算形状中心相对于形状左下角的偏移
// 形状的 pattern 中,(0, 0) 是左上角,所以形状左下角在 pattern 中的相对位置是 (0, height-1)
// 在形状的本地坐标系中(假设形状中心在 (0, 0)),形状左下角的位置是:
// localX = -width * cellSize / 2
// localY = -height * cellSize / 2 + (height-1) * cellSize = (height-1) * cellSize - height * cellSize / 2
// 形状中心相对于形状左下角的偏移 = (0, 0) - (localX, localY) = (width * cellSize / 2, height * cellSize / 2 - (height-1) * cellSize)
// 为了简化,我们假设形状的中心在形状的几何中心
// 形状中心相对于形状左下角的偏移 = (width * cellSize / 2, height * cellSize / 2)
// 但考虑到形状的 pattern 中 (0, 0) 是左上角,形状左下角在 pattern 中的位置是 (0, height-1)
// 所以形状中心相对于形状左下角的偏移 = (width * cellSize / 2, (height-1) * cellSize / 2)
// 实际上,我们需要根据形状的实际布局计算形状中心的位置
// 但为了简化,我们直接使用网格单元格左下角作为形状左下角的位置
// 然后根据形状的实际布局计算形状中心的位置
// 形状左下角对齐到网格单元格左下角
// 形状中心的世界坐标 = 网格单元格左下角的世界坐标 + 形状中心相对于形状左下角的偏移
// 使用网格的 CellSize,因为形状的 cellSize 应该与网格的 CellSize 相同
float shapeCenterOffsetX = _blockShape.Width * targetGrid.CellSize / 2f;
float shapeCenterOffsetY = (_blockShape.Height - 1) * targetGrid.CellSize / 2f;
snapPos = gridBottomLeft + new Vector3(shapeCenterOffsetX, shapeCenterOffsetY, 0);
return true;
}
return false;
}
/// <summary>
/// 尝试放置到网格
/// </summary>
/// <param name="gridX">网格X坐标(形状左下角位置)</param>
/// <param name="gridY">网格Y坐标(形状左下角位置)</param>
private bool TryPlaceOnGrid(int gridX, int gridY)
{
if (!CanPlaceAt(gridX, gridY)) return false;
Vector2Int[] blockPositions = _blockShape.GetBlockPositions();
if (blockPositions == null) return false;
// 形状的坐标系统:
// - GetBlockPositions() 返回的坐标中,(0, 0) 是形状的左上角
// - gridX, gridY 是形状左下角在网格中的位置
// - 需要将形状的相对坐标转换为网格坐标
foreach (var blockPos in blockPositions)
{
// 将形状的相对坐标转换为网格坐标(左下角为基准)
// 形状的 (0, 0) 对应网格的 (gridX, gridY + shape.Height - 1)
// 形状的 (i, j) 对应网格的 (gridX + i, gridY + shape.Height - 1 - j)
int x = gridX + blockPos.x;
int y = gridY + _blockShape.Height - 1 - blockPos.y;
targetGrid.SetCellState(x, y, GridCellState.Occupied);
}
return true;
}
/// <summary>
/// 显示吸附预览效果
/// </summary>
private void ShowSnapPreview(bool show, bool isValid)
{
Color previewColor = show ? (isValid ? snapColor : invalidColor) : normalColor;
// 更新主SpriteRenderer
if (_spriteRenderer != null)
{
_spriteRenderer.color = previewColor;
}
}
/// <summary>
/// 形状放置完成
/// </summary>
private void OnShapePlaced()
{
// 可以在这里添加放置后的逻辑
// 例如:禁用拖拽、播放动画等
isDraggable = false;
}
/// <summary>
/// 获取鼠标世界坐标
/// </summary>
private Vector3 GetMouseWorldPosition()
{
return CameraKit.GetMouseWorldPos(Input.mousePosition);
}
private void OnDisable()
{
if (_isDragging)
{
CancelDrag();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ad01079e32d2314eb3719ead5f50ddb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -135,8 +135,8 @@ namespace AibisDream
for (int x = 0; x <= _grid.Width; x++)
{
CreateLine(
_grid.GridToWorld(x, 0),
_grid.GridToWorld(x, _grid.Height),
_grid.GridVerticesToWorld(x, 0)[0],
_grid.GridVerticesToWorld(x, _grid.Height)[0],
gridLineWidth,
gridLineColor
);
@@ -145,8 +145,8 @@ namespace AibisDream
for (int y = 0; y <= _grid.Height; y++)
{
CreateLine(
_grid.GridToWorld(0, y),
_grid.GridToWorld(_grid.Width, y),
_grid.GridVerticesToWorld(0, y)[0],
_grid.GridVerticesToWorld(_grid.Width, y)[0],
gridLineWidth,
gridLineColor
);
@@ -0,0 +1,112 @@
using System.Collections.Generic;
using UnityEngine;
namespace AibisDream
{
/// <summary>
/// 放置验证器实现
/// 负责验证形状是否可以放置在指定位置
/// </summary>
public class PlacementValidator : IPlacementValidator
{
/// <summary>
/// 检查形状是否可以放置在指定位置
/// </summary>
/// <param name="grid">网格系统</param>
/// <param name="shape">要放置的形状</param>
/// <param name="gridX">网格X坐标(形状左下角位置)</param>
/// <param name="gridY">网格Y坐标(形状左下角位置)</param>
/// <returns>放置验证结果</returns>
public 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数组中)
// - pattern[i, j] 中,i 是 x(从左到右),j 是 y(从上到下,j=0 是顶部)
// - gridX, gridY 是形状左下角在网格中的位置
// - 需要将形状的相对坐标转换为网格坐标
foreach (var blockPos in blockPositions)
{
// 将形状的相对坐标转换为网格坐标
// 形状的 (0, 0) 对应网格的 (gridX, gridY + shape.Height - 1)
// 形状的 (i, j) 对应网格的 (gridX + i, gridY + shape.Height - 1 - j)
int gridCellX = gridX + blockPos.x;
int gridCellY = gridY + shape.Height - 1 - 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 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 (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)
{
// 将形状的相对坐标转换为网格坐标(左下角为基准)
int gridCellX = gridX + blockPos.x;
int gridCellY = gridY + shape.Height - 1 - blockPos.y;
occupiedCells.Add((gridCellX, gridCellY));
}
return occupiedCells.ToArray();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 826c5312f504d944c924549de95babe7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: abf557018657fa642a0d79187ed450eb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -117,6 +117,12 @@ namespace AibisDream
/// </summary>
/// <returns>总单元格数量</returns>
int GetTotalCellCount();
/// <summary>
/// 获取网格顶点信息
/// </summary>
/// <returns>网格四个顶点位置</returns>
Vector3[] GridVerticesToWorld(int x, int y);
}
}
@@ -100,7 +100,7 @@ namespace AibisDream
/// 创建形状的副本
/// </summary>
/// <returns>形状副本</returns>
IBlockShape Clone();
// IBlockShape Clone();
}
}
@@ -0,0 +1,105 @@
using UnityEngine;
using System;
namespace AibisDream
{
/// <summary>
/// 拖拽状态
/// </summary>
public enum DragState
{
Idle, // 空闲
Dragging, // 拖拽中
Snapping, // 吸附中
Placed // 已放置
}
/// <summary>
/// 形状拖拽接口
/// 负责处理形状的拖拽、吸附和放置逻辑
/// </summary>
public interface IBlockShapeDragger
{
/// <summary>
/// 当前拖拽状态
/// </summary>
DragState CurrentState { get; }
/// <summary>
/// 是否可拖拽
/// </summary>
bool IsDraggable { get; set; }
/// <summary>
/// 目标网格
/// </summary>
IBlockPuzzleGrid TargetGrid { get; set; }
/// <summary>
/// 吸附距离阈值
/// </summary>
float SnapDistance { get; set; }
/// <summary>
/// 拖拽开始事件
/// </summary>
event Action<IBlockShape> OnDragStart;
/// <summary>
/// 拖拽更新事件
/// </summary>
event Action<IBlockShape, Vector3> OnDragUpdate;
/// <summary>
/// 拖拽结束事件
/// </summary>
event Action<IBlockShape, bool> OnDragEnd; // bool表示是否成功放置
/// <summary>
/// 吸附事件
/// </summary>
event Action<IBlockShape, Vector3, int, int> OnSnap; // 形状, 位置, 网格X, 网格Y
/// <summary>
/// 取消吸附事件
/// </summary>
event Action<IBlockShape> OnUnsnap;
/// <summary>
/// 开始拖拽
/// </summary>
void StartDrag();
/// <summary>
/// 更新拖拽位置
/// </summary>
void UpdateDrag();
/// <summary>
/// 结束拖拽
/// </summary>
void EndDrag();
/// <summary>
/// 取消拖拽(返回原位置)
/// </summary>
void CancelDrag();
/// <summary>
/// 检查是否可以放置到指定位置
/// </summary>
/// <param name="gridX">网格X坐标</param>
/// <param name="gridY">网格Y坐标</param>
/// <returns>是否可以放置</returns>
bool CanPlaceAt(int gridX, int gridY);
/// <summary>
/// 获取当前吸附的网格坐标
/// </summary>
/// <param name="gridX">输出的网格X坐标</param>
/// <param name="gridY">输出的网格Y坐标</param>
/// <returns>是否正在吸附</returns>
bool GetSnapGridPosition(out int gridX, out int gridY);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5b2cbf04f1d9ef6448d01501a7861a46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -22,8 +22,8 @@ namespace AibisDream
/// </summary>
/// <param name="grid">网格系统</param>
/// <param name="shape">要放置的形状</param>
/// <param name="gridX">网格X坐标(形状左角位置)</param>
/// <param name="gridY">网格Y坐标(形状左角位置)</param>
/// <param name="gridX">网格X坐标(形状左角位置)</param>
/// <param name="gridY">网格Y坐标(形状左角位置)</param>
/// <returns>放置验证结果</returns>
PlacementResult CanPlace(IBlockPuzzleGrid grid, IBlockShape shape, int gridX, int gridY);
@@ -32,8 +32,8 @@ namespace AibisDream
/// </summary>
/// <param name="grid">网格系统</param>
/// <param name="shape">要放置的形状</param>
/// <param name="gridX">网格X坐标(形状左角位置)</param>
/// <param name="gridY">网格Y坐标(形状左角位置)</param>
/// <param name="gridX">网格X坐标(形状左角位置)</param>
/// <param name="gridY">网格Y坐标(形状左角位置)</param>
/// <param name="failureReason">失败原因(如果返回false</param>
/// <returns>是否可以放置</returns>
bool CanPlaceDetailed(IBlockPuzzleGrid grid, IBlockShape shape, int gridX, int gridY, out PlacementResult failureReason);
@@ -42,10 +42,9 @@ namespace AibisDream
/// 获取形状在指定位置的所有占用格子坐标
/// </summary>
/// <param name="shape">形状</param>
/// <param name="gridX">网格X坐标(形状左角位置)</param>
/// <param name="gridY">网格Y坐标(形状左角位置)</param>
/// <param name="gridX">网格X坐标(形状左角位置)</param>
/// <param name="gridY">网格Y坐标(形状左角位置)</param>
/// <returns>占用格子的网格坐标数组</returns>
(int x, int y)[] GetOccupiedCells(IBlockShape shape, int gridX, int gridY);
}
}