620 lines
21 KiB
C#
620 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using Newtonsoft.Json;
|
|
using AibisDream.Utility;
|
|
using AibisDream.Framework;
|
|
using UnityEngine;
|
|
using Yarn.Unity;
|
|
|
|
namespace AibisDream
|
|
{
|
|
public static class BlockPuzzleKit
|
|
{
|
|
/// <summary>
|
|
/// 保存单个 BlockShapeData 到文件,确保 Key 唯一(如果 Key 已存在则更新,不存在则添加)
|
|
/// </summary>
|
|
/// <param name="data">要保存的数据</param>
|
|
/// <param name="key">数据的键(用于字典存储)</param>
|
|
/// <param name="path">保存路径,如果为空则使用默认路径</param>
|
|
public static void SaveBlockShapeData(BlockShapeData data, string key, string path = null)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
path = ConstRef.BlockShapeDataPath;
|
|
}
|
|
|
|
// 确保目录存在
|
|
string directory = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
// 读取现有字典(如果文件存在)
|
|
Dictionary<string, BlockShapeData> dataDict;
|
|
if (File.Exists(path))
|
|
{
|
|
try
|
|
{
|
|
dataDict = JsonUtil.ReadBeanDict<BlockShapeData>(path) ?? new Dictionary<string, BlockShapeData>();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"读取现有 BlockShapeData 文件失败,将创建新文件: {e.Message}");
|
|
dataDict = new Dictionary<string, BlockShapeData>();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dataDict = new Dictionary<string, BlockShapeData>();
|
|
}
|
|
|
|
// 更新或添加数据(确保 Key 唯一)
|
|
dataDict[key] = data;
|
|
|
|
// 保存整个字典
|
|
JsonUtil.SaveBean(dataDict, path);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 从文件加载单个 BlockShapeData
|
|
/// </summary>
|
|
/// <param name="key">数据的键</param>
|
|
/// <param name="path">文件路径,如果为空则使用默认路径</param>
|
|
/// <param name="data">加载的数据</param>
|
|
/// <returns>是否加载成功</returns>
|
|
public static bool TryLoadBlockShapeData(string key, out BlockShapeData data, string path = null)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
path = ConstRef.BlockShapeDataPath;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
data = default;
|
|
return false;
|
|
}
|
|
|
|
var dict = JsonUtil.ReadBeanDict<BlockShapeData>(path);
|
|
var infoDict = JsonUtil.ReadBeanDict<BlockShapeInfo>(ConstRef.BlockShapeInfoPath);
|
|
if (dict != null && dict.ContainsKey(key))
|
|
{
|
|
data = dict[key];
|
|
data.blockShapeInfo = infoDict[key];
|
|
return true;
|
|
}
|
|
|
|
data = default;
|
|
return false;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"加载 BlockShapeData 失败: {e.Message}");
|
|
data = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从文件加载单个 BlockPuzzleData
|
|
/// </summary>
|
|
/// <param name="key">数据的键</param>
|
|
/// <param name="data">加载的数据</param>
|
|
/// <returns>是否加载成功</returns>
|
|
public static bool TryLoadBlockPuzzleData(string key, out BlockPuzzleData data)
|
|
{
|
|
var path = ConstRef.BlockPuzzleDataPath;
|
|
// 加载配置
|
|
try
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
data = default;
|
|
return false;
|
|
}
|
|
|
|
var dict = JsonUtil.ReadBeanDict<BlockPuzzleData>(path);
|
|
if (dict != null && dict.ContainsKey(key))
|
|
{
|
|
data = dict[key];
|
|
return true;
|
|
}
|
|
|
|
data = default;
|
|
return false;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"加载 BlockPuzzleData 失败: {e.Message}");
|
|
data = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 保存单个 BlockPuzzleData 到文件,确保 Key 唯一(如果 Key 已存在则更新,不存在则添加)
|
|
/// </summary>
|
|
/// <param name="data">要保存的数据</param>
|
|
/// <param name="key">数据的键(用于字典存储)</param>
|
|
/// <param name="path">保存路径,如果为空则使用默认路径</param>
|
|
public static void SaveBlockPuzzleData(BlockPuzzleData data, string key, string path = null)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
path = ConstRef.BlockPuzzleDataPath;
|
|
}
|
|
|
|
// 确保目录存在
|
|
string directory = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
// 读取现有字典(如果文件存在)
|
|
Dictionary<string, BlockPuzzleData> dataDict;
|
|
if (File.Exists(path))
|
|
{
|
|
try
|
|
{
|
|
dataDict = JsonUtil.ReadBeanDict<BlockPuzzleData>(path) ?? new Dictionary<string, BlockPuzzleData>();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"读取现有 BlockPuzzleData 文件失败,将创建新文件: {e.Message}");
|
|
dataDict = new Dictionary<string, BlockPuzzleData>();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dataDict = new Dictionary<string, BlockPuzzleData>();
|
|
}
|
|
|
|
// 更新或添加数据(确保 Key 唯一)
|
|
dataDict[key] = data;
|
|
|
|
// 保存整个字典
|
|
JsonUtil.SaveBean(dataDict, path);
|
|
}
|
|
|
|
// ---------------------- 碰撞箱计算 ----------------------
|
|
/// <summary>
|
|
/// 将网格数组转换为多边形顶点数组
|
|
/// </summary>
|
|
/// <param name="grid">二维数组,值为1的格子表示填充区域</param>
|
|
/// <returns>按顺序排列的多边形顶点数组,可以围成闭合多边形</returns>
|
|
public static Vector2[] GridToPolygonVertices(int[,] grid)
|
|
{
|
|
if (grid == null || grid.GetLength(0) == 0 || grid.GetLength(1) == 0)
|
|
return new Vector2[0];
|
|
|
|
int width = grid.GetLength(0);
|
|
int height = grid.GetLength(1);
|
|
|
|
// 查找起始边界点
|
|
Vector2Int? startPoint = FindStartBoundaryPoint(grid, width, height);
|
|
if (!startPoint.HasValue)
|
|
return new Vector2[0];
|
|
|
|
// 边界追踪
|
|
List<Vector2> vertices = TraceBoundary(grid, width, height, startPoint.Value);
|
|
|
|
if (vertices.Count == 0)
|
|
return new Vector2[0];
|
|
|
|
// 顶点优化:移除共线点
|
|
vertices = OptimizeVertices(vertices);
|
|
|
|
return vertices.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 查找起始边界点
|
|
/// 找到第一个值为1的格子,确定其边界上的起始追踪点
|
|
/// </summary>
|
|
private static Vector2Int? FindStartBoundaryPoint(int[,] grid, int width, int height)
|
|
{
|
|
// 从左下角开始,按行扫描,找到第一个值为1的格子
|
|
for (int j = 0; j < height; j++)
|
|
{
|
|
for (int i = 0; i < width; i++)
|
|
{
|
|
if (grid[i, j] == 1)
|
|
{
|
|
// 找到第一个填充格子,检查其下边界(最可能的外边界)
|
|
// 如果下边界是外边界,返回左下角点
|
|
if (j == 0 || (j > 0 && grid[i, j - 1] == 0))
|
|
{
|
|
// 返回格子(i,j)的左下角点坐标
|
|
return new Vector2Int(i, j);
|
|
}
|
|
// 否则检查左边界
|
|
else if (i == 0 || (i > 0 && grid[i - 1, j] == 0))
|
|
{
|
|
return new Vector2Int(i, j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 边界追踪:从起始点开始,沿着边界顺时针追踪,记录所有边界顶点
|
|
/// 使用边界边收集和连接的方法
|
|
/// 边长固定为2,所有坐标使用整数
|
|
/// </summary>
|
|
private static List<Vector2> TraceBoundary(int[,] grid, int width, int height, Vector2Int startCell)
|
|
{
|
|
// 收集所有边界边:每条边用起点和终点表示(整数坐标)
|
|
List<(Vector2Int start, Vector2Int end)> boundaryEdges = CollectBoundaryEdges(grid, width, height);
|
|
|
|
if (boundaryEdges.Count == 0)
|
|
return new List<Vector2>();
|
|
|
|
// 构建顶点邻接表(直接使用整数坐标)
|
|
Dictionary<Vector2Int, List<Vector2Int>> adjacency = BuildAdjacencyList(boundaryEdges);
|
|
|
|
if (adjacency.Count == 0)
|
|
return new List<Vector2>();
|
|
|
|
// 找到起始顶点(最左下角的顶点)
|
|
Vector2Int startVertex = FindStartVertex(adjacency);
|
|
|
|
// 沿着邻接表追踪,形成有序顶点序列
|
|
List<Vector2> vertices = TraceVertices(adjacency, startVertex);
|
|
|
|
return vertices;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 收集所有边界边
|
|
/// 坐标系统:格子(i,j)的中心为(i*2, j*2),边长为2
|
|
/// 格子(i,j)的四个角点为:(i*2-1, j*2-1), (i*2+1, j*2-1), (i*2+1, j*2+1), (i*2-1, j*2+1)
|
|
/// </summary>
|
|
private static List<(Vector2Int start, Vector2Int end)> CollectBoundaryEdges(int[,] grid, int width, int height)
|
|
{
|
|
List<(Vector2Int, Vector2Int)> edges = new List<(Vector2Int, Vector2Int)>();
|
|
|
|
for (int i = 0; i < width; i++)
|
|
{
|
|
for (int j = 0; j < height; j++)
|
|
{
|
|
if (grid[i, j] != 1)
|
|
continue;
|
|
|
|
// 计算格子(i,j)的四个角点坐标(整数)
|
|
int left = i * 2 - 1;
|
|
int right = i * 2 + 1;
|
|
int bottom = j * 2 - 1;
|
|
int top = j * 2 + 1;
|
|
|
|
// 检查四个方向的边界
|
|
// 下边界(从左下角到右下角)
|
|
if (j == 0 || (j > 0 && grid[i, j - 1] == 0))
|
|
{
|
|
edges.Add((new Vector2Int(left, bottom), new Vector2Int(right, bottom)));
|
|
}
|
|
|
|
// 右边界(从右下角到右上角)
|
|
if (i == width - 1 || (i < width - 1 && grid[i + 1, j] == 0))
|
|
{
|
|
edges.Add((new Vector2Int(right, bottom), new Vector2Int(right, top)));
|
|
}
|
|
|
|
// 上边界(从右上角到左上角)
|
|
if (j == height - 1 || (j < height - 1 && grid[i, j + 1] == 0))
|
|
{
|
|
edges.Add((new Vector2Int(right, top), new Vector2Int(left, top)));
|
|
}
|
|
|
|
// 左边界(从左上角到左下角)
|
|
if (i == 0 || (i > 0 && grid[i - 1, j] == 0))
|
|
{
|
|
edges.Add((new Vector2Int(left, top), new Vector2Int(left, bottom)));
|
|
}
|
|
}
|
|
}
|
|
|
|
return edges;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建顶点邻接表
|
|
/// 直接使用整数坐标,无需量化
|
|
/// </summary>
|
|
private static Dictionary<Vector2Int, List<Vector2Int>> BuildAdjacencyList(List<(Vector2Int start, Vector2Int end)> edges)
|
|
{
|
|
Dictionary<Vector2Int, List<Vector2Int>> adjacency = new Dictionary<Vector2Int, List<Vector2Int>>();
|
|
|
|
foreach (var edge in edges)
|
|
{
|
|
Vector2Int key1 = edge.start;
|
|
Vector2Int key2 = edge.end;
|
|
|
|
if (!adjacency.ContainsKey(key1))
|
|
adjacency[key1] = new List<Vector2Int>();
|
|
if (!adjacency.ContainsKey(key2))
|
|
adjacency[key2] = new List<Vector2Int>();
|
|
|
|
if (!adjacency[key1].Contains(key2))
|
|
adjacency[key1].Add(key2);
|
|
if (!adjacency[key2].Contains(key1))
|
|
adjacency[key2].Add(key1);
|
|
}
|
|
|
|
return adjacency;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 找到起始顶点(最左下角的顶点)
|
|
/// </summary>
|
|
private static Vector2Int FindStartVertex(Dictionary<Vector2Int, List<Vector2Int>> adjacency)
|
|
{
|
|
Vector2Int start = new Vector2Int(int.MaxValue, int.MaxValue);
|
|
|
|
foreach (var key in adjacency.Keys)
|
|
{
|
|
if (key.y < start.y || (key.y == start.y && key.x < start.x))
|
|
{
|
|
start = key;
|
|
}
|
|
}
|
|
|
|
return start;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 沿着邻接表追踪顶点,形成有序序列(顺时针)
|
|
/// 直接使用整数坐标,最后转换为Vector2返回
|
|
/// </summary>
|
|
private static List<Vector2> TraceVertices(Dictionary<Vector2Int, List<Vector2Int>> adjacency, Vector2Int start)
|
|
{
|
|
List<Vector2> vertices = new List<Vector2>();
|
|
|
|
Vector2Int current = start;
|
|
Vector2Int? previous = null;
|
|
|
|
do
|
|
{
|
|
// 将整数坐标转换为Vector2
|
|
Vector2 currentPos = new Vector2(current.x, current.y);
|
|
|
|
// 避免重复添加相同顶点(除非是起点)
|
|
if (vertices.Count == 0 || vertices[vertices.Count - 1] != currentPos)
|
|
{
|
|
vertices.Add(currentPos);
|
|
}
|
|
|
|
// 找到下一个顶点:选择使方向变化最小的邻居(顺时针)
|
|
List<Vector2Int> neighbors = adjacency[current];
|
|
Vector2Int? next = null;
|
|
|
|
if (neighbors.Count == 1)
|
|
{
|
|
// 只有一个邻居
|
|
next = neighbors[0];
|
|
}
|
|
else if (neighbors.Count == 2)
|
|
{
|
|
// 有两个邻居,选择不是前一个的那个
|
|
foreach (var neighbor in neighbors)
|
|
{
|
|
if (!previous.HasValue || neighbor != previous.Value)
|
|
{
|
|
next = neighbor;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 多个邻居,选择角度最小的(顺时针方向)
|
|
Vector2 currentVec = new Vector2(current.x, current.y);
|
|
Vector2 direction;
|
|
|
|
if (previous.HasValue)
|
|
{
|
|
Vector2 prevVec = new Vector2(previous.Value.x, previous.Value.y);
|
|
direction = (currentVec - prevVec).normalized;
|
|
}
|
|
else
|
|
{
|
|
// 起始点,选择最下方的邻居(顺时针起始方向)
|
|
direction = new Vector2(1, 0); // 向右
|
|
}
|
|
|
|
float bestAngle = float.MaxValue;
|
|
foreach (var neighbor in neighbors)
|
|
{
|
|
if (previous.HasValue && neighbor == previous.Value)
|
|
continue;
|
|
|
|
Vector2 neighborVec = new Vector2(neighbor.x, neighbor.y);
|
|
Vector2 toNeighbor = (neighborVec - currentVec).normalized;
|
|
|
|
// 计算从当前方向到邻居方向的角度(顺时针)
|
|
float angle = GetClockwiseAngle(direction, toNeighbor);
|
|
if (angle < bestAngle)
|
|
{
|
|
bestAngle = angle;
|
|
next = neighbor;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!next.HasValue)
|
|
break;
|
|
|
|
previous = current;
|
|
current = next.Value;
|
|
|
|
} while (current != start && vertices.Count < adjacency.Count * 2);
|
|
|
|
return vertices;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算从向量a到向量b的顺时针角度(0到2π)
|
|
/// </summary>
|
|
private static float GetClockwiseAngle(Vector2 a, Vector2 b)
|
|
{
|
|
float angleA = Mathf.Atan2(a.y, a.x);
|
|
float angleB = Mathf.Atan2(b.y, b.x);
|
|
float angle = angleB - angleA;
|
|
|
|
// 转换为0到2π范围
|
|
if (angle < 0)
|
|
angle += 2 * Mathf.PI;
|
|
|
|
return angle;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 优化顶点:移除共线的顶点,保留关键转折点
|
|
/// </summary>
|
|
private static List<Vector2> OptimizeVertices(List<Vector2> vertices)
|
|
{
|
|
if (vertices.Count <= 3)
|
|
return vertices;
|
|
|
|
List<Vector2> optimized = new List<Vector2>();
|
|
const float epsilon = 0.0001f; // 浮点数比较阈值
|
|
|
|
for (int i = 0; i < vertices.Count; i++)
|
|
{
|
|
Vector2 prev = vertices[(i - 1 + vertices.Count) % vertices.Count];
|
|
Vector2 current = vertices[i];
|
|
Vector2 next = vertices[(i + 1) % vertices.Count];
|
|
|
|
// 计算两个向量
|
|
Vector2 v1 = current - prev;
|
|
Vector2 v2 = next - current;
|
|
|
|
// 检查是否共线(叉积接近0)且方向相同(点积大于0)
|
|
float crossProduct = v1.x * v2.y - v1.y * v2.x;
|
|
float dotProduct = Vector2.Dot(v1.normalized, v2.normalized);
|
|
|
|
// 如果共线且方向相同,跳过当前点
|
|
if (Mathf.Abs(crossProduct) < epsilon && dotProduct > 0.99f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
optimized.Add(current);
|
|
}
|
|
|
|
return optimized;
|
|
}
|
|
}
|
|
|
|
public static class BlockPuzzleYarnCommand
|
|
{
|
|
[YarnCommand("init_block_puzzle")]
|
|
public static void InitBlockPuzzle(string puzzleName)
|
|
{
|
|
if (BlockPuzzleKit.TryLoadBlockPuzzleData(puzzleName, out var blockPuzzleData))
|
|
{
|
|
BlockPuzzleSystem.Instance.Initialize(blockPuzzleData);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError($"加载 BlockPuzzleData 失败: {puzzleName}");
|
|
}
|
|
}
|
|
|
|
[YarnCommand("clear_block_puzzle")]
|
|
public static void ClearBlockPuzzle()
|
|
{
|
|
BlockPuzzleSystem.Instance.ClearGame();
|
|
}
|
|
|
|
// [YarnCommand("reset_block_puzzle")]
|
|
// public static void ResetBlockPuzzle()
|
|
// {
|
|
// BlockPuzzleSystem.Instance.ResetGame();
|
|
// }
|
|
}
|
|
|
|
[Serializable]
|
|
public struct BlockPuzzleData
|
|
{
|
|
public string puzzleName;
|
|
public int gridWidth;
|
|
public int gridHeight;
|
|
public float cellSize;
|
|
// 网格内Shape
|
|
public GridShapeData[] gridShapeDataInfos;
|
|
public string[] shapeInRack;
|
|
// 验证器
|
|
public string validatorName;
|
|
}
|
|
|
|
[Serializable]
|
|
public struct GridShapeData
|
|
{
|
|
public string shapeKey;
|
|
public Vector2Int rootCell;
|
|
public RotationAngle rotationAngle;
|
|
public bool isDraggable;
|
|
}
|
|
|
|
[Serializable]
|
|
public struct BlockShapeData
|
|
{
|
|
public int shapeId;
|
|
public string shapeName;
|
|
public int width;
|
|
public int height;
|
|
public float cellSize;
|
|
public string originalPatternStr;
|
|
public string prefabPath;
|
|
|
|
[Header("额外限制")]
|
|
public bool extraLimits;
|
|
public Vector2Int rootLimit;
|
|
public RotationAngle rotationAngleLimit;
|
|
|
|
[JsonIgnore]
|
|
[HideInInspector]
|
|
public BlockShapeInfo blockShapeInfo;
|
|
|
|
public GameObject GetVisualPrefab()
|
|
{
|
|
if (string.IsNullOrEmpty(prefabPath)) return null;
|
|
return ResourceKit.LoadAssetSync<GameObject>(prefabPath);
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public struct BlockShapeInfo
|
|
{
|
|
public string shapeName;
|
|
|
|
public BlockPuzzleParamValue paramValue;
|
|
public string shapeDescription;
|
|
|
|
public string[] states;
|
|
|
|
public string buttonImagePath;
|
|
|
|
public Sprite GetButtonImage()
|
|
{
|
|
return ResourceKit.LoadAssetSync<Sprite>(buttonImagePath);
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public struct BlockPuzzleParamValue
|
|
{
|
|
public int power;
|
|
public int noise;
|
|
public int heat;
|
|
}
|
|
} |