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> _arrowOccupiedCells = new Dictionary>(); // 爱心数量 private int _heartCount = 3; // 记录已经发生过碰撞的箭头对象(去重:同个箭头多次碰撞只扣一次血) private HashSet _collidedArrows = new HashSet(); // 统计场上还存在的箭头总数(用于判断全部移除=通关) private int _remainArrowCount = 0; // 记录当前UI所有由 CrazyAsyKit 启动的协程,用于统一停止 private List _activeCoroutines = new List(); // 是否开启删除道具模式 private bool _isDeleteMode = false; // 手指提示图标 private com_finger _fingerTipObj; // 当前正在提示的箭头 private ArrowConfig _currentTipArrow; // 手指图标偏移(微调位置) private readonly Vector2 _fingerOffset = new Vector2(-60, -60); #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(); // 关闭按钮 ui.btn_close.SetClick(() => { CtrlCloseUI(); }); ui.btn_clear.SetClick(OnClickDeleteItem); ui.btn_hint.SetClick(OnClickHint); _fingerTipObj = com_finger.CreateInstance(); _viewContainer.AddChild(_fingerTipObj); _fingerTipObj.visible = false; _currentTipArrow = null; _zoomSlider = ui.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().GetData(makeupTaskData.tableId); if (vo == null) { return; } GameHelper.showGameUI = false; uiCtrlDispatcher.Dispatch(UICtrlMsg.MakeupConfirmUI_Open, makeupTaskData); } #endregion #region UI相关 /// /// 切换模式 默认黑夜模式 /// /// true:白天 false:黑夜 private void SetModel(object mod = null) { int idx = 0; if (mod != null) { idx = (bool)mod? 1 : 0; } ui.mode.selectedIndex = idx; } #endregion private void InitGridBounds() { gridLeft = ui.point.x; gridRight = ui.point.x + gridColNum * pointSpace; gridTop = ui.point.y; gridBottom = ui.point.y + gridRowNum * pointSpace; } /// 初始化网格占用二维数组 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(); } /// 标记单个箭头占用的所有网格点 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; } /// 释放单个箭头占用的网格(箭头消失时调用) 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; } } } /// /// 从 all_levels.json 中按 levelId 读取指定关卡 /// /// 目标关卡ID 如 1001 /// 找到返回关卡数据,没找到返回null /// 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(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 【核心】网格坐标工具 /// 点位ID 转 行列索引 private void PointIdToRowCol(int pointId, int totalCol, out int row, out int col) { row = pointId / totalCol; col = pointId % totalCol; } /// 行列 → 实际像素坐标 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); } /// 点位ID → 最终像素坐标 private Vector2 PointIdToPos(int pointId, int totalCol) { PointIdToRowCol(pointId, totalCol, out int r, out int c); return RowColToPos(r, c); } /// 像素坐标反向计算点位ID 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; } /// 根据FGUI旋转角度获取方向 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"; } /// 方向转旋转角度(箭头默认朝上) 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); } } // ========= 新增:网格绘制完成后,设置居中缩放轴心 ========= CalculateGridCenter(cfg); SetViewPivotToGridCenter(); } /// 计算整个网格圆点区域的世界中心点 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; } /// 设置_viewContainer缩放轴心 = 网格中心,实现居中缩放 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(); 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(); arrow.originalLinePos = new List(); arrow.lineTrackIndex = new List(); arrow.totalTrack = new List(); List 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 tilePositions = new List(); 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(); for (int i = 0; i < arrow.lineUnits.Count; i++) { arrow.lineInitOffsets.Add(i * 10f); } } #endregion #region 道具(删除和提示) /// 点击删除道具,开启/关闭删除模式 private void OnClickDeleteItem() { _isDeleteMode = !_isDeleteMode; // 可选:加提示,区分状态 if (_isDeleteMode) { Debug.Log("已开启删除模式,点击箭头即可删除"); // 可加UI提示:如图标高亮、文字提示 } else { Debug.Log("已关闭删除模式"); } } private void OnClickHint() { ShowRandomArrowTip(); } /// 随机选中有效箭头,显示静态手指提示(不跟随移动) private void ShowRandomArrowTip() { // 先隐藏上一次提示 HideFingerTip(); if (_levelConfig == null || _levelConfig.arrows == null || _levelConfig.arrows.Count == 0) { Debug.Log("暂无可提示的箭头"); return; } // 筛选有效箭头 List 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; } /// 隐藏手指提示并清空标记 private void HideFingerTip() { if (_fingerTipObj != null) { _fingerTipObj.visible = false; } _currentTipArrow = null; } #endregion #region 缩放 /// 根据缩放值,计算滑动条 value(0~1) 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); } /// 根据滑动条 value(0~1),计算实际缩放值 private float GetScaleBySliderValue(float sliderValue) { // 先把滑块值钳位在 0~100 内 float val = Mathf.Clamp(sliderValue, 0f, 100f); // 线性映射:0→0.6,100→1.5 float ratio = val / 100f; float scale = _minScale + ratio * (_maxScale - _minScale); return Mathf.Clamp(scale, _minScale, _maxScale); } /// 滑动条数值变化回调 private void OnZoomSliderChange() { float scale = GetScaleBySliderValue((float)_zoomSlider.value); _viewContainer.scaleX = scale; _viewContainer.scaleY = scale; } /// 移动容器 + 边界限制,防止完全拖出屏幕 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; } /// 彻底删除指定箭头:销毁UI+清数据+停逻辑+移除列表 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; } /// 统一更新箭头+所有线条的位置 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 移动工具方法 /// 根据距离,计算沿轨迹的实际坐标 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; } } /// 方向字符串转单位向量 /// 方向字符串转单位向量(适配 FGUI 坐标系:y 轴向下) 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) }; } /// 统一获取箭头最终移动方向 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"; } /// 统一边界检测 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 /// /// 消除每条线时概率获得奖励 /// private void DisappearLineGetReward() { bool isGet = false; int money_rate = ConfigSystem.GetConfig().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().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 资源清理 /// 全局启动协程 + 自动记录,关闭界面时统一停止 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 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 结算 /// /// 弹出结算 /// isSuccess: true=成功,false=失败 /// 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); }); } } /// /// 重新开始本局 /// /// true: 重生 false:重开一局或者下一局 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(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 arrows; } [System.Serializable] public class ArrowConfig { public int id; public int startPoint; public int endPoint; public string color; public List path; public string defaultDir; public List pathPointList; public List lineUnits; public GComponent arrowObj; public bool isMoving; public List originalLinePos; public Vector2 originalArrowPos; public List totalTrack; public List lineTrackIndex; public int arrowTrackIndex; public string finalMoveDir; // ========== 新增:距离驱动移动所需字段 ========== public float movedDistance; // 当前整体累计移动距离 public float trackTotalLength; // 整条轨迹的总像素长度 public List lineInitOffsets; // 每个线条单元的初始距离偏移 public float arrowInitOffset; // 箭头的初始距离偏移 } [System.Serializable] public class AllLevelRoot { public List levels; } #endregion }