Files
RedHotRoast-ios/Assets/Scripts/ModuleUI/ArrowGame/ArrowGameUI.cs
T
2026-07-14 17:56:40 +08:00

2251 lines
80 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;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DG.Tweening;
using FairyGUI;
using FGUI.Arrow_game;
using FGUI.ZM_Common_01;
using UnityEngine;
using Random = UnityEngine.Random;
namespace RedHotRoast
{
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<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(-20, -20);
// 所有网格圆点实例列表,用于批量改色/清理
private List<ArrorPoint> _gridDotList = new List<ArrorPoint>();
#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.GetCommonConf().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;
// coin *= (decimal)GameHelper.GetExchangeRateVo().Multi;
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");
Set102();
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 = MakeupModel.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.ArrowSettingUI_Open, _moveSpeed);
});
if (!GameHelper.IsGiftSwitch() && ConfigSystem.GetCommonConf().WVswitch == 0)
{
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.GetCommonConf().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;
/// <summary>
/// 主题变更统一回调(昼夜/箭头主题切换都会触发)
/// </summary>
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}");
}
/// <summary>
/// 批量设置网格圆点颜色(随昼夜模式切换)
/// // 色值映射:白天#2c3a62,黑夜#d4e3f2
/// </summary>
/// <param name="isDay">true=黑夜模式,false=白天模式</param>
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;
}
}
/// <summary>
/// 根据当前主题,为单个箭头生成对应颜色
/// </summary>
/// <param name="arrow">箭头配置对象</param>
/// <param name="isRandomNew">彩色模式下是否重新随机颜色</param>
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;
}
/// 批量更新场上所有箭头的颜色(线条+单箭头)
/// </summary>
/// <param name="forceRandomColorful">彩色模式下是否强制重新随机</param>
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;
}
}
}
}
//反馈特效工具
/// <summary>
/// 触发震动反馈
/// </summary>
/// <param name="isStrong">true=强震动(阻挡时用),false=轻震动(普通点击)</param>
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<AndroidJavaObject>("currentActivity");
AndroidJavaObject vibrator = activity.Call<AndroidJavaObject>("getSystemService", "vibrator");
vibrator.Call("vibrate", duration);
}
}
/// <summary>
/// 触发阻挡时两侧红色闪烁效果
/// </summary>
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;
}
/// <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=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;
// }
// }
}
/// <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");
#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<AllLevelRoot>(json);
}
else
{
Debug.LogError($"Failed to load all_levels.json: {www.error}");
}
}
#else
// Editor 和其他平台直接读取
string json = File.ReadAllText(path);
_allLevelData = JsonUtility.FromJson<AllLevelRoot>(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 【核心】网格坐标工具
/// <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)
{
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);
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<float>();
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);
}
}
}
/// <summary>点击删除道具,开启/关闭删除模式</summary>
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);
}
/// <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 &&
!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;
}
/// <summary>隐藏手指提示并清空标记</summary>
private void HideFingerTip()
{
if (_fingerTipObj != null)
{
_fingerTipObj.visible = false;
}
_currentTipArrow = null;
}
#endregion
#region 缩放
/// <summary>
/// 判断FGUI全局坐标是否落在拖拽有效区域(view_container_parent)内
/// </summary>
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;
}
/// <summary>
/// 判断FGUI全局坐标是否落在圆点网格范围内
/// </summary>
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;
}
/// <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;
}
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);
}
}
/// <summary>
/// 纯检测:箭头前方是否有阻挡(无副作用,不扣血、不改状态)
/// </summary>
/// <returns>true=前方有阻挡,false=可直接飞出消除</returns>
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;
}
/// <summary>
/// 计算箭头沿射出方向,到前方第一个阻挡点的前移距离(像素)
/// </summary>
/// <param name="arrow">目标箭头</param>
/// <param name="hitMargin">碰撞预留余量,离阻挡点还差多少像素就停下回弹</param>
/// <returns>实际前移的总像素距离</returns>
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++;
}
}
/// <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);
}
// 运行时主题取色
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;
}
/// <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));
}
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 移动工具方法
/// <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()
{
if (!GameHelper.IsGiftSwitch()) return;
bool isGet = false;
int money_rate = ConfigSystem.GetCommonConf().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.ch_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.ArrowGameResultUI_Open, temp);
});
}
if (!isGet && Random.Range(0, 100) < ConfigSystem.GetCommonConf().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 = Math.Round(GameHelper.GetExchangeRateVo().Multi * cash_array[0],2);
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 资源清理
/// <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.ArrowGameResultUI_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.ch_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.ArrowGameResultUI_Open, temp);
});
}
else
{
//通关失败
float[] cash_array = GameHelper.GetRewardValue(2);
var temp = new SuccessData();
temp.IsWin = false;
temp.ch_number = cash_array[0];
temp.IsLevelSuccess = true;
temp.IsH5Reward = false;
temp.boost_array = GameHelper.GetRewardBoost(2);
DOVirtual.DelayedCall(0.2f, () =>
{
UICtrlDispatcher.Instance.Dispatch(UICtrlMsg.ArrowGameResultUI_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();
//获取下一关的配置数据
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<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 List<string> path;
[NonSerialized] public int endPoint;
[NonSerialized] public string color;
[NonSerialized]public string defaultDir;
[NonSerialized]public List<Vector2> pathPointList;
[NonSerialized]public List<GComponent> lineUnits;
[NonSerialized]public GComponent arrowObj;
[NonSerialized]public bool isMoving;
[NonSerialized]public List<Vector2> originalLinePos;
[NonSerialized]public Vector2 originalArrowPos;
[NonSerialized]public List<Vector2> totalTrack;
[NonSerialized]public List<int> lineTrackIndex;
[NonSerialized]public int arrowTrackIndex;
[NonSerialized]public string finalMoveDir;
// ========== 距离驱动移动所需字段 ==========
[NonSerialized]public float movedDistance; // 当前整体累计移动距离
[NonSerialized]public float trackTotalLength; // 整条轨迹的总像素长度
[NonSerialized]public List<float> lineInitOffsets; // 每个线条单元的初始距离偏移
[NonSerialized]public float arrowInitOffset; // 箭头的初始距离偏移
// ========== 运行时颜色缓存 ==========
[NonSerialized]public Color runtimeColor;
}
[System.Serializable]
public class AllLevelRoot
{
public List<LevelConfig> levels;
}
#endregion
}