Files
aibis-dream/Assets/Scripts/MiniGame/BlockPuzzle/Class/BlockPuzzleKit.cs
T

717 lines
24 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using AibisDream.Utility;
using AibisDream.Framework;
using AibisDream.FixSystem;
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 (JsonUtil.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 (!JsonUtil.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>
/// 批量从文件加载 BlockShapeData
/// </summary>
/// <param name="keys">要加载的数据键数组</param>
/// <param name="path">文件路径,如果为空则使用默认路径</param>
/// <returns>成功加载的数据列表</returns>
public static Dictionary<string, BlockShapeData> LoadBlockShapeDataBatch(string[] keys, string path = null)
{
var result = new Dictionary<string, BlockShapeData>();
if (keys == null || keys.Length == 0)
{
return result;
}
if (string.IsNullOrEmpty(path))
{
path = ConstRef.BlockShapeDataPath;
}
try
{
if (!JsonUtil.Exists(path))
{
Debug.LogWarning($"BlockShapeData 文件不存在: {path}");
return result;
}
var dict = JsonUtil.ReadBeanDict<BlockShapeData>(path);
var infoDict = JsonUtil.ReadBeanDict<BlockShapeInfo>(ConstRef.BlockShapeInfoPath);
if (dict == null)
{
Debug.LogWarning($"读取 BlockShapeData 字典失败: {path}");
return result;
}
foreach (string key in keys)
{
if (string.IsNullOrEmpty(key))
continue;
if (dict.ContainsKey(key))
{
BlockShapeData data = dict[key];
// 尝试加载对应的 BlockShapeInfo
if (infoDict != null && infoDict.ContainsKey(key))
{
data.blockShapeInfo = infoDict[key];
}
result.Add(key, data);
}
else
{
Debug.LogWarning($"未找到键为 '{key}' 的 BlockShapeData");
}
}
}
catch (Exception e)
{
Debug.LogError($"批量加载 BlockShapeData 失败: {e.Message}");
}
return result;
}
/// <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 (!JsonUtil.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 (JsonUtil.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
{
private static BlockPuzzleSystem BlockPuzzleSystem => FixSystemCenter.SystemDic.Get<BlockPuzzleSystem>();
[YarnCommand("init_block_puzzle")]
public static void InitBlockPuzzle(string puzzleName)
{
if (BlockPuzzleKit.TryLoadBlockPuzzleData(puzzleName, out var blockPuzzleData))
{
BlockPuzzleSystem.Initialize(blockPuzzleData);
}
else
{
Debug.LogError($"加载 BlockPuzzleData 失败: {puzzleName}");
}
}
[YarnCommand("load_shape_rack")]
public static void LoadShapeRack()
{
BlockPuzzleSystem.LoadShapeRack();
}
[YarnCommand("unlock_block_puzzle_system")]
public static IEnumerator UnlockSystem()
{
yield return BlockPuzzleSystem.UnlockSystem();
}
[YarnCommand("lock_block_puzzle_system")]
public static IEnumerator LockSystem()
{
yield return BlockPuzzleSystem.LockSystem();
}
[YarnCommand("enable_drag")]
public static void EnableDrag()
{
BlockPuzzleSystem.EnableDrag();
}
[YarnCommand("clear_block_puzzle")]
public static void ClearBlockPuzzle()
{
BlockPuzzleSystem.ClearGame();
}
}
[Serializable]
public struct BlockPuzzleData
{
public string puzzleName;
public int gridWidth;
public int gridHeight;
public float cellSize;
// 网格内Shape
public GridShapeData[] gridShapeDataInfos;
public string[] shapeInRack;
// 回收时不添加 DeviceButton 的 Shape 名称列表
public string[] recycleNoButtonShapes;
// 验证器
public string validatorName;
}
[Serializable]
public struct GridShapeData
{
public string shapeKey;
public Vector2Int rootCell;
public RotationAngle rotationAngle;
}
[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 string detailImagePath;
public Sprite GetButtonImage()
{
return ResourceKit.LoadAssetSync<Sprite>(buttonImagePath);
}
public Sprite GetDetailImage()
{
return ResourceKit.LoadAssetSync<Sprite>(detailImagePath);
}
}
[Serializable]
public struct BlockPuzzleParamValue
{
public int power;
public int noise;
public int heat;
}
}