fix:1、多语言优化。2、添加死局检查脚本
This commit is contained in:
Binary file not shown.
+79
-79
@@ -195,9 +195,9 @@ public class WebViewObject : MonoBehaviour
|
||||
case "SetKeyboardVisible":
|
||||
SetKeyboardVisible(s.Substring(i + 1));
|
||||
break;
|
||||
case "RequestFileChooserPermissions":
|
||||
RequestFileChooserPermissions();
|
||||
break;
|
||||
// case "RequestFileChooserPermissions":
|
||||
// // RequestFileChooserPermissions();
|
||||
// break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,82 +219,82 @@ public class WebViewObject : MonoBehaviour
|
||||
}
|
||||
|
||||
/// Called from Java native plugin to request permissions for the file chooser.
|
||||
public void RequestFileChooserPermissions()
|
||||
{
|
||||
var permissions = new List<string>();
|
||||
using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
|
||||
{
|
||||
if (version.GetStatic<int>("SDK_INT") >= 33)
|
||||
{
|
||||
if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_IMAGES"))
|
||||
{
|
||||
permissions.Add("android.permission.READ_MEDIA_IMAGES");
|
||||
}
|
||||
if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_VIDEO"))
|
||||
{
|
||||
permissions.Add("android.permission.READ_MEDIA_VIDEO");
|
||||
}
|
||||
if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_AUDIO"))
|
||||
{
|
||||
permissions.Add("android.permission.READ_MEDIA_AUDIO");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead))
|
||||
{
|
||||
permissions.Add(Permission.ExternalStorageRead);
|
||||
}
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
|
||||
{
|
||||
permissions.Add(Permission.ExternalStorageWrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.Camera))
|
||||
{
|
||||
permissions.Add(Permission.Camera);
|
||||
}
|
||||
if (permissions.Count > 0)
|
||||
{
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
var grantedCount = 0;
|
||||
var deniedCount = 0;
|
||||
var callbacks = new PermissionCallbacks();
|
||||
callbacks.PermissionGranted += (permission) =>
|
||||
{
|
||||
grantedCount++;
|
||||
if (grantedCount + deniedCount == permissions.Count)
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
}
|
||||
};
|
||||
callbacks.PermissionDenied += (permission) =>
|
||||
{
|
||||
deniedCount++;
|
||||
if (grantedCount + deniedCount == permissions.Count)
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
}
|
||||
};
|
||||
callbacks.PermissionDeniedAndDontAskAgain += (permission) =>
|
||||
{
|
||||
deniedCount++;
|
||||
if (grantedCount + deniedCount == permissions.Count)
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
}
|
||||
};
|
||||
Permission.RequestUserPermissions(permissions.ToArray(), callbacks);
|
||||
#else
|
||||
StartCoroutine(RequestFileChooserPermissionsCoroutine(permissions.ToArray()));
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(true));
|
||||
}
|
||||
}
|
||||
// public void RequestFileChooserPermissions()
|
||||
// {
|
||||
// var permissions = new List<string>();
|
||||
// using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
|
||||
// {
|
||||
// if (version.GetStatic<int>("SDK_INT") >= 33)
|
||||
// {
|
||||
// if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_IMAGES"))
|
||||
// {
|
||||
// permissions.Add("android.permission.READ_MEDIA_IMAGES");
|
||||
// }
|
||||
// if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_VIDEO"))
|
||||
// {
|
||||
// permissions.Add("android.permission.READ_MEDIA_VIDEO");
|
||||
// }
|
||||
// if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_AUDIO"))
|
||||
// {
|
||||
// permissions.Add("android.permission.READ_MEDIA_AUDIO");
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead))
|
||||
// {
|
||||
// permissions.Add(Permission.ExternalStorageRead);
|
||||
// }
|
||||
// if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
|
||||
// {
|
||||
// permissions.Add(Permission.ExternalStorageWrite);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (!Permission.HasUserAuthorizedPermission(Permission.Camera))
|
||||
// {
|
||||
// permissions.Add(Permission.Camera);
|
||||
// }
|
||||
// if (permissions.Count > 0)
|
||||
// {
|
||||
// #if UNITY_2020_2_OR_NEWER
|
||||
// var grantedCount = 0;
|
||||
// var deniedCount = 0;
|
||||
// var callbacks = new PermissionCallbacks();
|
||||
// callbacks.PermissionGranted += (permission) =>
|
||||
// {
|
||||
// grantedCount++;
|
||||
// if (grantedCount + deniedCount == permissions.Count)
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
// }
|
||||
// };
|
||||
// callbacks.PermissionDenied += (permission) =>
|
||||
// {
|
||||
// deniedCount++;
|
||||
// if (grantedCount + deniedCount == permissions.Count)
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
// }
|
||||
// };
|
||||
// callbacks.PermissionDeniedAndDontAskAgain += (permission) =>
|
||||
// {
|
||||
// deniedCount++;
|
||||
// if (grantedCount + deniedCount == permissions.Count)
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
// }
|
||||
// };
|
||||
// Permission.RequestUserPermissions(permissions.ToArray(), callbacks);
|
||||
// #else
|
||||
// StartCoroutine(RequestFileChooserPermissionsCoroutine(permissions.ToArray()));
|
||||
// #endif
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(true));
|
||||
// }
|
||||
// }
|
||||
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
#else
|
||||
|
||||
+87
-87
@@ -214,9 +214,9 @@ namespace Gree.UnityWebView
|
||||
case "SetKeyboardVisible":
|
||||
SetKeyboardVisible(s.Substring(i + 1));
|
||||
break;
|
||||
case "RequestFileChooserPermissions":
|
||||
RequestFileChooserPermissions();
|
||||
break;
|
||||
// case "RequestFileChooserPermissions":
|
||||
// RequestFileChooserPermissions();
|
||||
// break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,90 +239,90 @@ namespace Gree.UnityWebView
|
||||
}
|
||||
|
||||
/// Called from Java native plugin to request permissions for the file chooser.
|
||||
public void RequestFileChooserPermissions()
|
||||
{
|
||||
var permissions = new List<string>();
|
||||
using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
|
||||
{
|
||||
if (version.GetStatic<int>("SDK_INT") >= 33)
|
||||
{
|
||||
if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_IMAGES"))
|
||||
{
|
||||
permissions.Add("android.permission.READ_MEDIA_IMAGES");
|
||||
}
|
||||
if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_VIDEO"))
|
||||
{
|
||||
permissions.Add("android.permission.READ_MEDIA_VIDEO");
|
||||
}
|
||||
if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_AUDIO"))
|
||||
{
|
||||
permissions.Add("android.permission.READ_MEDIA_AUDIO");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead))
|
||||
{
|
||||
permissions.Add(Permission.ExternalStorageRead);
|
||||
}
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
|
||||
{
|
||||
permissions.Add(Permission.ExternalStorageWrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
#if UNITYWEBVIEW_ANDROID_ENABLE_CAMERA
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.Camera) && mAllowVideoCapture)
|
||||
{
|
||||
permissions.Add(Permission.Camera);
|
||||
}
|
||||
#endif
|
||||
#if UNITYWEBVIEW_ANDROID_ENABLE_MICROPHONE
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone) && mAllowAudioCapture)
|
||||
{
|
||||
permissions.Add(Permission.Microphone);
|
||||
}
|
||||
#endif
|
||||
if (permissions.Count > 0)
|
||||
{
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
var grantedCount = 0;
|
||||
var deniedCount = 0;
|
||||
var callbacks = new PermissionCallbacks();
|
||||
callbacks.PermissionGranted += (permission) =>
|
||||
{
|
||||
grantedCount++;
|
||||
if (grantedCount + deniedCount == permissions.Count)
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
}
|
||||
};
|
||||
callbacks.PermissionDenied += (permission) =>
|
||||
{
|
||||
deniedCount++;
|
||||
if (grantedCount + deniedCount == permissions.Count)
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
}
|
||||
};
|
||||
callbacks.PermissionDeniedAndDontAskAgain += (permission) =>
|
||||
{
|
||||
deniedCount++;
|
||||
if (grantedCount + deniedCount == permissions.Count)
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
}
|
||||
};
|
||||
Permission.RequestUserPermissions(permissions.ToArray(), callbacks);
|
||||
#else
|
||||
StartCoroutine(RequestFileChooserPermissionsCoroutine(permissions.ToArray()));
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(CallOnRequestFileChooserPermissionsResult(true));
|
||||
}
|
||||
}
|
||||
// public void RequestFileChooserPermissions()
|
||||
// {
|
||||
// var permissions = new List<string>();
|
||||
// using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
|
||||
// {
|
||||
// if (version.GetStatic<int>("SDK_INT") >= 33)
|
||||
// {
|
||||
// if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_IMAGES"))
|
||||
// {
|
||||
// permissions.Add("android.permission.READ_MEDIA_IMAGES");
|
||||
// }
|
||||
// if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_VIDEO"))
|
||||
// {
|
||||
// permissions.Add("android.permission.READ_MEDIA_VIDEO");
|
||||
// }
|
||||
// if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_AUDIO"))
|
||||
// {
|
||||
// permissions.Add("android.permission.READ_MEDIA_AUDIO");
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead))
|
||||
// {
|
||||
// permissions.Add(Permission.ExternalStorageRead);
|
||||
// }
|
||||
// if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite))
|
||||
// {
|
||||
// permissions.Add(Permission.ExternalStorageWrite);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// #if UNITYWEBVIEW_ANDROID_ENABLE_CAMERA
|
||||
// if (!Permission.HasUserAuthorizedPermission(Permission.Camera) && mAllowVideoCapture)
|
||||
// {
|
||||
// permissions.Add(Permission.Camera);
|
||||
// }
|
||||
// #endif
|
||||
// #if UNITYWEBVIEW_ANDROID_ENABLE_MICROPHONE
|
||||
// if (!Permission.HasUserAuthorizedPermission(Permission.Microphone) && mAllowAudioCapture)
|
||||
// {
|
||||
// permissions.Add(Permission.Microphone);
|
||||
// }
|
||||
// #endif
|
||||
// if (permissions.Count > 0)
|
||||
// {
|
||||
// #if UNITY_2020_2_OR_NEWER
|
||||
// var grantedCount = 0;
|
||||
// var deniedCount = 0;
|
||||
// var callbacks = new PermissionCallbacks();
|
||||
// callbacks.PermissionGranted += (permission) =>
|
||||
// {
|
||||
// grantedCount++;
|
||||
// if (grantedCount + deniedCount == permissions.Count)
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
// }
|
||||
// };
|
||||
// callbacks.PermissionDenied += (permission) =>
|
||||
// {
|
||||
// deniedCount++;
|
||||
// if (grantedCount + deniedCount == permissions.Count)
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
// }
|
||||
// };
|
||||
// callbacks.PermissionDeniedAndDontAskAgain += (permission) =>
|
||||
// {
|
||||
// deniedCount++;
|
||||
// if (grantedCount + deniedCount == permissions.Count)
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count));
|
||||
// }
|
||||
// };
|
||||
// Permission.RequestUserPermissions(permissions.ToArray(), callbacks);
|
||||
// #else
|
||||
// StartCoroutine(RequestFileChooserPermissionsCoroutine(permissions.ToArray()));
|
||||
// #endif
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// StartCoroutine(CallOnRequestFileChooserPermissionsResult(true));
|
||||
// }
|
||||
// }
|
||||
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ChillConnect;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 关卡死局检测工具(Unity编辑器下使用)
|
||||
/// </summary>
|
||||
public class ArrowGameValidator
|
||||
{
|
||||
// ========== 方向转换工具 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 方向字符串转行列偏移量
|
||||
/// </summary>
|
||||
private static (int dRow, int dCol) DirToOffset(string dir)
|
||||
{
|
||||
return dir switch
|
||||
{
|
||||
"up" => (-1, 0),
|
||||
"down" => (1, 0),
|
||||
"left" => (0, -1),
|
||||
"right" => (0, 1),
|
||||
_ => (0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// pointId 转 (row, col)
|
||||
/// </summary>
|
||||
private static (int row, int col) PointIdToRowCol(int pointId, int totalCols)
|
||||
{
|
||||
int row = pointId / totalCols;
|
||||
int col = pointId % totalCols;
|
||||
return (row, col);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// (row, col) 转 pointId
|
||||
/// </summary>
|
||||
private static int RowColToPointId(int row, int col, int totalCols)
|
||||
{
|
||||
return row * totalCols + col;
|
||||
}
|
||||
|
||||
// ========== 核心检测逻辑 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 构建所有箭头占用的网格点集合
|
||||
/// 返回: Dictionary<pointId, List<arrowId>> —— 每个网格点被哪些箭头占用
|
||||
/// </summary>
|
||||
private static Dictionary<int, List<int>> BuildGridOccupation(LevelConfig config)
|
||||
{
|
||||
var gridOccupation = new Dictionary<int, List<int>>();
|
||||
int totalCols = config.gridCols;
|
||||
|
||||
foreach (var arrow in config.arrows)
|
||||
{
|
||||
int curPointId = arrow.startPoint;
|
||||
|
||||
// 起点也加入占用
|
||||
if (!gridOccupation.ContainsKey(curPointId))
|
||||
gridOccupation[curPointId] = new List<int>();
|
||||
gridOccupation[curPointId].Add(arrow.id);
|
||||
|
||||
// 沿路径逐步添加占用
|
||||
foreach (string dir in arrow.path)
|
||||
{
|
||||
var (row, col) = PointIdToRowCol(curPointId, totalCols);
|
||||
var (dRow, dCol) = DirToOffset(dir);
|
||||
row += dRow;
|
||||
col += dCol;
|
||||
|
||||
// 边界检查
|
||||
if (row < 0 || row >= config.gridRows || col < 0 || col >= config.gridCols)
|
||||
{
|
||||
Debug.LogWarning($"箭头 {arrow.id} 路径超出网格边界");
|
||||
break;
|
||||
}
|
||||
|
||||
curPointId = RowColToPointId(row, col, totalCols);
|
||||
|
||||
if (!gridOccupation.ContainsKey(curPointId))
|
||||
gridOccupation[curPointId] = new List<int>();
|
||||
gridOccupation[curPointId].Add(arrow.id);
|
||||
}
|
||||
}
|
||||
|
||||
return gridOccupation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取箭头的最终方向(头部朝向)
|
||||
/// </summary>
|
||||
private static string GetFinalDirection(ArrowConfig arrow)
|
||||
{
|
||||
if (arrow.path != null && arrow.path.Count > 0)
|
||||
return arrow.path[arrow.path.Count - 1];
|
||||
return "up"; // 默认向上
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取箭头头部的网格位置 (pointId)
|
||||
/// </summary>
|
||||
private static int GetArrowHeadPointId(ArrowConfig arrow, int totalCols)
|
||||
{
|
||||
int curPointId = arrow.startPoint;
|
||||
foreach (string dir in arrow.path)
|
||||
{
|
||||
var (row, col) = PointIdToRowCol(curPointId, totalCols);
|
||||
var (dRow, dCol) = DirToOffset(dir);
|
||||
curPointId = RowColToPointId(row + dRow, col + dCol, totalCols);
|
||||
}
|
||||
return curPointId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建依赖图:检测每个箭头前方被哪些其他箭头阻挡
|
||||
/// 返回: 邻接表 —— blockedBy[arrowId] = List of arrowIds that block it
|
||||
/// 即:要消除 arrowId,必须先消除 blockedBy[arrowId] 中的箭头
|
||||
/// </summary>
|
||||
private static Dictionary<int, HashSet<int>> BuildDependencyGraph(
|
||||
LevelConfig config,
|
||||
Dictionary<int, List<int>> gridOccupation)
|
||||
{
|
||||
var blockedBy = new Dictionary<int, HashSet<int>>();
|
||||
int totalCols = config.gridCols;
|
||||
int totalRows = config.gridRows;
|
||||
|
||||
// 初始化
|
||||
foreach (var arrow in config.arrows)
|
||||
{
|
||||
blockedBy[arrow.id] = new HashSet<int>();
|
||||
}
|
||||
|
||||
foreach (var arrow in config.arrows)
|
||||
{
|
||||
string finalDir = GetFinalDirection(arrow);
|
||||
int headPointId = GetArrowHeadPointId(arrow, totalCols);
|
||||
var (headRow, headCol) = PointIdToRowCol(headPointId, totalCols);
|
||||
var (dRow, dCol) = DirToOffset(finalDir);
|
||||
|
||||
// 沿最终方向逐格扫描
|
||||
int curRow = headRow + dRow;
|
||||
int curCol = headCol + dCol;
|
||||
|
||||
while (curRow >= 0 && curRow < totalRows && curCol >= 0 && curCol < totalCols)
|
||||
{
|
||||
int curPointId = RowColToPointId(curRow, curCol, totalCols);
|
||||
|
||||
// 检查该网格点是否被其他箭头占用
|
||||
if (gridOccupation.TryGetValue(curPointId, out var occupyingArrows))
|
||||
{
|
||||
foreach (int occupyingArrowId in occupyingArrows)
|
||||
{
|
||||
// 不能阻挡自己
|
||||
if (occupyingArrowId != arrow.id)
|
||||
{
|
||||
// occupyingArrow 阻挡了当前箭头
|
||||
// 要消除当前箭头,必须先消除 occupyingArrow
|
||||
blockedBy[arrow.id].Add(occupyingArrowId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curRow += dRow;
|
||||
curCol += dCol;
|
||||
}
|
||||
}
|
||||
|
||||
return blockedBy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用拓扑排序检测是否有环(死局)
|
||||
/// 返回: (isSolvable, unsolvableArrowIds)
|
||||
/// </summary>
|
||||
private static (bool isSolvable, List<int> unsolvableArrowIds) DetectDeadlock(
|
||||
LevelConfig config,
|
||||
Dictionary<int, HashSet<int>> blockedBy)
|
||||
{
|
||||
// 计算每个箭头的入度(被多少箭头阻挡)
|
||||
var inDegree = new Dictionary<int, int>();
|
||||
foreach (var arrow in config.arrows)
|
||||
{
|
||||
inDegree[arrow.id] = 0;
|
||||
}
|
||||
|
||||
// 构建反向邻接表:谁依赖于我
|
||||
var dependents = new Dictionary<int, List<int>>();
|
||||
foreach (var arrow in config.arrows)
|
||||
{
|
||||
dependents[arrow.id] = new List<int>();
|
||||
}
|
||||
|
||||
foreach (var kvp in blockedBy)
|
||||
{
|
||||
int arrowId = kvp.Key;
|
||||
foreach (int blockerId in kvp.Value)
|
||||
{
|
||||
// arrowId 依赖 blockerId
|
||||
// blockerId -> arrowId
|
||||
dependents[blockerId].Add(arrowId);
|
||||
inDegree[arrowId]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Kahn 拓扑排序
|
||||
var queue = new Queue<int>();
|
||||
foreach (var arrow in config.arrows)
|
||||
{
|
||||
if (inDegree[arrow.id] == 0)
|
||||
{
|
||||
queue.Enqueue(arrow.id);
|
||||
}
|
||||
}
|
||||
|
||||
int removedCount = 0;
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
int current = queue.Dequeue();
|
||||
removedCount++;
|
||||
|
||||
foreach (int dependent in dependents[current])
|
||||
{
|
||||
inDegree[dependent]--;
|
||||
if (inDegree[dependent] == 0)
|
||||
{
|
||||
queue.Enqueue(dependent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有箭头都能被移除,则可解
|
||||
if (removedCount == config.arrows.Count)
|
||||
{
|
||||
return (true, new List<int>());
|
||||
}
|
||||
|
||||
// 找出无法移除的箭头(入度 > 0 的)
|
||||
var unsolvableArrows = new List<int>();
|
||||
foreach (var arrow in config.arrows)
|
||||
{
|
||||
if (inDegree[arrow.id] > 0)
|
||||
{
|
||||
unsolvableArrows.Add(arrow.id);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, unsolvableArrows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从环路中提取所有涉及的箭头ID
|
||||
/// </summary>
|
||||
private static List<int> ExtractCycleArrowIds(
|
||||
LevelConfig config,
|
||||
Dictionary<int, HashSet<int>> blockedBy,
|
||||
List<int> unsolvableArrows)
|
||||
{
|
||||
// 从不可解的箭头出发,DFS找出所有在环路中的箭头
|
||||
var allCycleNodes = new HashSet<int>();
|
||||
|
||||
foreach (int startId in unsolvableArrows)
|
||||
{
|
||||
if (allCycleNodes.Contains(startId))
|
||||
continue;
|
||||
|
||||
// 尝试从 startId 出发找环
|
||||
var visited = new HashSet<int>();
|
||||
var inStack = new HashSet<int>();
|
||||
var path = new List<int>();
|
||||
|
||||
if (FindCycle(startId, blockedBy, visited, inStack, path, out var cycleNodes))
|
||||
{
|
||||
foreach (int node in cycleNodes)
|
||||
{
|
||||
allCycleNodes.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allCycleNodes.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DFS寻找环路
|
||||
/// </summary>
|
||||
private static bool FindCycle(
|
||||
int current,
|
||||
Dictionary<int, HashSet<int>> blockedBy,
|
||||
HashSet<int> visited,
|
||||
HashSet<int> inStack,
|
||||
List<int> path,
|
||||
out List<int> cycleNodes)
|
||||
{
|
||||
visited.Add(current);
|
||||
inStack.Add(current);
|
||||
path.Add(current);
|
||||
|
||||
if (blockedBy.TryGetValue(current, out var blockers))
|
||||
{
|
||||
foreach (int blocker in blockers)
|
||||
{
|
||||
if (!visited.Contains(blocker))
|
||||
{
|
||||
if (FindCycle(blocker, blockedBy, visited, inStack, path, out cycleNodes))
|
||||
{
|
||||
inStack.Remove(current);
|
||||
path.RemoveAt(path.Count - 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (inStack.Contains(blocker))
|
||||
{
|
||||
// 找到环
|
||||
int cycleStartIndex = path.IndexOf(blocker);
|
||||
cycleNodes = path.GetRange(cycleStartIndex, path.Count - cycleStartIndex);
|
||||
inStack.Remove(current);
|
||||
path.RemoveAt(path.Count - 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inStack.Remove(current);
|
||||
path.RemoveAt(path.Count - 1);
|
||||
cycleNodes = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========== 公共接口 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 检测单个关卡是否可解
|
||||
/// </summary>
|
||||
public static ValidationResult ValidateLevel(LevelConfig config)
|
||||
{
|
||||
var result = new ValidationResult
|
||||
{
|
||||
levelId = config.levelId,
|
||||
levelName = config.levelName
|
||||
};
|
||||
|
||||
if (config.arrows == null || config.arrows.Count == 0)
|
||||
{
|
||||
result.isSolvable = true;
|
||||
result.message = "关卡无箭头";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 1. 构建网格占用
|
||||
var gridOccupation = BuildGridOccupation(config);
|
||||
|
||||
// 2. 构建依赖图
|
||||
var blockedBy = BuildDependencyGraph(config, gridOccupation);
|
||||
|
||||
// 3. 拓扑排序检测
|
||||
var (isSolvable, unsolvableArrows) = DetectDeadlock(config, blockedBy);
|
||||
|
||||
result.isSolvable = isSolvable;
|
||||
result.blockedBy = blockedBy;
|
||||
|
||||
if (isSolvable)
|
||||
{
|
||||
result.message = "✅ 可解";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 找出环路中的具体箭头
|
||||
var cycleArrows = ExtractCycleArrowIds(config, blockedBy, unsolvableArrows);
|
||||
result.unsolvableArrowIds = cycleArrows.OrderBy(x => x).ToList();
|
||||
result.message = $"❌ 死局,无法消除的箭头: [{string.Join(", ", result.unsolvableArrowIds)}]";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量检测关卡目录下所有JSON文件
|
||||
/// </summary>
|
||||
public static void ValidateAllLevels(string folderPath)
|
||||
{
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
Debug.LogError($"目录不存在: {folderPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] jsonFiles = Directory.GetFiles(folderPath, "*.json");
|
||||
int totalCount = jsonFiles.Length;
|
||||
int solvableCount = 0;
|
||||
int unsolvableCount = 0;
|
||||
var unsolvableResults = new List<ValidationResult>();
|
||||
|
||||
foreach (string filePath in jsonFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(filePath);
|
||||
LevelConfig config = JsonUtility.FromJson<LevelConfig>(json);
|
||||
|
||||
var result = ValidateLevel(config);
|
||||
|
||||
if (result.isSolvable)
|
||||
solvableCount++;
|
||||
else
|
||||
{
|
||||
unsolvableCount++;
|
||||
unsolvableResults.Add(result);
|
||||
}
|
||||
|
||||
Debug.Log($"[{filePath}] {result.message}");
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"解析文件失败: {filePath}, 错误: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("========== 检测汇总 ==========");
|
||||
Debug.Log($"总关卡数: {totalCount}");
|
||||
Debug.Log($"可解关卡: {solvableCount}");
|
||||
Debug.Log($"死局关卡: {unsolvableCount}");
|
||||
|
||||
if (unsolvableResults.Count > 0)
|
||||
{
|
||||
Debug.Log("========== 死局关卡详情 ==========");
|
||||
foreach (var result in unsolvableResults)
|
||||
{
|
||||
Debug.Log($"关卡 {result.levelId} ({result.levelName}): {result.message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 编辑器菜单 ==========
|
||||
|
||||
[MenuItem("Tools/箭头游戏/检测单个关卡(选择JSON文件)")]
|
||||
public static void ValidateSelectedLevel()
|
||||
{
|
||||
string path = EditorUtility.OpenFilePanel("选择关卡JSON", Application.streamingAssetsPath, "json");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
LevelConfig config = JsonUtility.FromJson<LevelConfig>(json);
|
||||
|
||||
var result = ValidateLevel(config);
|
||||
|
||||
Debug.Log($"========== 关卡 {result.levelId} ({result.levelName}) 检测结果 ==========");
|
||||
Debug.Log(result.message);
|
||||
|
||||
if (!result.isSolvable)
|
||||
{
|
||||
Debug.Log($"环路箭头: [{string.Join(", ", result.unsolvableArrowIds)}]");
|
||||
|
||||
// 打印依赖详情
|
||||
Debug.Log("========== 依赖关系 ==========");
|
||||
foreach (int id in result.unsolvableArrowIds)
|
||||
{
|
||||
if (result.blockedBy.TryGetValue(id, out var blockers))
|
||||
{
|
||||
Debug.Log($"箭头 {id} 被阻挡于: [{string.Join(", ", blockers)}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.DisplayDialog("检测完成", result.message, "确定");
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"检测失败: {e.Message}");
|
||||
EditorUtility.DisplayDialog("错误", $"检测失败: {e.Message}", "确定");
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Tools/箭头游戏/批量检测所有关卡")]
|
||||
public static void ValidateAllLevelsMenu()
|
||||
{
|
||||
string folderPath = EditorUtility.OpenFolderPanel("选择关卡目录", Application.streamingAssetsPath, "");
|
||||
if (string.IsNullOrEmpty(folderPath)) return;
|
||||
|
||||
ValidateAllLevels(folderPath);
|
||||
EditorUtility.DisplayDialog("批量检测完成", "请查看Console输出结果", "确定");
|
||||
}
|
||||
|
||||
// ========== 数据结构 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 检测结果
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
public int levelId;
|
||||
public string levelName;
|
||||
public bool isSolvable;
|
||||
public string message;
|
||||
public List<int> unsolvableArrowIds = new List<int>();
|
||||
public Dictionary<int, HashSet<int>> blockedBy;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da19b18902544191a3062014594d3b22
|
||||
timeCreated: 1784305307
|
||||
@@ -4,6 +4,12 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.unity3d.player"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- 强制移除webview aar自带的媒体权限,解决Play未声明报错 -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" tools:node="remove"/>
|
||||
|
||||
<application>
|
||||
<activity android:name="com.unity3d.player.UnityPlayerActivity"
|
||||
android:theme="@style/UnityThemeSelector">
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><resources><string name="com.crashlytics.android.build_id" translatable="false">a3b8638f-c8ff-4e13-89ae-85cb2b148835</string></resources>
|
||||
<?xml version="1.0" encoding="utf-8"?><resources><string name="com.crashlytics.android.build_id" translatable="false">d4a07f28-7baa-4f99-aa00-e8f80b7c45a1</string></resources>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"failed_save": "Failed to save information., please try again!",
|
||||
"un_lock": "To be unlocked",
|
||||
"exchange_succ": "Claim({0})",
|
||||
"exchange_success": "Claim X{0}",
|
||||
"redeem_success": "Claim X{0}",
|
||||
"free_revive": "Free Revive",
|
||||
"revive": "Revive",
|
||||
"need_lv_text":"Level {0}",
|
||||
@@ -40,7 +40,7 @@
|
||||
"failed_save": "情報の保存に失敗しました。再度お試しください!",
|
||||
"un_lock": "解放待ち",
|
||||
"exchange_succ": "受け取る({0})",
|
||||
"exchange_success": "X{0}を受け取る",
|
||||
"redeem_success": "X{0}を受け取る",
|
||||
"free_revive": "無料復活",
|
||||
"revive": "復活",
|
||||
"need_lv_text": "レベル{0}",
|
||||
@@ -69,7 +69,7 @@
|
||||
"failed_save": "정보 저장에 실패했습니다. 다시 시도해 주세요!",
|
||||
"un_lock": "잠금 해제 예정",
|
||||
"exchange_succ": "수령({0})",
|
||||
"exchange_success": "X{0} 수령",
|
||||
"redeem_success": "X{0} 수령",
|
||||
"free_revive": "무료 부활",
|
||||
"revive": "부활",
|
||||
"need_lv_text": "레벨 {0}",
|
||||
@@ -97,7 +97,7 @@
|
||||
"failed_save": "Не удалось сохранить данные, попробуйте снова!",
|
||||
"un_lock": "Будет разблокировано",
|
||||
"exchange_succ": "Забрать({0})",
|
||||
"exchange_success": "Забрать X{0}",
|
||||
"redeem_success": "Забрать X{0}",
|
||||
"free_revive": "Бесплатное воскрешение",
|
||||
"revive": "Воскреснуть",
|
||||
"need_lv_text": "Уровень {0}",
|
||||
@@ -125,7 +125,7 @@
|
||||
"failed_save": "जानकारी सहेजने में विफल, कृपया पुनः प्रयास करें!",
|
||||
"un_lock": "जल्द अनलॉक होगा",
|
||||
"exchange_succ": "दावा करें({0})",
|
||||
"exchange_success": "X{0} दावा करें",
|
||||
"redeem_success": "X{0} दावा करें",
|
||||
"free_revive": "मुफ्त पुनर्जीवन",
|
||||
"revive": "पुनर्जीवित करें",
|
||||
"need_lv_text": "स्तर {0}",
|
||||
@@ -141,7 +141,7 @@
|
||||
"exchange_success": "विनिमय सफल"
|
||||
},
|
||||
"pt": {
|
||||
"copy_succeed": "Cópia realizada com sucesso",
|
||||
"copy_succeed": " Cópia realizada com sucesso",
|
||||
"not_ads": "Os anúncios ainda não estão prontos!",
|
||||
"try_send": "Enviando e-mail…",
|
||||
"no_network": "Sua conexão com a internet está instável!",
|
||||
@@ -153,7 +153,7 @@
|
||||
"failed_save": "Falha ao salvar as informações, tente novamente!",
|
||||
"un_lock": "A ser desbloqueado",
|
||||
"exchange_succ": "Resgatar({0})",
|
||||
"exchange_success": "Resgatar X{0}",
|
||||
"redeem_success": "Resgatar X{0}",
|
||||
"free_revive": "Reviver grátis",
|
||||
"revive": "Reviver",
|
||||
"need_lv_text": "Nível {0}",
|
||||
@@ -181,7 +181,7 @@
|
||||
"failed_save": "Speichern fehlgeschlagen, bitte erneut versuchen!",
|
||||
"un_lock": "Wird freigeschaltet",
|
||||
"exchange_succ": "Einlösen({0})",
|
||||
"exchange_success": "X{0} einlösen",
|
||||
"redeem_success": "X{0} einlösen",
|
||||
"free_revive": "Kostenlose Wiederbelebung",
|
||||
"revive": "Wiederbeleben",
|
||||
"need_lv_text": "Level {0}",
|
||||
@@ -209,7 +209,7 @@
|
||||
"failed_save": "Échec de l'enregistrement, veuillez réessayer !",
|
||||
"un_lock": "À débloquer",
|
||||
"exchange_succ": "Réclamer({0})",
|
||||
"exchange_success": "Réclamer X{0}",
|
||||
"redeem_success": "Réclamer X{0}",
|
||||
"free_revive": "Réanimation gratuite",
|
||||
"revive": "Réanimer",
|
||||
"need_lv_text": "Niveau {0}",
|
||||
@@ -237,7 +237,7 @@
|
||||
"failed_save": "Error al guardar la información, inténtalo de nuevo!",
|
||||
"un_lock": "Pendiente de desbloquear",
|
||||
"exchange_succ": "Reclamar({0})",
|
||||
"exchange_success": "Reclamar X{0}",
|
||||
"redeem_success": "Reclamar X{0}",
|
||||
"free_revive": "Revivir gratis",
|
||||
"revive": "Revivir",
|
||||
"need_lv_text": "Nivel {0}",
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace ChillConnect
|
||||
});
|
||||
|
||||
|
||||
ui.btn_mult.GetChild("title").text = Language.GetContentParams("exchange_success", AwardRate);
|
||||
ui.btn_mult.GetChild("title").text = Language.GetContentParams(" redeem_success", AwardRate);
|
||||
|
||||
if (SuccessDatas.IsH5Reward)
|
||||
ui.com_ch.visible = false;
|
||||
|
||||
@@ -141,7 +141,7 @@ PlayerSettings:
|
||||
loadStoreDebugModeEnabled: 0
|
||||
visionOSBundleVersion: 1.0
|
||||
tvOSBundleVersion: 1.0
|
||||
bundleVersion: 1.0.0
|
||||
bundleVersion: 1.0.1
|
||||
preloadedAssets: []
|
||||
metroInputSource: 0
|
||||
wsaTransparentSwapchain: 0
|
||||
@@ -171,7 +171,7 @@ PlayerSettings:
|
||||
iPhone: 2
|
||||
tvOS: 0
|
||||
overrideDefaultApplicationIdentifier: 1
|
||||
AndroidBundleVersionCode: 1
|
||||
AndroidBundleVersionCode: 2
|
||||
AndroidMinSdkVersion: 24
|
||||
AndroidTargetSdkVersion: 36
|
||||
AndroidPreferredInstallLocation: 1
|
||||
@@ -849,7 +849,7 @@ PlayerSettings:
|
||||
webGLMemoryGeometricGrowthCap: 96
|
||||
webGLPowerPreference: 2
|
||||
scriptingDefineSymbols:
|
||||
Android: UNITY_VISUAL_SCRIPTING;ES3_TMPRO;ES3_UGUI;USE_ADDRESSABLES;GAME_RELEASE
|
||||
Android: UNITY_VISUAL_SCRIPTING;ES3_TMPRO;ES3_UGUI;USE_ADDRESSABLES;GAME_RELEASE1
|
||||
EmbeddedLinux: UNITY_VISUAL_SCRIPTING;ES3_TMPRO;ES3_UGUI
|
||||
LinuxHeadlessSimulation: UNITY_VISUAL_SCRIPTING;ES3_TMPRO;ES3_UGUI
|
||||
Nintendo Switch: UNITY_VISUAL_SCRIPTING;ES3_TMPRO;ES3_UGUI
|
||||
|
||||
Reference in New Issue
Block a user