using System; 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; using btn_saveingpot = FGUI.Common_01.btn_saveingpot; using Random = UnityEngine.Random; namespace ChillConnect { public class ArrowGameUI : BaseUI { private ArrowGameUICtrl ctrl; private com_arrow_game ui; // FGUI界面根容器 private GComponent _viewContainer; // 关卡总配置数据 private LevelConfig _levelConfig; // 网格基础参数 private int gridColNum = 10; private int gridRowNum = 10; private readonly float pointSpace = 60f; private readonly float outOffset = 200f; // 网格边界 private Vector2 _gridWorldCenter; // 网格世界中心点 private float gridLeft; private float gridRight; 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(-20, -20); // 所有网格圆点实例列表,用于批量改色/清理 private List _gridDotList = new List(); #region 箭头颜色配置 // 8种预设彩色,可按需调整色值 private readonly Color[] _colorfulPalette = new Color[] { new Color(1f, 0.3f, 0.3f), // 红 new Color(1f, 0.6f, 0.2f), // 橙 new Color(1f, 0.9f, 0.2f), // 黄 new Color(0.3f, 0.8f, 0.4f), // 绿 new Color(0.2f, 0.8f, 0.9f), // 青 new Color(0.3f, 0.5f, 1f), // 蓝 new Color(0.7f, 0.4f, 1f), // 紫 new Color(1f, 0.5f, 0.8f) // 粉 }; #endregion #region 缩放、拖拽、滑动条 全局变量 // 缩放阈值(滑动条左右边界对应这两个值) private readonly float _minScale = 0.5f; 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 = -300f; #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.Bottom; uiInfo.isNeedOpenAnim = false; uiInfo.isNeedCloseAnim = false; uiInfo.isNeedUIMask = false; 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(); _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); } #region 全局计数式开关 // 全局开关(true: 恢复输入 false:禁用输入) private void OpenOrStopGameInput(object obj = null) { if (obj != null && !(bool)obj) { // 禁用输入 DisableGameInput(); } else { // 恢复输入 EnableGameInput(); } } private int _inputDisableCount = 0; // 禁用输入 private void DisableGameInput() { _inputDisableCount++; // 只有第一次禁用时执行清理 if (_inputDisableCount == 1) { if (_viewContainer != null) _viewContainer.touchable = false; _isDraging = false; _isTwoFingerTouch = false; _lastTouchPos = Vector2.zero; } } // 恢复输入 private void EnableGameInput(object obj = null) { _inputDisableCount = Mathf.Max(0, _inputDisableCount - 1); // 计数归0时才真正恢复 if (_inputDisableCount == 0) { if (_viewContainer != null) _viewContainer.touchable = true; } } #endregion public override void OnUpdate() { // 禁用输入时,直接跳过拖拽 if (_inputDisableCount > 0) { // Debug.Log($"输入已禁用,直接跳过拖拽"); return; } // 初始化未完成,直接跳过拖拽 if (!_initComplete) return; #if UNITY_EDITOR float moveThreshold = 1f; // 鼠标按下:仅在拖拽区域内按下才开启拖拽 if (InputHelper.Instance.GetMouseButtonDown(0)) { Vector2 unityPos = InputHelper.Instance.MousePosition; Vector2 fguiPos = UnityMouseToFGUIPos(unityPos); if (IsInDragArea(fguiPos)) { _lastTouchPos = fguiPos; _isDraging = true; // Debug.Log("鼠标按下,在拖拽区域内,开始拖拽"); } return; } // 拖拽中:原有逻辑不变 if (_isDraging && InputHelper.Instance.GetMouseButton(0)) { Vector2 unityPos = InputHelper.Instance.MousePosition; Vector2 curMousePos = UnityMouseToFGUIPos(unityPos); Vector2 offset = curMousePos - _lastTouchPos; 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); // 触摸坐标和鼠标同属Unity屏幕坐标系,统一转成FGUI坐标系 Vector2 curTouchPos = UnityMouseToFGUIPos(touch.position); if (touch.phase == TouchPhase.Began) { // 只有在拖拽区域内按下,才开启拖拽 if (IsInDragArea(curTouchPos)) { _lastTouchPos = curTouchPos; _isDraging = true; _isTwoFingerTouch = false; } // 只有点击在网格范围内才触发震动 // if (IsInGridArea(curTouchPos)) // { // TriggerVibration(); // } } 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) { if (Screen.safeArea.y != 0) {//刘海屏 ui.panel.y += Screen.safeArea.y - 25; ui.com_money.y += Screen.safeArea.y- 15; ui.com_gem.y += Screen.safeArea.y - 15; ui.btn_signin.y += Screen.safeArea.y - 25; ui.com_gift.y += Screen.safeArea.y - 15; ui.btn_wv.y += Screen.safeArea.y - 25; } ui.state.selectedIndex = GameHelper.IsGiftSwitch() ? 1 : 0; //加载配置 // InitAllLevelData(); LoadLevelConfig(); _viewContainer = ui.view_container_parent.panel; InitGridBounds(); // 初始化网格占用数组 InitGridOccupied(); // ========== 新增:对局状态重置 ========== _heartCount = 3; _collidedArrows.Clear(); if(_levelConfig != null) _remainArrowCount = _levelConfig.arrows.Count; InitView(); ((com_money)ui.com_money).btn_ch.title = GameHelper.getDesByKey("ch_out_1"); if (GameHelper.IsGiftSwitch() && ConfigSystem.GetConfig().PiggyBankSwitch == 1) { ui.btn_saveingpot.visible = true; ui.com_gift.visible = true; } GameHelper.IsShowFirstReward(); GameHelper.ShowStatementView(); } private void RefreshGift(object obj = null) { ui.com_gift.text_gift.text = GameHelper.Get102Str((decimal)SaveData.GetSaveObject().saveingpot_ch); } private void Set101(decimal coin = -1) { if (coin < 0) { coin = DataMgr.Coin.Value; } if (ui.com_gem is com_gold btnCoin) btnCoin.text_gold.text = $"{coin:N0}"; } private void Set102() { decimal coin = DataMgr.Ticket.Value; if (ui.com_money is com_money comMoney) comMoney.text_gold.text = coin.ToString("0.00"); } public void OnUpdate101(object obj = null) { Set101(); } public void OnUpdate102(object obj = null) { Set102(); } 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); GameDispatcher.Instance.AddListener(GameMsg.ThemeChange, SetModel); GameDispatcher.Instance.AddListener(GameMsg.UpdateSpeed, SetSpeed); GameDispatcher.Instance.AddListener(GameMsg.UseProps, SetUserPorp); GameDispatcher.Instance.AddListener(GameMsg.StopArrowTouch,OpenOrStopGameInput); GameDispatcher.Instance.AddListener(GameMsg.refreshGift,RefreshGift); } protected override void RemoveListener() { GameDispatcher.Instance.RemoveListener(GameMsg.reset_game, OnRestartGame); GameDispatcher.Instance.RemoveListener(GameMsg.Update102, SetTopCurr); GameDispatcher.Instance.RemoveListener(GameMsg.Update101, SetTopCurr); GameDispatcher.Instance.RemoveListener(GameMsg.ThemeChange, SetModel); GameDispatcher.Instance.RemoveListener(GameMsg.UpdateSpeed, SetSpeed); GameDispatcher.Instance.RemoveListener(GameMsg.UseProps, SetUserPorp); GameDispatcher.Instance.RemoveListener(GameMsg.StopArrowTouch, OpenOrStopGameInput); GameDispatcher.Instance.RemoveListener(GameMsg.refreshGift,RefreshGift); } #endregion #region 页面初始化 private bool _initComplete = false; private void InitView() { // 重置手势状态 _isTwoFingerTouch = false; _isDraging = false; ui.text_level.SetVar("lv", GameHelper.GetLevel().ToString()).FlushVars(); // 按钮点击事件绑定 OnBtnClickBindEvent(); //爱心重新更新显示 for (int i = _heartCount; i > 0; i--) { ui.HeartsPanel.GetChild($"xin_{i}").visible = true; } // ui.ch_progress.value = DataMgr.ArrowChProgress.Value; _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); // ===== 新增:计算并应用初始缩放 ===== float initialScale = CalculateInitialScale(); _viewContainer.scaleX = initialScale; _viewContainer.scaleY = initialScale; // 同步滑动条位置(假设滑动条映射范围为 _minScale~_maxScale) float sliderValue = GetSliderValueByScale(initialScale); _zoomSlider.value = Mathf.Clamp(sliderValue, 0f, 100f); // 确保不越界 // ========== 核心:把网格中心钉在屏幕中心 ========== // _gridWorldCenter 是 DrawGridDots 里 CalculateGridCenter 算出的值 float screenCenterX = ui.view_container_parent.width * 0.5f; float screenCenterY = ui.view_container_parent.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; SetModel(); SetTopCurr(); Set101(); RefreshGift(); _fingerTipObj = com_finger.CreateInstance(); _viewContainer.AddChild(_fingerTipObj); _fingerTipObj.visible = false; _fingerTipObj.touchable = false; _fingerTipObj.sortingOrder = 9999; if (GameHelper.IsShowOpenGameUI() && HallManager.Instance.openTipsTimes >= 1) { UICtrlDispatcher.Instance.Dispatch(UICtrlMsg.OpenGameUI_Open); } } private float CalculateInitialScale() { if (_levelConfig == null) return 0.8f; // 保底 int cols = _levelConfig.gridCols; int rows = _levelConfig.gridRows; float spacing = _levelConfig.pointSpacing; // 网格实际像素宽高 float gridWidth = (cols - 1) * spacing; float gridHeight = (rows - 1) * spacing; // 容器有效尺寸(可减去固定边距,如 40px) float margin = 80f; float containerWidth = ui.view_container_parent.width - margin * 2; float containerHeight = ui.view_container_parent.height - margin * 2; // 边界保护(网格可能只有一行/列) if (gridWidth <= 0) gridWidth = 1f; if (gridHeight <= 0) gridHeight = 1f; float scaleX = containerWidth / gridWidth; float scaleY = containerHeight / gridHeight; float scale = Mathf.Min(scaleX, scaleY); // 限制上限(避免网格过大时缩放太小;下限由滑动条范围决定,建议调整 _minScale) scale = Mathf.Clamp(scale, _minScale, _maxScale); // 下限设 0.3 更合理 return scale; } 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(); Debug.Log($"makeupTaskData============{DataMgr.MakeupTaskHistory.Value.Count} {makeupTaskData.tableId}"); if (DataMgr.MakeupTaskHistory.Value.Count > 0 && makeupTaskData.tableId <= 0) { makeupTaskData.tableId = DataMgr.MakeupTaskHistory.Value.Count; } var vo = ConfigSystem.GetConfig().GetData(makeupTaskData.tableId); if (vo == null) { Debug.LogError("MakeupTaskData 为空"); 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_setting.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.PersonViewUI_Open, _moveSpeed); }); if (!GameHelper.IsGiftSwitch() && ConfigSystem.GetConfig().WVswitch == 0) { ui.btn_wv.visible = false; } ui.btn_wv.visible = false; // // ui.btn_wv.SetClick(() => // { // SDKOpenConfig openConfig = new SDKOpenConfig // { // normal = true, // url = "" // }; // CtrlDispatcher.Instance.Dispatch(CtrlMsg.open_wb,openConfig); // }); // 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); }); if (ConfigSystem.GetConfig().StatementSwitch == 1) { ui.btn_statement.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.StatementViewUI_Open); }); } else { HallManager.Instance.openTipsTimes++; ui.btn_statement.visible = false; } ui.btn_saveingpot.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.SaveingPotUI_Open); }); ui.com_gift.SetClick(() => { uiCtrlDispatcher.Dispatch(UICtrlMsg.SaveingPotUI_Open); }); } // 缓存上一次箭头主题,用于判断是否需要重新随机彩色 private int _lastArrowTheme = -1; /// /// 主题变更统一回调(昼夜/箭头主题切换都会触发) /// private void SetModel(object a = null) { int darkThemeIdx = DataMgr.ArrowDarkTheme.Value; int currentArrowTheme = DataMgr.ArrowTheme.Value; ui.mode.selectedIndex = darkThemeIdx; // 1. 更新网格圆点昼夜配色 bool isDarkMode = darkThemeIdx == 0; // 0=黑夜 Debug.Log($"ArrowDarkTheme==={darkThemeIdx}"); UpdateGridDotColor(isDarkMode); // 2. 判断是否需要重新随机彩色:只有从非彩色切到彩色时才刷新 bool forceRandomColorful = currentArrowTheme == 2 && _lastArrowTheme != currentArrowTheme; // 3. 更新所有箭头颜色 UpdateAllArrowsColor(forceRandomColorful); // 4. 更新缓存,用于下次对比 _lastArrowTheme = currentArrowTheme; Debug.Log($"主题刷新完成:昼夜={darkThemeIdx},箭头主题={currentArrowTheme}"); } /// /// 批量设置网格圆点颜色(随昼夜模式切换) /// // 色值映射:白天#2c3a62,黑夜#d4e3f2 /// /// true=黑夜模式,false=白天模式 private string DarkColor = "#d4e3f2"; private string LightColor = "#2c3a62"; private void UpdateGridDotColor(bool isDay) { if (_gridDotList == null || _gridDotList.Count == 0) return; Debug.Log($"isDay=========={isDay}"); string colorHex = isDay ? DarkColor : LightColor; ColorUtility.TryParseHtmlString(colorHex, out Color targetColor); foreach (var dot in _gridDotList) { if (dot == null) continue; dot.point.color = targetColor; } } /// /// 根据当前主题,为单个箭头生成对应颜色 /// /// 箭头配置对象 /// 彩色模式下是否重新随机颜色 private Color GetArrowRuntimeColor(ArrowConfig arrow, bool isRandomNew = false) { int themeType = DataMgr.ArrowTheme.Value; // 0 = 纯色模式(黑色按钮):白天黑、黑夜白 if (themeType == 0) { bool isDark = DataMgr.ArrowDarkTheme.Value == 0; // 0=黑夜 return isDark ? Color.white : Color.black; } // 2 = 彩色随机模式 else if (themeType == 2) { if (isRandomNew || arrow.runtimeColor == default) { int randomIndex = Random.Range(0, _colorfulPalette.Length); arrow.runtimeColor = _colorfulPalette[randomIndex]; } return arrow.runtimeColor; } // 兜底默认黑色 return Color.black; } /// 批量更新场上所有箭头的颜色(线条+单箭头) /// /// 彩色模式下是否强制重新随机 private void UpdateAllArrowsColor(bool forceRandomColorful = false) { if (_levelConfig == null || _levelConfig.arrows == null) return; int themeType = DataMgr.ArrowTheme.Value; bool isColorfulMode = themeType == 2; foreach (var arrow in _levelConfig.arrows) { // 只有强制刷新时,彩色模式才重新随机颜色 bool isRandomNew = isColorfulMode && forceRandomColorful; Color targetColor = GetArrowRuntimeColor(arrow, isRandomNew); // 更新线条颜色 if (arrow.lineUnits != null) { foreach (var line in arrow.lineUnits) { if (line != null) { line.GetChild("line").asGraph.color = targetColor; } } } // 更新单箭头颜色 if (arrow.arrowObj != null) { GImage icon = arrow.arrowObj.GetChild("icon").asImage; if (icon != null) { icon.color = targetColor; } } } } //反馈特效工具 /// /// 触发震动反馈 /// /// true=强震动(阻挡时用),false=轻震动(普通点击) private void TriggerVibration(bool isStrong = false) { // 编辑器模式跳过震动 #if UNITY_EDITOR return; #endif // 通用震动(兼容iOS/安卓,系统默认时长) // Handheld.Vibrate(); Debug.Log($"震动反馈:{isStrong}"); // ===== 进阶:安卓自定义震动时长(可选开启)===== if (Application.platform == RuntimePlatform.Android) { long duration = isStrong ? 100L : 50L; AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject activity = unityPlayer.GetStatic("currentActivity"); AndroidJavaObject vibrator = activity.Call("getSystemService", "vibrator"); vibrator.Call("vibrate", duration); } } /// /// 触发阻挡时两侧红色闪烁效果 /// private void TriggerBlockFlash() { ui.t1.Play(); // 先终止正在播放的动画,避免叠加错乱 ui.com_ficker.visible = true; ui.com_ficker.t0.Play(() => { ui.com_ficker.visible = false; }); } #endregion private void InitGridBounds() { // 第0列、第0行的起点坐标 gridLeft = ui.point.x; gridTop = ui.point.y; // 正确计算:(点数 - 1) × 点间距 = 总跨度 gridRight = ui.point.x + (_levelConfig.gridCols - 1) * _levelConfig.pointSpacing; gridBottom = ui.point.y + (_levelConfig.gridRows - 1) * _levelConfig.pointSpacing; } /// 初始化网格占用二维数组 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=0 int currentLevelId = GameHelper.GetLevel(); // 替换为:按ID加载单关 bool loadSuccess = LevelManager.Instance.LoadLevel(currentLevelId); if (!loadSuccess) { // 加载失败兜底(比如回到第一关) return; } // 直接取当前关卡配置,后续逻辑完全不变 _levelConfig = LevelManager.Instance.CurrentLevel; // if (_allLevelData.levels.Count < currentLevelId) currentLevelId = _allLevelData.levels.Count; // _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"); #if UNITY_ANDROID && !UNITY_EDITOR // Android 平台需要使用 UnityWebRequest 读取 StreamingAssets using (UnityEngine.Networking.UnityWebRequest www = UnityEngine.Networking.UnityWebRequest.Get(path)) { www.SendWebRequest(); while (!www.isDone) { } if (www.result == UnityEngine.Networking.UnityWebRequest.Result.Success) { string json = www.downloadHandler.text; _allLevelData = JsonUtility.FromJson(json); } else { Debug.LogError($"Failed to load all_levels.json: {www.error}"); } } #else // Editor 和其他平台直接读取 string json = File.ReadAllText(path); _allLevelData = JsonUtility.FromJson(json); #endif } 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) { 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(); } /// 计算整个网格圆点区域的世界中心点 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); for (int i = 0; i < tilePositions.Count; i++) { Vector2 pos = tilePositions[i]; LineTile unit = LineTile.CreateInstance(); unit.SetPosition(pos.x, pos.y, 0); // 运行时主题取色 Color lineColor = GetArrowRuntimeColor(arrow); unit.GetChild("line").asGraph.color = lineColor; unit.visible = true; unit.touchable = true; unit.name = "Arrow_" + arrow.id; 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; // ========== 初始化距离偏移 ========== 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 SetUserPorp(object a) { if (a != null) { var type = (int)a; Debug.Log($"Set User Prop: {type}"); if (type == 0) { // 提示道具 ShowRandomArrowTip(); } else if (type == 1) { _isDeleteMode = !_isDeleteMode; GameHelper.ShowTips("delete_arrow", true); } } } /// 点击删除道具,开启/关闭删除模式 private void OnClickDeleteItem() { // _isDeleteMode = !_isDeleteMode; UICtrlDispatcher.Instance.Dispatch(UICtrlMsg.ArrowTipsUI_Open, 1); // // 可选:加提示,区分状态 // if (_isDeleteMode) // { // Debug.Log("已开启删除模式,点击箭头即可删除"); // // 可加UI提示:如图标高亮、文字提示 // } // else // { // Debug.Log("已关闭删除模式"); // } } private void OnClickHint() { UICtrlDispatcher.Instance.Dispatch(UICtrlMsg.ArrowTipsUI_Open, 0); } /// 随机选中有效箭头,显示静态手指提示(不跟随移动) 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 && !arrow.isMoving && !IsArrowBlocked(arrow)) .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 缩放 /// /// 判断FGUI全局坐标是否落在拖拽有效区域(view_container_parent)内 /// private bool IsInDragArea(Vector2 fguiGlobalPos) { if (ui.view_container_parent == null) return false; // 全局坐标转成父容器本地坐标(左上角为原点) Vector2 localPos = ui.view_container_parent.GlobalToLocal(fguiGlobalPos); // 判断是否在容器矩形范围内 return localPos.x >= 0 && localPos.x <= ui.view_container_parent.width && localPos.y >= 0 && localPos.y <= ui.view_container_parent.height; } /// /// 判断FGUI全局坐标是否落在圆点网格范围内 /// private bool IsInGridArea(Vector2 fguiGlobalPos) { if (_viewContainer == null || _levelConfig == null) return false; // 全局坐标转成 _viewContainer 本地坐标(网格坐标基于该容器) Vector2 localPos = _viewContainer.GlobalToLocal(fguiGlobalPos); // 和网格四边边界比对 return localPos.x >= gridLeft && localPos.x <= gridRight && localPos.y >= gridTop && localPos.y <= gridBottom; } /// 根据缩放值,计算滑动条 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; } TriggerVibration(true); // 点击的是当前提示箭头,直接隐藏手指 if (_currentTipArrow == clickArrow) { HideFingerTip(); } // ========= 删除模式:直接删除箭头,不走移动逻辑 ========= if (_isDeleteMode) { DeleteArrowCompletely(clickArrow); return; } clickArrow.isMoving = true; bool hasBlock = CheckBlockByGrid(clickArrow); Debug.Log("[arrow] 点击箭头 id:" + clickArrow.id +$" hasBlock=阻挡=={hasBlock}"); if (hasBlock) { MoveThenReset(clickArrow); } else { ReleaseArrowOccupied(clickArrow); MoveToDisappear(clickArrow); } } /// /// 纯检测:箭头前方是否有阻挡(无副作用,不扣血、不改状态) /// /// true=前方有阻挡,false=可直接飞出消除 private bool IsArrowBlocked(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"; } // 沿射出方向遍历前方网格 switch (moveDir) { case "up": for (int r = curRow - 1; r >= 0; r--) { if (_gridOccupied[r, curCol]) return true; } break; case "down": for (int r = curRow + 1; r < _levelConfig.gridRows; r++) { if (_gridOccupied[r, curCol]) return true; } break; case "left": for (int c = curCol - 1; c >= 0; c--) { if (_gridOccupied[curRow, c]) return true; } break; case "right": for (int c = curCol + 1; c < _levelConfig.gridCols; c++) { if (_gridOccupied[curRow, c]) return true; } break; } return false; } // 基于网格行列的阻挡检测(带扣血逻辑,点击箭头时调用) private bool CheckBlockByGrid(ArrowConfig curArrow) { bool isCollide = IsArrowBlocked(curArrow); // ========== 碰撞判重 + 扣爱心 ========== 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; } /// /// 计算箭头沿射出方向,到前方第一个阻挡点的前移距离(像素) /// /// 目标箭头 /// 碰撞预留余量,离阻挡点还差多少像素就停下回弹 /// 实际前移的总像素距离 private float GetDistanceToFirstBlock(ArrowConfig arrow, float hitMargin = 20f) { if (_gridOccupied == null || _levelConfig == null) return 20f; // 异常兜底,保持原默认值 // 计算箭头头部所在的网格坐标(路径终点) int curRow = arrow.startPoint / _levelConfig.gridCols; int curCol = arrow.startPoint % _levelConfig.gridCols; // 沿路径走到终点,定位箭头头部网格位置 foreach (string dir in arrow.path) { switch (dir) { case "up": curRow--; break; case "down": curRow++; break; case "left": curCol--; break; case "right": curCol++; break; } } string moveDir = GetFinalMoveDir(arrow); int emptyGridCount = 0; // 沿射出方向逐格检测第一个阻挡 while (true) { // 前进一步 switch (moveDir) { case "up": curRow--; break; case "down": curRow++; break; case "left": curCol--; break; case "right": curCol++; break; } // 出界无阻挡,兜底返回默认距离 if (curRow < 0 || curRow >= _levelConfig.gridRows || curCol < 0 || curCol >= _levelConfig.gridCols) { return 20f; } // 找到阻挡点 if (_gridOccupied[curRow, curCol]) { // 总距离 = 空格数*点间距 + (单格距离 - 预留余量) float total = (emptyGridCount + 1) * _levelConfig.pointSpacing - hitMargin; // 最小前移保护,避免阻挡太近时几乎没动画 return Mathf.Max(total, 6f); } emptyGridCount++; } } /// 彻底删除指定箭头:销毁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); } // 运行时主题取色 Color arrowColor = GetArrowRuntimeColor(arrow); GImage icon = arrowIns.GetChild("icon").asImage; if (icon != null) icon.color = arrowColor; arrowIns.visible = true; arrowIns.name = "Arrow_" + arrow.id; arrowIns.onClick.Add(() => OnPathClick(arrow)); _viewContainer.AddChild(arrowIns); arrow.arrowObj = arrowIns; arrow.isMoving = false; } // 移动后复位动画(回弹,不释放占用) private void MoveThenReset(ArrowConfig arrow) { RunCoroutine(MoveAndResetCoroutine(arrow)); } private readonly float _bounceSpeed = 1200f; private IEnumerator MoveAndResetCoroutine(ArrowConfig arrow) { arrow.movedDistance = 0f; arrow.finalMoveDir = GetFinalMoveDir(arrow); // 动态计算:前移到快碰到阻挡点的距离 float forwardTotal = GetDistanceToFirstBlock(arrow, 3f); float duration = forwardTotal / _bounceSpeed; float timer = 0f; // ========== 第一段:向前移动到快碰到阻挡 ========== while (timer < duration) { if (arrow == null || arrow.arrowObj == null) yield break; timer += Time.deltaTime; arrow.movedDistance = Mathf.Min(_bounceSpeed * timer, forwardTotal); UpdateAllElementsPosition(arrow); yield return null; } // 核心:阻挡强震动 + 红色氛围闪烁 TriggerBlockFlash(); // 强制对齐前移终点,消除浮点误差 arrow.movedDistance = forwardTotal; UpdateAllElementsPosition(arrow); // ========== 直接瞬间复位(删除原渐变回弹逻辑)========== // 可选:加 1 帧停顿,模拟碰撞卡顿感,觉得突兀就打开注释 // yield return null; // 距离归零 + 原始坐标强制校准,双保险保证精准回到初始位置 arrow.movedDistance = 0f; UpdateAllElementsPosition(arrow); if (arrow.arrowObj != null) { arrow.arrowObj.SetPosition(arrow.originalArrowPos.x, arrow.originalArrowPos.y, 0); } for (int i = 0; i < arrow.lineUnits.Count; i++) { if (arrow.lineUnits[i] != null && i < arrow.originalLinePos.Count) { arrow.lineUnits[i].SetPosition( arrow.originalLinePos[i].x, arrow.originalLinePos[i].y, 0); } } 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)); } private void SetSpeed(object a = null) { if (a != null) { _moveSpeed = (float)a; } } // 新增:统一移动速度(像素/秒),数值越大移动越快,可自由修改 [SerializeField] private float _moveSpeed = 1500f; 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() { if (!GameHelper.IsGiftSwitch()) return; 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 (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.25f, () => { 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); var littleReward = GameHelper.GetExchangeRateVo().Multi * cash_array[0]; Debug.Log($"小额奖励:{cash_array[0]} {littleReward}"); var rewardSingleData = new RewardSingleData(102, (decimal)littleReward, 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 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 结算 /// /// 弹出结算 /// 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); // DataMgr.ArrowChProgress.Value += 10; //通关成功 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.2f, () => { 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.2f, () => { 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(); //获取下一关的配置数据 LoadLevelConfig(); // 重新走初始化流程 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 List path; [NonSerialized] public int endPoint; [NonSerialized] public string color; [NonSerialized]public string defaultDir; [NonSerialized]public List pathPointList; [NonSerialized]public List lineUnits; [NonSerialized]public GComponent arrowObj; [NonSerialized]public bool isMoving; [NonSerialized]public List originalLinePos; [NonSerialized]public Vector2 originalArrowPos; [NonSerialized]public List totalTrack; [NonSerialized]public List lineTrackIndex; [NonSerialized]public int arrowTrackIndex; [NonSerialized]public string finalMoveDir; // ========== 距离驱动移动所需字段 ========== [NonSerialized]public float movedDistance; // 当前整体累计移动距离 [NonSerialized]public float trackTotalLength; // 整条轨迹的总像素长度 [NonSerialized]public List lineInitOffsets; // 每个线条单元的初始距离偏移 [NonSerialized]public float arrowInitOffset; // 箭头的初始距离偏移 // ========== 运行时颜色缓存 ========== [NonSerialized]public Color runtimeColor; } [System.Serializable] public class AllLevelRoot { public List levels; } #endregion public class SuccessData { public bool IsWin; public float cash_number; public bool IsLevelSuccess; public bool IsH5Reward; public int rate; public int[] boost_array; } }