Files
ArrowBeatTap-Gp/Assets/Scripts/ModuleUI/ArrowGame/ArrowGameUI.cs
T

1693 lines
59 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DG.Tweening;
using FairyGUI;
using FGUI.Arrow_game;
using FGUI.Common_01;
using IgnoreOPS;
using UnityEngine;
namespace ChillConnect
{
public class ArrowGameUI : BaseUI
{
private ArrowGameUICtrl ctrl;
private com_arrow_game ui;
// FGUI界面根容器
private GComponent _viewContainer;
#region FGUI 模板引用
private ArrorPoint _dotTemplate;
private ArrowEnd _arrowTemplate;
private LineTile _lineUnitTemplate;
#endregion
// 关卡总配置数据
private LevelConfig _levelConfig;
// 网格基础参数
private readonly int gridColNum = 10;
private readonly float pointSpace = 60f;
private readonly float outOffset = 200f;
// 网格边界
private Vector2 _gridWorldCenter; // 网格世界中心点
private float gridLeft;
private float gridRight;
private readonly int gridRowNum = 10;
private float gridTop;
private float gridBottom;
// 网格行列、单个格子尺寸(你原有网格参数)
private int _gridRow;
private int _gridCol;
private float _cellSize;
// 网格占用标记:true=该点位被占用,false=空闲
private bool[,] _gridOccupied;
// 记录每个箭头占用的所有网格点位(行,列)
private Dictionary<ArrowConfig, List<(int row, int col)>> _arrowOccupiedCells = new Dictionary<ArrowConfig, List<(int row, int col)>>();
// 爱心数量
private int _heartCount = 3;
// 记录已经发生过碰撞的箭头对象(去重:同个箭头多次碰撞只扣一次血)
private HashSet<ArrowConfig> _collidedArrows = new HashSet<ArrowConfig>();
// 统计场上还存在的箭头总数(用于判断全部移除=通关)
private int _remainArrowCount = 0;
// 记录当前UI所有由 CrazyAsyKit 启动的协程,用于统一停止
private List<Coroutine> _activeCoroutines = new List<Coroutine>();
// 是否开启删除道具模式
private bool _isDeleteMode = false;
// 手指提示图标
private com_finger _fingerTipObj;
// 当前正在提示的箭头
private ArrowConfig _currentTipArrow;
// 手指图标偏移(微调位置)
private readonly Vector2 _fingerOffset = new Vector2(-60, -60);
// 所有网格圆点实例列表,用于批量改色/清理
private List<ArrorPoint> _gridDotList = new List<ArrorPoint>();
#region 缩放、拖拽、滑动条 全局变量
// 缩放阈值(滑动条左右边界对应这两个值)
private readonly float _minScale = 0.6f;
private readonly float _maxScale = 1.5f;
// FGUI 滑动条组件
private GSlider _zoomSlider;
// 双指缩放手势
private float _lastTwoFingerDistance;
private bool _isTwoFingerTouch;
// 单指拖拽平移
private Vector2 _lastTouchPos;
private bool _isDraging;
// 拖拽边界留边(防止完全移出屏幕)
private readonly float _dragBorder = 100f;
#endregion
public ArrowGameUI(ArrowGameUICtrl ctrl) : base(ctrl)
{
uiName = UIConst.ArrowGameUI;
this.ctrl = ctrl;
}
protected override void SetUIInfo(UIInfo uiInfo)
{
uiInfo.packageName = "Arrow_game";
uiInfo.assetName = "com_arrow_game";
uiInfo.layerType = UILayerType.Popup;
uiInfo.isNeedOpenAnim = false;
uiInfo.isNeedCloseAnim = false;
uiInfo.isNeedUIMask = true;
uiInfo.isTickUpdate = true;
}
#region 生命周期
protected override void OnInit()
{
}
protected override void OnClose()
{
// ========= 第一步:统一停止所有协程(核心修复) =========
foreach (var co in _activeCoroutines)
{
if (co != null)
{
CrazyAsyKit.StopCoroutine(co);
}
}
// 清空列表
_activeCoroutines.Clear();
// 保险========= 强制停止所有移动,终止对应协程 =========
if (_levelConfig != null)
{
foreach (var arrow in _levelConfig.arrows)
{
arrow.isMoving = false;
}
}
// 销毁所有动态FGUI对象
ClearAllDynamicObj();
HideFingerTip();
// 释放模板资源
if (_dotTemplate != null) _dotTemplate.Dispose();
if (_arrowTemplate != null) _arrowTemplate.Dispose();
if (_lineUnitTemplate != null) _lineUnitTemplate.Dispose();
_dotTemplate = null;
_arrowTemplate = null;
_lineUnitTemplate = null;
_levelConfig = null;
// 清空网格占用数据
_gridOccupied = null;
_arrowOccupiedCells.Clear();
}
protected override void OnBind()
{
ui = baseUI as com_arrow_game;
}
// 新增坐标转换方法
private Vector2 UnityMouseToFGUIPos(Vector2 unityPos)
{
float x = unityPos.x;
float y = Screen.height - unityPos.y;
return new Vector2(x, y);
}
public override void OnUpdate()
{
// if (_levelConfig == null) return;
// foreach (var arrow in _levelConfig.arrows)
// {
// UpdateArrowMove(arrow);
// }
//
// ========= 编辑器模拟触摸(仅编辑模式生效) =========
// ========== 编辑器鼠标拖拽 ==========
// 初始化未完成,直接跳过拖拽
if (!_initComplete)
return;
#if UNITY_EDITOR
float moveThreshold = 1f; // 移动阈值,大于这个值才算有效拖动
// 鼠标按下:仅初始化状态和起点
if (InputHelper.Instance.GetMouseButtonDown(0))
{
Vector2 unityPos = InputHelper.Instance.MousePosition;
_lastTouchPos = UnityMouseToFGUIPos(unityPos);
_isDraging = true;
Debug.Log("鼠标按下,开始拖拽");
return; // 本帧直接返回,不执行拖动逻辑
}
// 拖拽中:按住左键 + 拖拽标记为true
if (_isDraging && InputHelper.Instance.GetMouseButton(0))
{
Vector2 unityPos = InputHelper.Instance.MousePosition;
Vector2 curMousePos = UnityMouseToFGUIPos(unityPos);
Vector2 offset = curMousePos - _lastTouchPos;
Debug.Log($"curMousePos=={curMousePos} _lastTouchPos=={_lastTouchPos} offset=={offset}");
// 只有偏移超过阈值才移动 & 更新基准点
if (offset.magnitude > moveThreshold)
{
MoveContainer(offset);
_lastTouchPos = curMousePos; // 移动后再刷新起点
}
}
// 鼠标抬起,结束拖拽
if (InputHelper.Instance.GetMouseButtonUp(0))
{
_isDraging = false;
Debug.Log("鼠标抬起,结束拖拽");
}
#endif
// ========= 双指 = 缩放(优先级最高) =========
if (InputHelper.Instance.TouchCount == 2)
{
_isDraging = false;
Touch t0 = InputHelper.Instance.GetTouch(0);
Touch t1 = InputHelper.Instance.GetTouch(1);
float currentDis = Vector2.Distance(t0.position, t1.position);
if (!_isTwoFingerTouch)
{
_lastTwoFingerDistance = currentDis;
_isTwoFingerTouch = true;
}
else
{
float delta = currentDis - _lastTwoFingerDistance;
float curScale = _viewContainer.scaleX;
// 缩放灵敏度,按需微调
curScale += delta * 0.001f;
curScale = Mathf.Clamp(curScale, _minScale, _maxScale);
// 应用缩放
_viewContainer.scaleX = curScale;
_viewContainer.scaleY = curScale;
// 关键:双指缩放后,同步更新滑动条位置
_zoomSlider.value = GetSliderValueByScale(curScale);
_lastTwoFingerDistance = currentDis;
}
}
// ========= 单指 = 拖拽平移 =========
else if (InputHelper.Instance.TouchCount == 1)
{
Touch touch = InputHelper.Instance.GetTouch(0);
Vector2 curTouchPos = touch.position;
if (touch.phase == TouchPhase.Began)
{
_lastTouchPos = curTouchPos;
_isDraging = true;
_isTwoFingerTouch = false;
}
else if (touch.phase == TouchPhase.Moved && _isDraging)
{
Vector2 offset = curTouchPos - _lastTouchPos;
MoveContainer(offset);
_lastTouchPos = curTouchPos;
}
else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
{
_isDraging = false;
}
}
// 无触摸,重置状态
else
{
_isTwoFingerTouch = false;
_isDraging = false;
}
}
protected override void OnOpenBefore(object args)
{
//加载配置
InitAllLevelData();
_viewContainer = ui.view_container;
// 初始化FGUI模板
_dotTemplate = (ArrorPoint)UIPackage.CreateObject("Arrow_game", "ArrorPoint");
_arrowTemplate = (ArrowEnd)UIPackage.CreateObject("Arrow_game", "ArrowEnd");
_lineUnitTemplate = (LineTile)UIPackage.CreateObject("Arrow_game", "LineTile");
LoadLevelConfig();
InitGridBounds();
// 初始化网格占用数组
InitGridOccupied();
// ========== 新增:对局状态重置 ==========
_heartCount = 3;
_collidedArrows.Clear();
if(_levelConfig != null)
_remainArrowCount = _levelConfig.arrows.Count;
InitView();
}
protected override void OnOpen(object args)
{
}
protected override void OnHide()
{
}
protected override void OnDisplay(object args)
{
}
#endregion
#region 消息监听
protected override void AddListener()
{
GameDispatcher.Instance.AddListener(GameMsg.reset_game, OnRestartGame);
GameDispatcher.Instance.AddListener(GameMsg.Update102, SetTopCurr);
GameDispatcher.Instance.AddListener(GameMsg.Update101, SetTopCurr);
}
protected override void RemoveListener()
{
GameDispatcher.Instance.RemoveListener(GameMsg.reset_game, OnRestartGame);
GameDispatcher.Instance.RemoveListener(GameMsg.Update102, SetTopCurr);
GameDispatcher.Instance.RemoveListener(GameMsg.Update101, SetTopCurr);
}
#endregion
#region 页面初始化
private bool _initComplete = false;
private void InitView()
{
// 重置手势状态
_isTwoFingerTouch = false;
_isDraging = false;
SetModel();
ui.text_level.SetVar("lv", GameHelper.GetLevel().ToString()).FlushVars();
// 按钮点击事件绑定
OnBtnClickBindEvent();
_fingerTipObj = com_finger.CreateInstance();
_viewContainer.AddChild(_fingerTipObj);
_fingerTipObj.visible = false;
_currentTipArrow = null;
_zoomSlider = ui.com_bottom.zoomSlide;
// 2. 绑定滑动条拖动回调
_zoomSlider.onChanged.Add(OnZoomSliderChange);
// 3. 容器初始缩放、位置
_viewContainer.scaleX = 1f;
_viewContainer.scaleY = 1f;
// 滑动条初始居中(对应 1.0 缩放)
float initValue = GetSliderValueByScale(1f);
_zoomSlider.value = initValue;
// 先兜底重置为默认锚点
_viewContainer.pivot = new Vector2(0, 0);
if (_levelConfig == null)
{
_initComplete = true;
return;
}
// 第一步:绘制网格 → 内部会计算网格中心、修改pivot为网格中心
DrawGridDots(_levelConfig);
// ========== 核心:把网格中心钉在屏幕中心 ==========
// _gridWorldCenter 是 DrawGridDots 里 CalculateGridCenter 算出的值
float screenCenterX = Screen.width * 0.5f;
float screenCenterY = Screen.height * 0.5f;
// 计算容器需要移动多少,才能让网格中心正好对准屏幕中心
float targetContainerX = screenCenterX - _gridWorldCenter.x;
float targetContainerY = screenCenterY - _gridWorldCenter.y;
// 直接设置容器位置
_viewContainer.SetPosition(targetContainerX, targetContainerY, 0);
// ==================================================
foreach (var arrow in _levelConfig.arrows)
{
BuildArrowPath(arrow, _levelConfig);
CreateArrowAndLines(arrow, _levelConfig);
MarkArrowOccupiedCells(arrow, _levelConfig);
}
_initComplete = true;
SetTopCurr();
}
private void SetTopCurr(object a = null)
{
if (ui.com_money is com_money btnMoney)
{
btnMoney.text_gold.text = DataMgr.Ticket.Value.ToString("0.00");
btnMoney.SetClick(PopMakeup);
}
}
private void PopMakeup()
{
var makeupTaskData = DataMgr.MakeupTaskHistory.Value.Last();
var vo = ConfigSystem.GetConfig<MakeupModel>().GetData(makeupTaskData.tableId);
if (vo == null)
{
return;
}
GameHelper.showGameUI = false;
uiCtrlDispatcher.Dispatch(UICtrlMsg.MakeupConfirmUI_Open, makeupTaskData);
}
#endregion
#region UI相关
private void OnBtnClickBindEvent()
{
ui.btn_close.SetClick(CtrlCloseUI);
ui.com_bottom.btn_clear.SetClick(OnClickDeleteItem);
ui.com_bottom.btn_hint.SetClick(OnClickHint);
ui.com_bottom.btn_skin.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.ArrowThemeUI_Open); });
ui.btn_petty.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.PettyAwardUI_Open); });
ui.btn_signin.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.SignInUI_Open); });
ui.btn_statement.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.StatementViewUI_Open); });
ui.btn_saveingpot.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.SaveingPotUI_Open); });
}
/// <summary>
/// 切换模式 默认黑夜模式
/// </summary>
/// <param name="mod">true:白天 false:黑夜</param>
private void SetModel(object mod = null)
{
int idx = 0;
if (mod != null)
{
idx = (bool)mod? 1 : 0;
}
ui.mode.selectedIndex = idx;
// 同步更新网格圆点颜色
bool isDayMode = idx == 1;
UpdateGridDotColor(isDayMode);
}
/// <summary>
/// 批量设置网格圆点颜色(随昼夜模式切换)
/// </summary>
/// <param name="isDay">true=白天模式,false=黑夜模式</param>
private void UpdateGridDotColor(bool isDay)
{
if (_gridDotList == null || _gridDotList.Count == 0)
return;
// 色值映射:白天#2c3a62,黑夜#d4e3f2
string colorHex = isDay ? "#2c3a62" : "#d4e3f2";
ColorUtility.TryParseHtmlString(colorHex, out Color targetColor);
foreach (var dot in _gridDotList)
{
if (dot == null) continue;
dot.point.color = targetColor;
}
}
#endregion
private void InitGridBounds()
{
gridLeft = ui.point.x;
gridRight = ui.point.x + gridColNum * pointSpace;
gridTop = ui.point.y;
gridBottom = ui.point.y + gridRowNum * pointSpace;
}
/// <summary>初始化网格占用二维数组</summary>
private void InitGridOccupied()
{
int rows = _levelConfig.gridRows;
int cols = _levelConfig.gridCols;
_gridOccupied = new bool[rows, cols];
// 初始全部置为空闲
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
_gridOccupied[r, c] = false;
_arrowOccupiedCells.Clear();
}
/// <summary>标记单个箭头占用的所有网格点</summary>
private void MarkArrowOccupiedCells(ArrowConfig arrow, LevelConfig cfg)
{
List<(int row, int col)> cellList = new List<(int row, int col)>();
int totalCol = cfg.gridCols;
// 遍历箭头整条路径的所有网格点位
foreach (var pos in arrow.pathPointList)
{
int pointId = GetPointIdByPos(pos, cfg);
PointIdToRowCol(pointId, totalCol, out int r, out int c);
// 边界保护
if (r >= 0 && r < cfg.gridRows && c >= 0 && c < cfg.gridCols)
{
_gridOccupied[r, c] = true;
cellList.Add((r, c));
}
}
_arrowOccupiedCells[arrow] = cellList;
}
/// <summary>释放单个箭头占用的网格(箭头消失时调用)</summary>
private void ReleaseArrowOccupied(ArrowConfig arrow)
{
if (!_arrowOccupiedCells.TryGetValue(arrow, out var cellList))
return;
foreach (var (r, c) in cellList)
{
if (r >= 0 && r < _levelConfig.gridRows && c >= 0 && c < _levelConfig.gridCols)
{
_gridOccupied[r, c] = false;
}
}
// 移除字典记录,避免冗余
_arrowOccupiedCells.Remove(arrow);
}
#region 配置加载
private void LoadLevelConfig()
{
// _levelConfig = JsonHelper.LoadLevel("level_1001.json");
// 示例:加载第1关 levelId=1001
int currentLevelId = GameHelper.GetLevel();
_levelConfig = LoadLevelById(currentLevelId);
// 给 path 为空的箭头,设置默认方向
foreach (var arrow in _levelConfig.arrows)
{
if (arrow.path == null || arrow.path.Count == 0)
{
arrow.defaultDir = "up";
}
else
{
arrow.defaultDir = null;
}
}
}
/// <summary>
/// 从 all_levels.json 中按 levelId 读取指定关卡
/// </summary>
/// <param name="targetLevelId">目标关卡ID 如 1001</param>
/// <returns>找到返回关卡数据,没找到返回null</returns>
///
private static AllLevelRoot _allLevelData;
// 游戏启动时调用一次
public static void InitAllLevelData()
{
string path = Path.Combine(Application.streamingAssetsPath, "all_levels.json");
string json = File.ReadAllText(path);
_allLevelData = JsonUtility.FromJson<AllLevelRoot>(json);
}
public static LevelConfig LoadLevelById(int targetLevelId)
{
AllLevelRoot root = _allLevelData;
if (root == null || root.levels == null) return null;
// 遍历查找对应关卡
foreach (var lv in _allLevelData.levels)
{
if (lv.levelId == targetLevelId)
{
return lv;
}
}
Debug.LogWarning($"未找到关卡ID: {targetLevelId}");
return null;
}
#endregion
#region 【核心】网格坐标工具
/// <summary>点位ID 转 行列索引</summary>
private void PointIdToRowCol(int pointId, int totalCol, out int row, out int col)
{
row = pointId / totalCol;
col = pointId % totalCol;
}
/// <summary>行列 → 实际像素坐标</summary>
private Vector2 RowColToPos(int row, int col)
{
float originX = ui.point.x;
float originY = ui.point.y;
float x = originX + col * _levelConfig.pointSpacing;
float y = originY + row * _levelConfig.pointSpacing;
return new Vector2(x, y);
}
/// <summary>点位ID → 最终像素坐标</summary>
private Vector2 PointIdToPos(int pointId, int totalCol)
{
PointIdToRowCol(pointId, totalCol, out int r, out int c);
return RowColToPos(r, c);
}
/// <summary>像素坐标反向计算点位ID</summary>
private int GetPointIdByPos(Vector2 pos, LevelConfig cfg)
{
float originX = ui.point.x;
float originY = ui.point.y;
int col = Mathf.RoundToInt((pos.x - originX) / cfg.pointSpacing);
int row = Mathf.RoundToInt((pos.y - originY) / cfg.pointSpacing);
return row * cfg.gridCols + col;
}
/// <summary> 根据FGUI旋转角度获取方向 </summary>
private string GetDirByRotation(float rot)
{
rot = Mathf.Round(rot);
if (rot == 0) return "right";
if (rot == 90) return "down";
if (rot == 180) return "left";
if (rot == 270) return "up";
return "right";
}
/// <summary>方向转旋转角度(箭头默认朝上)</summary>
private float DirToRotation(string dir)
{
return dir switch
{
"up" => 0f,
"right" => 90f,
"down" => 180f,
"left" => 270f,
_ => 0f
};
}
#endregion
#region 绘制网格圆点
private void DrawGridDots(LevelConfig cfg)
{
if (_dotTemplate == null || _viewContainer == null) return;
_dotTemplate.visible = false;
var rows = cfg.gridRows;
var cols = cfg.gridCols;
for (var r = 0; r < rows; r++)
{
for (var c = 0; c < cols; c++)
{
var dot = ArrorPoint.CreateInstance();
var pos = RowColToPos(r, c);
dot.SetPosition(pos.x, pos.y, 0);
_viewContainer.AddChild(dot);
// 加入列表统一管理
_gridDotList.Add(dot);
}
}
// ========= 网格绘制完成后,设置居中缩放轴心 =========
CalculateGridCenter(cfg);
SetViewPivotToGridCenter();
}
/// <summary>计算整个网格圆点区域的世界中心点</summary>
private void CalculateGridCenter(LevelConfig cfg)
{
int rows = cfg.gridRows;
int cols = cfg.gridCols;
// 取四个角点位,算出整体中心
Vector2 topLeft = RowColToPos(0, 0);
Vector2 bottomRight = RowColToPos(rows - 1, cols - 1);
// 几何中心
_gridWorldCenter.x = (topLeft.x + bottomRight.x) * 0.5f;
_gridWorldCenter.y = (topLeft.y + bottomRight.y) * 0.5f;
}
/// <summary>设置_viewContainer缩放轴心 = 网格中心,实现居中缩放</summary>
private void SetViewPivotToGridCenter()
{
if (_viewContainer == null) return;
// FGUI pivot 取值范围 0~1,是容器内部相对比例
float pivotX = _gridWorldCenter.x / _viewContainer.width;
float pivotY = _gridWorldCenter.y / _viewContainer.height;
// 限制在合法区间
pivotX = Mathf.Clamp01(pivotX);
pivotY = Mathf.Clamp01(pivotY);
// 设置轴心,此后所有缩放都围绕网格中心
_viewContainer.pivot = new Vector2(pivotX, pivotY);
}
#endregion
#region 构建箭头路径点位
private void BuildArrowPath(ArrowConfig arrow, LevelConfig cfg)
{
arrow.pathPointList = new List<Vector2>();
int totalCol = cfg.gridCols;
int curPointId = arrow.startPoint;
Vector2 curPos = PointIdToPos(curPointId, totalCol);
arrow.pathPointList.Add(curPos);
foreach (string dir in arrow.path)
{
PointIdToRowCol(curPointId, totalCol, out int r, out int c);
switch (dir)
{
case "up": r--; break;
case "down": r++; break;
case "left": c--; break;
case "right": c++; break;
}
curPointId = r * totalCol + c;
curPos = PointIdToPos(curPointId, totalCol);
arrow.pathPointList.Add(curPos);
}
}
#endregion
#region 创建箭头 + 线条方块
private void CreateArrowAndLines(ArrowConfig arrow, LevelConfig cfg)
{
arrow.lineUnits = new List<GComponent>();
arrow.originalLinePos = new List<Vector2>();
arrow.lineTrackIndex = new List<int>();
arrow.totalTrack = new List<Vector2>();
List<Vector2> pathPoints = arrow.pathPointList;
// 生成整条连续行走轨迹
if (pathPoints.Count >= 2)
{
for (int i = 0; i < pathPoints.Count - 1; i++)
{
Vector2 segStart = pathPoints[i];
string dir = arrow.path[i];
float offX = 0, offY = 0;
switch (dir)
{
case "right": offX = 10; break;
case "left": offX = -10; break;
case "down": offY = 10; break;
case "up": offY = -10; break;
}
Vector2 cur = segStart;
for (int k = 0; k < 6; k++)
{
arrow.totalTrack.Add(cur);
cur.x += offX;
cur.y += offY;
}
}
if (arrow.path.Count > 0)
arrow.finalMoveDir = arrow.path[arrow.path.Count - 1];
}
// 路径只有单个点(纯箭头)
if (pathPoints.Count < 2)
{
CreateArrowOnly(arrow, pathPoints[0]);
// ========= 修复1:单点箭头也初始化轨迹索引 =========
arrow.arrowTrackIndex = 0;
arrow.finalMoveDir = GetDirByRotation(arrow.arrowObj.rotation);
// ========= 修复2:强制记录原始坐标(关键!)=========
arrow.originalArrowPos = new Vector2(arrow.arrowObj.x, arrow.arrowObj.y);
// 优化:给单点箭头填充一个轨迹点,和有线箭头逻辑完全一致
arrow.totalTrack.Add(pathPoints[0]);
return;
}
// 生成线条图块
List<Vector2> tilePositions = new List<Vector2>();
for (int i = 0; i < pathPoints.Count - 1; i++)
{
Vector2 segStart = pathPoints[i];
string dir = arrow.path[i];
float offX = 0, offY = 0;
switch (dir)
{
case "right": offX = 10; break;
case "left": offX = -10; break;
case "down": offY = 10; break;
case "up": offY = -10; break;
}
Vector2 cur = segStart;
for (int k = 0; k < 6; k++)
{
tilePositions.Add(cur);
cur.x += offX;
cur.y += offY;
}
}
if (tilePositions.Count > 0)
tilePositions.RemoveAt(tilePositions.Count - 1);
ColorUtility.TryParseHtmlString(arrow.color, out Color lineColor);
for (int i = 0; i < tilePositions.Count; i++)
{
Vector2 pos = tilePositions[i];
LineTile unit = LineTile.CreateInstance();
unit.SetPosition(pos.x, pos.y, 0);
unit.GetChild("line").asGraph.color = lineColor;
unit.visible = true;
unit.touchable = true;
unit.onClick.Add(() => OnPathClick(arrow));
_viewContainer.AddChild(unit);
arrow.lineUnits.Add(unit);
arrow.originalLinePos.Add(pos);
arrow.lineTrackIndex.Add(i);
}
// 创建箭头
CreateArrowOnly(arrow, pathPoints[pathPoints.Count - 1]);
arrow.originalArrowPos = new Vector2(arrow.arrowObj.x, arrow.arrowObj.y);
arrow.arrowTrackIndex = arrow.totalTrack.Count - 1;
// 在 CreateArrowAndLines 方法最后,补充这段初始化代码
// ========== 新增:初始化距离偏移 ==========
arrow.movedDistance = 0f;
// 轨迹总长度 = (点数-1) * 点间距
arrow.trackTotalLength = (arrow.totalTrack.Count - 1) * 10f;
// 箭头初始在轨迹末端
arrow.arrowInitOffset = arrow.trackTotalLength;
// 每个线条的初始偏移 = 索引 * 点间距
arrow.lineInitOffsets = new List<float>();
for (int i = 0; i < arrow.lineUnits.Count; i++)
{
arrow.lineInitOffsets.Add(i * 10f);
}
}
#endregion
#region 道具(删除和提示)
/// <summary>点击删除道具,开启/关闭删除模式</summary>
private void OnClickDeleteItem()
{
_isDeleteMode = !_isDeleteMode;
// 可选:加提示,区分状态
if (_isDeleteMode)
{
Debug.Log("已开启删除模式,点击箭头即可删除");
// 可加UI提示:如图标高亮、文字提示
}
else
{
Debug.Log("已关闭删除模式");
}
}
private void OnClickHint()
{
ShowRandomArrowTip();
}
/// <summary>随机选中有效箭头,显示静态手指提示(不跟随移动)</summary>
private void ShowRandomArrowTip()
{
// 先隐藏上一次提示
HideFingerTip();
if (_levelConfig == null || _levelConfig.arrows == null || _levelConfig.arrows.Count == 0)
{
Debug.Log("暂无可提示的箭头");
return;
}
// 筛选有效箭头
List<ArrowConfig> validArrows = _levelConfig.arrows
.Where(arrow => arrow != null && arrow.arrowObj != null)
.ToList();
if (validArrows.Count == 0)
{
Debug.Log("没有有效箭头可提示");
return;
}
// 随机选一个
int randomIndex = Random.Range(0, validArrows.Count);
_currentTipArrow = validArrows[randomIndex];
// 设置手指位置(仅初始化一次,不再刷新)
float x = _currentTipArrow.arrowObj.x + _fingerOffset.x;
float y = _currentTipArrow.arrowObj.y + _fingerOffset.y;
_fingerTipObj.SetPosition(x, y, 0);
_fingerTipObj.visible = true;
}
/// <summary>隐藏手指提示并清空标记</summary>
private void HideFingerTip()
{
if (_fingerTipObj != null)
{
_fingerTipObj.visible = false;
}
_currentTipArrow = null;
}
#endregion
#region 缩放
/// <summary>根据缩放值,计算滑动条 value(0~1)</summary>
private float GetSliderValueByScale(float scale)
{
float validScale = Mathf.Clamp(scale, _minScale, _maxScale);
float ratio = (validScale - _minScale) / (_maxScale - _minScale);
float sliderVal = ratio * 100f;
return Mathf.Clamp(sliderVal, 0f, 100f);
}
/// <summary>根据滑动条 value(0~1),计算实际缩放值</summary>
private float GetScaleBySliderValue(float sliderValue)
{
// 先把滑块值钳位在 0~100 内
float val = Mathf.Clamp(sliderValue, 0f, 100f);
// 线性映射:0→0.6100→1.5
float ratio = val / 100f;
float scale = _minScale + ratio * (_maxScale - _minScale);
return Mathf.Clamp(scale, _minScale, _maxScale);
}
/// <summary>滑动条数值变化回调</summary>
private void OnZoomSliderChange()
{
float scale = GetScaleBySliderValue((float)_zoomSlider.value);
_viewContainer.scaleX = scale;
_viewContainer.scaleY = scale;
}
/// <summary>移动容器 + 边界限制,防止完全拖出屏幕</summary>
private void MoveContainer(Vector2 offset)
{
Debug.Log($"[arrow] 移动容器:{offset}");
float newX = _viewContainer.x + offset.x;
float newY = _viewContainer.y + offset.y;
float viewW = _viewContainer.width * _viewContainer.scaleX;
float viewH = _viewContainer.height * _viewContainer.scaleY;
// 左右边界
float minX = -viewW + _dragBorder;
float maxX = Screen.width - _dragBorder;
newX = Mathf.Clamp(newX, minX, maxX);
// 上下边界
float minY = -viewH + _dragBorder;
float maxY = Screen.height - _dragBorder;
newY = Mathf.Clamp(newY, minY, maxY);
_viewContainer.SetPosition(newX, newY,0);
}
#endregion
#region 点击 & 移动逻辑
// 线条/箭头 统一点击回调
private void OnPathClick(ArrowConfig clickArrow)
{
if (clickArrow.isMoving)
{
Debug.Log($"[arrow] {clickArrow.id}正在移动,请勿点击其他箭头");
return;
}
// 点击的是当前提示箭头,直接隐藏手指
if (_currentTipArrow == clickArrow)
{
HideFingerTip();
}
// ========= 删除模式:直接删除箭头,不走移动逻辑 =========
if (_isDeleteMode)
{
DeleteArrowCompletely(clickArrow);
return;
}
clickArrow.isMoving = true;
bool hasBlock = CheckBlockByGrid(clickArrow);
Debug.Log("[arrow] 点击箭头:" + clickArrow.id +$"hasBlock==={hasBlock}");
if (hasBlock)
{
MoveThenReset(clickArrow);
}
else
{
MoveToDisappear(clickArrow);
}
}
// 基于网格行列的阻挡检测
private bool CheckBlockByGrid(ArrowConfig curArrow)
{
if (_gridOccupied == null || _levelConfig == null) return false;
// 当前箭头网格行列
Vector2 arrowPos = new Vector2(curArrow.arrowObj.x, curArrow.arrowObj.y);
int curPointId = GetPointIdByPos(arrowPos, _levelConfig);
PointIdToRowCol(curPointId, _levelConfig.gridCols, out int curRow, out int curCol);
// 超出网格直接判定无阻挡
if (curRow < 0 || curRow >= _levelConfig.gridRows || curCol < 0 || curCol >= _levelConfig.gridCols)
return false;
// 获取移动方向
string moveDir;
if (curArrow.path != null && curArrow.path.Count > 0)
{
moveDir = curArrow.path[curArrow.path.Count - 1];
}
else if (!string.IsNullOrEmpty(curArrow.defaultDir))
{
moveDir = curArrow.defaultDir;
}
else
{
moveDir = "up";
}
bool isCollide = false;
// 沿方向遍历前方网格
switch (moveDir)
{
case "up":
for (int r = curRow - 1; r >= 0; r--)
{
if (_gridOccupied[r, curCol])
{
isCollide = true;
break;
}
}
break;
case "down":
for (int r = curRow + 1; r < _levelConfig.gridRows; r++)
{
if (_gridOccupied[r, curCol])
{
isCollide = true;
break;
}
}
break;
case "left":
for (int c = curCol - 1; c >= 0; c--)
{
if (_gridOccupied[curRow, c])
{
isCollide = true;
break;
}
}
break;
case "right":
for (int c = curCol + 1; c < _levelConfig.gridCols; c++)
{
if (_gridOccupied[curRow, c])
{
isCollide = true;
break;
}
}
break;
}
// ========== 新增:碰撞判重 + 扣爱心 ==========
if (isCollide)
{
// 这条线第一次碰撞,才扣血
if (!_collidedArrows.Contains(curArrow))
{
_collidedArrows.Add(curArrow);
_heartCount--;
if (_heartCount > 0) ui.HeartsPanel.GetChild($"xin_{_heartCount+1}").visible = false;
// 爱心归零 → 失败结算
if (_heartCount <= 0)
{
ShowResult(false);
}
}
}
return isCollide;
}
/// <summary>彻底删除指定箭头:销毁UI+清数据+停逻辑+移除列表</summary>
private void DeleteArrowCompletely(ArrowConfig arrow)
{
if (arrow == null) return;
// 被删除的是提示箭头,隐藏手指
if (_currentTipArrow == arrow)
{
HideFingerTip();
}
// 1. 停止移动标记,拦截Update和协程
arrow.isMoving = false;
// 2. 销毁箭头中所有线条UI
if (arrow.lineUnits != null)
{
foreach (var line in arrow.lineUnits)
{
if (line != null)
{
_viewContainer.RemoveChild(line);
line.Dispose();
}
}
arrow.lineUnits.Clear();
}
// 3. 销毁箭头本体UI
if (arrow.arrowObj != null)
{
_viewContainer.RemoveChild(arrow.arrowObj);
arrow.arrowObj.Dispose();
arrow.arrowObj = null;
}
// 4. 释放网格占用数据(和飞出边界逻辑保持一致)
ReleaseArrowOccupied(arrow);
// 5. 存活箭头计数-1
_remainArrowCount--;
// 6. 从总列表移除该箭头(核心:删除数据源)
if (_levelConfig != null && _levelConfig.arrows != null)
{
_levelConfig.arrows.Remove(arrow);
}
// 7. 可选:判断是否全部删除完成,触发结算(按需开启)
if (_remainArrowCount <= 0)
{
ShowResult(true);
}
else
{
// 消除每条线小概率触发
DisappearLineGetReward();
}
// 8. 退出删除模式
_isDeleteMode = false;
Debug.Log("[arrow] 箭头已彻底删除");
}
// 创建箭头 + 绑定点击
private void CreateArrowOnly(ArrowConfig arrow, Vector2 arrowPos)
{
ArrowEnd arrowIns = ArrowEnd.CreateInstance();
arrowIns.SetPosition(arrowPos.x, arrowPos.y, 0);
arrowIns.touchable = true;
if (arrow.path != null && arrow.path.Count > 0)
{
string lastDir = arrow.path[arrow.path.Count - 1];
arrowIns.rotation = DirToRotation(lastDir);
}
else if (!string.IsNullOrEmpty(arrow.defaultDir))
{
// 纯单点箭头 使用默认方向旋转
arrowIns.rotation = DirToRotation(arrow.defaultDir);
}
ColorUtility.TryParseHtmlString(arrow.color, out Color arrowColor);
GImage icon = arrowIns.GetChild("icon").asImage;
if (icon != null) icon.color = arrowColor;
arrowIns.visible = true;
arrowIns.onClick.Add(() => OnPathClick(arrow));
_viewContainer.AddChild(arrowIns);
arrow.arrowObj = arrowIns;
arrow.isMoving = false;
}
// 移动后复位动画(回弹,不释放占用)
private void MoveThenReset(ArrowConfig arrow)
{
RunCoroutine(MoveAndResetCoroutine(arrow));
}
// 回弹速度单独配置,和移动速度分开
private float _bounceSpeed = 180f;
private IEnumerator MoveAndResetCoroutine(ArrowConfig arrow)
{
arrow.movedDistance = 0f;
arrow.finalMoveDir = GetFinalMoveDir(arrow);
// 向前移动的总距离(对应原逻辑2步=20px,可自行调整)
float forwardTotal = 20f;
float timer = 0f;
float duration = forwardTotal / _bounceSpeed;
// 第一段:向前移动
while (timer < duration)
{
if (arrow == null || arrow.arrowObj == null) yield break;
timer += Time.deltaTime;
arrow.movedDistance = _bounceSpeed * timer;
UpdateAllElementsPosition(arrow);
yield return null;
}
// 第二段:回弹到初始位置
timer = 0f;
float startDis = arrow.movedDistance;
while (timer < duration)
{
if (arrow == null || arrow.arrowObj == null) yield break;
timer += Time.deltaTime;
float t = timer / duration;
arrow.movedDistance = Mathf.Lerp(startDis, 0f, t);
UpdateAllElementsPosition(arrow);
yield return null;
}
// 强制复位到初始状态
arrow.movedDistance = 0f;
UpdateAllElementsPosition(arrow);
arrow.isMoving = false;
}
/// <summary>统一更新箭头+所有线条的位置</summary>
private void UpdateAllElementsPosition(ArrowConfig arrow)
{
for (int i = 0; i < arrow.lineUnits.Count; i++)
{
float curDist = arrow.lineInitOffsets[i] + arrow.movedDistance;
Vector2 pos = GetPositionByDistance(arrow, curDist);
arrow.lineUnits[i].SetPosition(pos.x, pos.y, 0);
}
float arrowCurDist = arrow.arrowInitOffset + arrow.movedDistance;
Vector2 arrowPos = GetPositionByDistance(arrow, arrowCurDist);
arrow.arrowObj.SetPosition(arrowPos.x, arrowPos.y, 0);
}
private void MoveToDisappear(ArrowConfig arrow)
{
RunCoroutine(MoveAndDisappearCoroutine(arrow));
}
// 新增:统一移动速度(像素/秒),数值越大移动越快,可自由修改
[SerializeField] private float _moveSpeed = 800f;
private IEnumerator MoveAndDisappearCoroutine(ArrowConfig arrow)
{
arrow.movedDistance = 0f;
arrow.finalMoveDir = GetFinalMoveDir(arrow);
while (true)
{
// 空对象兜底保护
if (arrow == null || arrow.arrowObj == null)
{
yield break;
}
// 每帧累计移动距离:速度 * 时间增量
float deltaDis = _moveSpeed * Time.deltaTime;
arrow.movedDistance += deltaDis;
// 更新所有线条位置
for (int i = 0; i < arrow.lineUnits.Count; i++)
{
float curDist = arrow.lineInitOffsets[i] + arrow.movedDistance;
Vector2 pos = GetPositionByDistance(arrow, curDist);
arrow.lineUnits[i].SetPosition(pos.x, pos.y, 0);
}
// 更新箭头位置
float arrowCurDist = arrow.arrowInitOffset + arrow.movedDistance;
Vector2 arrowPos = GetPositionByDistance(arrow, arrowCurDist);
arrow.arrowObj.SetPosition(arrowPos.x, arrowPos.y, 0);
// 边界检测:以尾部线条为准(和原逻辑一致)
Vector2 tailPos = GetPositionByDistance(arrow, arrow.movedDistance);
if (CheckIsOutOfBounds(tailPos, arrow.finalMoveDir))
{
// ========== 以下销毁逻辑和原代码保持一致 ==========
ReleaseArrowOccupied(arrow);
foreach (var line in arrow.lineUnits)
{
if (line != null)
{
_viewContainer.RemoveChild(line);
line.Dispose();
}
}
arrow.lineUnits.Clear();
if (arrow.arrowObj != null)
{
_viewContainer.RemoveChild(arrow.arrowObj);
arrow.arrowObj.Dispose();
arrow.arrowObj = null;
}
arrow.isMoving = false;
_remainArrowCount--;
if (_remainArrowCount <= 0)
{
ShowResult(true);
}
else
{
DisappearLineGetReward();
}
yield break;
}
yield return null; // 每帧执行一次,平滑移动
}
}
#region 移动工具方法
/// <summary>根据距离,计算沿轨迹的实际坐标</summary>
private Vector2 GetPositionByDistance(ArrowConfig arrow, float distance)
{
int trackCount = arrow.totalTrack.Count;
const float segmentLen = 10f; // 轨迹点固定间距
// 距离在轨迹范围内:两点插值得到精确位置
if (distance <= arrow.trackTotalLength)
{
float indexFloat = distance / segmentLen;
int index0 = Mathf.FloorToInt(indexFloat);
int index1 = Mathf.Min(index0 + 1, trackCount - 1);
float t = indexFloat - index0;
Vector2 p0 = arrow.totalTrack[index0];
Vector2 p1 = arrow.totalTrack[index1];
return Vector2.Lerp(p0, p1, t);
}
// 距离超出轨迹:沿最终方向直线延伸
else
{
float extraDis = distance - arrow.trackTotalLength;
Vector2 endPos = arrow.totalTrack[trackCount - 1];
Vector2 dir = DirToVector2(arrow.finalMoveDir);
return endPos + dir * extraDis;
}
}
/// <summary>方向字符串转单位向量</summary>
/// <summary>方向字符串转单位向量(适配 FGUI 坐标系:y 轴向下)</summary>
private Vector2 DirToVector2(string dir)
{
return dir switch
{
"up" => new Vector2(0, -1), // FGUI 向上 = y 减小
"down" => new Vector2(0, 1), // FGUI 向下 = y 增大
"left" => new Vector2(-1, 0),
"right" => new Vector2(1, 0),
_ => new Vector2(0, -1)
};
}
/// <summary>统一获取箭头最终移动方向</summary>
private string GetFinalMoveDir(ArrowConfig arrow)
{
if (arrow.path != null && arrow.path.Count > 0)
return arrow.path[arrow.path.Count - 1];
if (!string.IsNullOrEmpty(arrow.defaultDir))
return arrow.defaultDir;
return "up";
}
/// <summary>统一边界检测</summary>
private bool CheckIsOutOfBounds(Vector2 pos, string dir)
{
switch (dir)
{
case "right": return pos.x > gridRight + outOffset;
case "left": return pos.x < gridLeft - outOffset;
case "down": return pos.y > gridBottom + outOffset;
case "up": return pos.y < gridTop - outOffset;
default: return false;
}
}
#endregion
/// <summary>
/// 消除每条线时概率获得奖励
/// </summary>
private void DisappearLineGetReward()
{
bool isGet = false;
int money_rate = ConfigSystem.GetConfig<CommonModel>().rewardrate;
var randomNum = UnityEngine.Random.Range(0, 100);
// Debug.Log($"[creat] money_rate------------ {randomNum}==={money_rate}");
if (GameHelper.IsGiftSwitch() && randomNum < money_rate)
{
isGet = true;
// 显示大额奖励
float[] cash_array = GameHelper.GetRewardValue(1);
var temp = new SuccessData();
temp.IsWin = true;
temp.cash_number = cash_array[0];
temp.rate = (int)cash_array[1];
temp.IsLevelSuccess = false;
temp.IsH5Reward = false;
temp.boost_array = GameHelper.GetRewardBoost(1);
DOVirtual.DelayedCall(0.55f, () =>
{
UICtrlDispatcher.Instance.Dispatch(UICtrlMsg.LevelSuccessUI_Open, temp);
});
}
if (!isGet && Random.Range(0, 100) < ConfigSystem.GetConfig<CommonModel>().Smallrewardsrate)
{
var rewardData = new RewardData();
Vector2 fguiPosition = new Vector2(ui.samll_point.x, ui.samll_point.y);
var start = fguiPosition;
var end = GameHelper.GetUICenterPosition(ui.com_money.GetChild("text_gold"));
float[] cash_array = GameHelper.GetRewardValue(0);
Debug.Log($"小额奖励:{cash_array[0]}");
var rewardSingleData = new RewardSingleData(102, (decimal)cash_array[0], RewardOrigin.AdTask)
{
startPosition = start,
endPosition = new Vector2(end.x - 135, end.y - 135)
};
rewardData.AddReward(rewardSingleData);
rewardData.displayType = RewardDisplayType.RewardFly | RewardDisplayType.ValueChange;
GameDispatcher.Instance.Dispatch(GameMsg.GetReward, rewardData);
}
}
#endregion
#region 资源清理
/// <summary>全局启动协程 + 自动记录,关闭界面时统一停止</summary>
private Coroutine RunCoroutine(IEnumerator cor)
{
if (cor == null) return null;
Coroutine co = CrazyAsyKit.StartCoroutine(cor);
if (co != null)
{
_activeCoroutines.Add(co);
}
return co;
}
private void ClearAllDynamicObj()
{
if (_levelConfig == null || _viewContainer == null) return;
// 销毁网格圆点
foreach (var dot in _gridDotList)
{
if (dot != null)
{
_viewContainer.RemoveChild(dot);
dot.Dispose();
}
}
_gridDotList.Clear();
foreach (var arrow in _levelConfig.arrows)
{
if (arrow.arrowObj != null)
{
_viewContainer.RemoveChild(arrow.arrowObj);
arrow.arrowObj.Dispose();
arrow.arrowObj = null;
}
foreach (var line in arrow.lineUnits)
{
_viewContainer.RemoveChild(line);
line.Dispose();
}
arrow.lineUnits.Clear();
}
}
#endregion
#region 结算
/// <summary>
/// 弹出结算
/// isSuccess: true=成功,false=失败
/// </summary>
private void ShowResult(bool isSuccess)
{
// 暂停所有交互/移动(可选)
foreach (var arrow in _levelConfig.arrows)
{
arrow.isMoving = false;
}
// uiCtrlDispatcher.Dispatch(UICtrlMsg.LevelSuccessUI_Open, isSuccess);
if (isSuccess)
{
GameHelper.SetLevel(GameHelper.GetLevel() + 1);
//通关成功
float[] cash_array = GameHelper.GetRewardValue(2);
var temp = new SuccessData();
temp.IsWin = true;
temp.cash_number = cash_array[0];
temp.rate = (int)cash_array[1];
temp.IsLevelSuccess = true;
temp.IsH5Reward = false;
temp.boost_array = GameHelper.GetRewardBoost(2);
DOVirtual.DelayedCall(0.5f, () =>
{
UICtrlDispatcher.Instance.Dispatch(UICtrlMsg.LevelSuccessUI_Open, temp);
});
}
else
{
//通关失败
float[] cash_array = GameHelper.GetRewardValue(2);
var temp = new SuccessData();
temp.IsWin = false;
temp.cash_number = cash_array[0];
temp.IsLevelSuccess = true;
temp.IsH5Reward = false;
temp.boost_array = GameHelper.GetRewardBoost(2);
DOVirtual.DelayedCall(0.5f, () =>
{
UICtrlDispatcher.Instance.Dispatch(UICtrlMsg.LevelSuccessUI_Open, temp);
});
}
}
/// <summary>
/// 重新开始本局
/// </summary>
/// <param name="a">true: 重生 false:重开一局或者下一局 </param>
private void OnRestartGame(object a = null)
{
Debug.Log($"[ArrowGameUI] 重新开始本局 a:{(bool)a}");
if (a != null && (bool)a)
{
_heartCount = 3;
for (int i = _heartCount; i > 0; i--)
{
ui.HeartsPanel.GetChild($"xin_{i}").visible = true;
}
return;
}
// 先清空当前所有动态物体
ClearAllDynamicObj();
// 重新走初始化流程
InitGridBounds();
InitGridOccupied();
_heartCount = 3;
_collidedArrows.Clear();
_remainArrowCount = _levelConfig.arrows.Count;
DataMgr.ArrowResultLevel.Value = 1;
InitView();
}
#endregion
}
#region 配置解析 & 数据结构
public static class JsonHelper
{
public static LevelConfig LoadLevel(string fileName)
{
string path = Path.Combine(Application.streamingAssetsPath, fileName);
return JsonUtility.FromJson<LevelConfig>(File.ReadAllText(path));
}
}
[System.Serializable]
public class LevelConfig
{
public int levelId;
public string levelName;
public int gridRows;
public int gridCols;
public int pointSpacing;
public List<ArrowConfig> arrows;
}
[System.Serializable]
public class ArrowConfig
{
public int id;
public int startPoint;
public int endPoint;
public string color;
public List<string> path;
public string defaultDir;
public List<Vector2> pathPointList;
public List<GComponent> lineUnits;
public GComponent arrowObj;
public bool isMoving;
public List<Vector2> originalLinePos;
public Vector2 originalArrowPos;
public List<Vector2> totalTrack;
public List<int> lineTrackIndex;
public int arrowTrackIndex;
public string finalMoveDir;
// ========== 新增:距离驱动移动所需字段 ==========
public float movedDistance; // 当前整体累计移动距离
public float trackTotalLength; // 整条轨迹的总像素长度
public List<float> lineInitOffsets; // 每个线条单元的初始距离偏移
public float arrowInitOffset; // 箭头的初始距离偏移
}
[System.Serializable]
public class AllLevelRoot
{
public List<LevelConfig> levels;
}
#endregion
}