Files
ArrowBeatTap-Gp/Assets/Scripts/ModuleUI/ArrowGame/ArrowGameUI.cs
T
2026-06-14 16:25:04 +08:00

1507 lines
52 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FairyGUI;
using FGUI.Arrow_game;
using UnityEngine;
namespace ChillConnect
{
public class ArrowGameUI : BaseUI
{
private ArrowGameUICtrl ctrl;
private com_arrow_game ui;
// FGUI界面根容器
private GComponent _viewContainer;
#region FGUI 模板引用
private ArrorPoint _dotTemplate;
private ArrowEnd _arrowTemplate;
private LineTile _lineUnitTemplate;
#endregion
// 关卡总配置数据
private LevelConfig _levelConfig;
// 全局固定参数
private const float _pointSpacing = 60f; // 点位间距 60px
private const float _moveSpeed = 120f; // 移动速度
private List<ArrowConfig> allArrowList = new List<ArrowConfig>();
// 网格基础参数
private readonly int gridColNum = 10;
private readonly float pointSpace = 60f;
private readonly float outOffset = 200f;
// 网格边界
private Vector2 _gridWorldCenter; // 网格世界中心点
private float gridLeft;
private float gridRight;
private readonly int gridRowNum = 10;
private float gridTop;
private float gridBottom;
// 网格行列、单个格子尺寸(你原有网格参数)
private int _gridRow;
private int _gridCol;
private float _cellSize;
// 网格占用标记:true=该点位被占用,false=空闲
private bool[,] _gridOccupied;
// 记录每个箭头占用的所有网格点位(行,列)
private Dictionary<ArrowConfig, List<(int row, int col)>> _arrowOccupiedCells = new Dictionary<ArrowConfig, List<(int row, int col)>>();
// 爱心数量
private int _heartCount = 3;
// 记录已经发生过碰撞的箭头对象(去重:同个箭头多次碰撞只扣一次血)
private HashSet<ArrowConfig> _collidedArrows = new HashSet<ArrowConfig>();
// 统计场上还存在的箭头总数(用于判断全部移除=通关)
private int _remainArrowCount = 0;
// 记录当前UI所有由 CrazyAsyKit 启动的协程,用于统一停止
private List<Coroutine> _activeCoroutines = new List<Coroutine>();
// 是否开启删除道具模式
private bool _isDeleteMode = false;
// 手指提示图标
private com_finger _fingerTipObj;
// 当前正在提示的箭头
private ArrowConfig _currentTipArrow;
// 手指图标偏移(微调位置)
private readonly Vector2 _fingerOffset = new Vector2(-60, -60);
#region 缩放、拖拽、滑动条 全局变量
// 缩放阈值(滑动条左右边界对应这两个值)
private readonly float _minScale = 0.6f;
private readonly float _maxScale = 1.5f;
// FGUI 滑动条组件
private GSlider _zoomSlider;
// 双指缩放手势
private float _lastTwoFingerDistance;
private bool _isTwoFingerTouch;
// 单指拖拽平移
private Vector2 _lastTouchPos;
private bool _isDraging;
// 拖拽边界留边(防止完全移出屏幕)
private readonly float _dragBorder = 100f;
#endregion
public ArrowGameUI(ArrowGameUICtrl ctrl) : base(ctrl)
{
uiName = UIConst.ArrowGameUI;
this.ctrl = ctrl;
}
protected override void SetUIInfo(UIInfo uiInfo)
{
uiInfo.packageName = "Arrow_game";
uiInfo.assetName = "com_arrow_game";
uiInfo.layerType = UILayerType.Popup;
uiInfo.isNeedOpenAnim = false;
uiInfo.isNeedCloseAnim = false;
uiInfo.isNeedUIMask = true;
uiInfo.isTickUpdate = true;
}
#region 生命周期
protected override void OnInit()
{
}
protected override void OnClose()
{
// ========= 第一步:统一停止所有协程(核心修复) =========
foreach (var co in _activeCoroutines)
{
if (co != null)
{
CrazyAsyKit.StopCoroutine(co);
}
}
// 清空列表
_activeCoroutines.Clear();
// 保险========= 强制停止所有移动,终止对应协程 =========
if (_levelConfig != null)
{
foreach (var arrow in _levelConfig.arrows)
{
arrow.isMoving = false;
}
}
// 销毁所有动态FGUI对象
ClearAllDynamicObj();
HideFingerTip();
// 释放模板资源
if (_dotTemplate != null) _dotTemplate.Dispose();
if (_arrowTemplate != null) _arrowTemplate.Dispose();
if (_lineUnitTemplate != null) _lineUnitTemplate.Dispose();
_dotTemplate = null;
_arrowTemplate = null;
_lineUnitTemplate = null;
_levelConfig = null;
// 清空网格占用数据
_gridOccupied = null;
_arrowOccupiedCells.Clear();
}
protected override void OnBind()
{
ui = baseUI as com_arrow_game;
}
public override void OnUpdate()
{
// if (_levelConfig == null) return;
// foreach (var arrow in _levelConfig.arrows)
// {
// UpdateArrowMove(arrow);
// }
//
// ========= 编辑器模拟触摸(仅编辑模式生效) =========
#if UNITY_EDITOR
//1. 鼠标左键 = 单指拖拽
// Debug.Log($"Input.GetMouseButtonDown======={Input.GetMouseButtonDown(0)}");
if (Input.GetMouseButtonDown(0))
{
_lastTouchPos = Input.mousePosition;
_isDraging = true;
}
if (Input.GetMouseButton(0) && _isDraging)
{
Vector2 curMousePos = Input.mousePosition;
Vector2 offset = curMousePos - _lastTouchPos;
MoveContainer(offset);
_lastTouchPos = curMousePos;
}
if (Input.GetMouseButtonUp(0))
{
_isDraging = false;
}
// 2. 鼠标滚轮 = 双指缩放
// float scrollDelta = Input.GetMouseWheelDelta();
// if (Mathf.Abs(scrollDelta) > 0)
// {
// float curScale = _viewContainer.scaleX;
// curScale += scrollDelta * 0.1f;
// curScale = Mathf.Clamp(curScale, _minScale, _maxScale);
//
// _viewContainer.scaleX = curScale;
// _viewContainer.scaleY = curScale;
// _zoomSlider.value = GetSliderValueByScale(curScale);
// }
#endif
// ========= 双指 = 缩放(优先级最高) =========
if (Input.touchCount == 2)
{
_isDraging = false;
Touch t0 = Input.GetTouch(0);
Touch t1 = Input.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 (Input.touchCount == 1)
{
Touch touch = Input.GetTouch(0);
Vector2 curTouchPos = touch.position;
if (touch.phase == TouchPhase.Began)
{
_lastTouchPos = curTouchPos;
_isDraging = true;
_isTwoFingerTouch = false;
}
else if (touch.phase == TouchPhase.Moved && _isDraging)
{
Vector2 offset = curTouchPos - _lastTouchPos;
MoveContainer(offset);
_lastTouchPos = curTouchPos;
}
else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
{
_isDraging = false;
}
}
// 无触摸,重置状态
else
{
_isTwoFingerTouch = false;
_isDraging = false;
}
}
protected override void OnOpenBefore(object args)
{
_viewContainer = ui.view_container;
// 初始化FGUI模板
_dotTemplate = (ArrorPoint)UIPackage.CreateObject("Arrow_game", "ArrorPoint");
_arrowTemplate = (ArrowEnd)UIPackage.CreateObject("Arrow_game", "ArrowEnd");
_lineUnitTemplate = (LineTile)UIPackage.CreateObject("Arrow_game", "LineTile");
LoadLevelConfig();
InitGridBounds();
// 初始化网格占用数组
InitGridOccupied();
// ========== 新增:对局状态重置 ==========
_heartCount = 3;
_collidedArrows.Clear();
if(_levelConfig != null)
_remainArrowCount = _levelConfig.arrows.Count;
InitView();
}
protected override void OnOpen(object args)
{
}
protected override void OnHide()
{
}
protected override void OnDisplay(object args)
{
}
#endregion
#region 页面初始化 & 关闭按钮
private void InitView()
{
// 关闭按钮
ui.btn_close.SetClick(() => { CtrlCloseUI(); });
ui.btn_clear.SetClick(OnClickDeleteItem);
ui.btn_hint.SetClick(OnClickHint);
_fingerTipObj = com_finger.CreateInstance();
_viewContainer.AddChild(_fingerTipObj);
_fingerTipObj.visible = false;
_currentTipArrow = null;
_zoomSlider = ui.zoomSlide;
// 2. 绑定滑动条拖动回调
_zoomSlider.onChanged.Add(OnZoomSliderChange);
// 3. 容器初始缩放、位置
_viewContainer.scaleX = 1f;
_viewContainer.scaleY = 1f;
// 滑动条初始居中(对应 1.0 缩放)
float initValue = GetSliderValueByScale(1f);
_zoomSlider.value = initValue;
// 容器初始居中
float startX = (Screen.width - _viewContainer.width) * 0.5f;
float startY = (Screen.height - _viewContainer.height) * 0.5f;
_viewContainer.SetPosition(startX, startY,0);
// 重置手势状态
_isTwoFingerTouch = false;
_isDraging = false;
if (_levelConfig == null) return;
// 1. 绘制网格圆点
DrawGridDots(_levelConfig);
allArrowList = _levelConfig.arrows;
// 2. 初始化所有箭头 + 路径线条 + 标记网格占用
foreach (var arrow in _levelConfig.arrows)
{
BuildArrowPath(arrow, _levelConfig);
CreateArrowAndLines(arrow, _levelConfig);
MarkArrowOccupiedCells(arrow, _levelConfig);
}
}
#endregion
private void InitGridBounds()
{
gridLeft = ui.point.x;
gridRight = ui.point.x + gridColNum * pointSpace;
gridTop = ui.point.y;
gridBottom = ui.point.y + gridRowNum * pointSpace;
}
/// <summary>初始化网格占用二维数组</summary>
private void InitGridOccupied()
{
int rows = _levelConfig.gridRows;
int cols = _levelConfig.gridCols;
_gridOccupied = new bool[rows, cols];
// 初始全部置为空闲
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
_gridOccupied[r, c] = false;
_arrowOccupiedCells.Clear();
}
/// <summary>标记单个箭头占用的所有网格点</summary>
private void MarkArrowOccupiedCells(ArrowConfig arrow, LevelConfig cfg)
{
List<(int row, int col)> cellList = new List<(int row, int col)>();
int totalCol = cfg.gridCols;
// 遍历箭头整条路径的所有网格点位
foreach (var pos in arrow.pathPointList)
{
int pointId = GetPointIdByPos(pos, cfg);
PointIdToRowCol(pointId, totalCol, out int r, out int c);
// 边界保护
if (r >= 0 && r < cfg.gridRows && c >= 0 && c < cfg.gridCols)
{
_gridOccupied[r, c] = true;
cellList.Add((r, c));
}
}
_arrowOccupiedCells[arrow] = cellList;
}
/// <summary>释放单个箭头占用的网格(箭头消失时调用)</summary>
private void ReleaseArrowOccupied(ArrowConfig arrow)
{
if (!_arrowOccupiedCells.TryGetValue(arrow, out var cellList))
return;
foreach (var (r, c) in cellList)
{
if (r >= 0 && r < _levelConfig.gridRows && c >= 0 && c < _levelConfig.gridCols)
{
_gridOccupied[r, c] = false;
}
}
// 移除字典记录,避免冗余
_arrowOccupiedCells.Remove(arrow);
}
#region 配置加载
private void LoadLevelConfig()
{
_levelConfig = JsonHelper.LoadLevel("level_1001.json");
// 给 path 为空的箭头,设置默认方向
foreach (var arrow in _levelConfig.arrows)
{
if (arrow.path == null || arrow.path.Count == 0)
{
arrow.defaultDir = "up";
}
else
{
arrow.defaultDir = null;
}
}
}
#endregion
#region 【核心】网格坐标工具
/// <summary>点位ID 转 行列索引</summary>
private void PointIdToRowCol(int pointId, int totalCol, out int row, out int col)
{
row = pointId / totalCol;
col = pointId % totalCol;
}
/// <summary>行列 → 实际像素坐标</summary>
private Vector2 RowColToPos(int row, int col)
{
float originX = ui.point.x;
float originY = ui.point.y;
float x = originX + col * _levelConfig.pointSpacing;
float y = originY + row * _levelConfig.pointSpacing;
return new Vector2(x, y);
}
/// <summary>点位ID → 最终像素坐标</summary>
private Vector2 PointIdToPos(int pointId, int totalCol)
{
PointIdToRowCol(pointId, totalCol, out int r, out int c);
return RowColToPos(r, c);
}
/// <summary>像素坐标反向计算点位ID</summary>
private int GetPointIdByPos(Vector2 pos, LevelConfig cfg)
{
float originX = ui.point.x;
float originY = ui.point.y;
int col = Mathf.RoundToInt((pos.x - originX) / cfg.pointSpacing);
int row = Mathf.RoundToInt((pos.y - originY) / cfg.pointSpacing);
return row * cfg.gridCols + col;
}
/// <summary> 根据FGUI旋转角度获取方向 </summary>
private string GetDirByRotation(float rot)
{
rot = Mathf.Round(rot);
if (rot == 0) return "right";
if (rot == 90) return "down";
if (rot == 180) return "left";
if (rot == 270) return "up";
return "right";
}
/// <summary>方向转旋转角度(箭头默认朝上)</summary>
private float DirToRotation(string dir)
{
return dir switch
{
"up" => 0f,
"right" => 90f,
"down" => 180f,
"left" => 270f,
_ => 0f
};
}
#endregion
#region 绘制网格圆点
private void DrawGridDots(LevelConfig cfg)
{
if (_dotTemplate == null || _viewContainer == null) return;
_dotTemplate.visible = false;
var rows = cfg.gridRows;
var cols = cfg.gridCols;
for (var r = 0; r < rows; r++)
{
for (var c = 0; c < cols; c++)
{
var dot = ArrorPoint.CreateInstance();
var pos = RowColToPos(r, c);
dot.SetPosition(pos.x, pos.y, 0);
_viewContainer.AddChild(dot);
}
}
// ========= 新增:网格绘制完成后,设置居中缩放轴心 =========
CalculateGridCenter(cfg);
SetViewPivotToGridCenter();
}
/// <summary>计算网格整体世界中心(原有网格坐标不变)</summary>
/// <summary>计算整个网格圆点区域的世界中心点</summary>
private void CalculateGridCenter(LevelConfig cfg)
{
int rows = cfg.gridRows;
int cols = cfg.gridCols;
// 取四个角点位,算出整体中心
Vector2 topLeft = RowColToPos(0, 0);
Vector2 bottomRight = RowColToPos(rows - 1, cols - 1);
// 几何中心
_gridWorldCenter.x = (topLeft.x + bottomRight.x) * 0.5f;
_gridWorldCenter.y = (topLeft.y + bottomRight.y) * 0.5f;
}
/// <summary>设置_viewContainer缩放轴心 = 网格中心,实现居中缩放</summary>
private void SetViewPivotToGridCenter()
{
if (_viewContainer == null) return;
// FGUI pivot 取值范围 0~1,是容器内部相对比例
float pivotX = _gridWorldCenter.x / _viewContainer.width;
float pivotY = _gridWorldCenter.y / _viewContainer.height;
// 限制在合法区间
pivotX = Mathf.Clamp01(pivotX);
pivotY = Mathf.Clamp01(pivotY);
// 设置轴心,此后所有缩放都围绕网格中心
_viewContainer.pivot = new Vector2(pivotX, pivotY);
}
#endregion
#region 构建箭头路径点位
private void BuildArrowPath(ArrowConfig arrow, LevelConfig cfg)
{
arrow.pathPointList = new List<Vector2>();
int totalCol = cfg.gridCols;
int curPointId = arrow.startPoint;
Vector2 curPos = PointIdToPos(curPointId, totalCol);
arrow.pathPointList.Add(curPos);
foreach (string dir in arrow.path)
{
PointIdToRowCol(curPointId, totalCol, out int r, out int c);
switch (dir)
{
case "up": r--; break;
case "down": r++; break;
case "left": c--; break;
case "right": c++; break;
}
curPointId = r * totalCol + c;
curPos = PointIdToPos(curPointId, totalCol);
arrow.pathPointList.Add(curPos);
}
}
#endregion
#region 创建箭头 + 线条方块
private void CreateArrowAndLines(ArrowConfig arrow, LevelConfig cfg)
{
arrow.lineUnits = new List<GComponent>();
arrow.originalLinePos = new List<Vector2>();
arrow.lineTrackIndex = new List<int>();
arrow.totalTrack = new List<Vector2>();
List<Vector2> pathPoints = arrow.pathPointList;
// 生成整条连续行走轨迹
if (pathPoints.Count >= 2)
{
for (int i = 0; i < pathPoints.Count - 1; i++)
{
Vector2 segStart = pathPoints[i];
string dir = arrow.path[i];
float offX = 0, offY = 0;
switch (dir)
{
case "right": offX = 10; break;
case "left": offX = -10; break;
case "down": offY = 10; break;
case "up": offY = -10; break;
}
Vector2 cur = segStart;
for (int k = 0; k < 6; k++)
{
arrow.totalTrack.Add(cur);
cur.x += offX;
cur.y += offY;
}
}
if (arrow.path.Count > 0)
arrow.finalMoveDir = arrow.path[arrow.path.Count - 1];
}
// 路径只有单个点(纯箭头)
if (pathPoints.Count < 2)
{
CreateArrowOnly(arrow, pathPoints[0]);
// ========= 修复1:单点箭头也初始化轨迹索引 =========
arrow.arrowTrackIndex = 0;
arrow.finalMoveDir = GetDirByRotation(arrow.arrowObj.rotation);
// ========= 修复2:强制记录原始坐标(关键!)=========
arrow.originalArrowPos = new Vector2(arrow.arrowObj.x, arrow.arrowObj.y);
// 优化:给单点箭头填充一个轨迹点,和有线箭头逻辑完全一致
arrow.totalTrack.Add(pathPoints[0]);
return;
}
// 生成线条图块
List<Vector2> tilePositions = new List<Vector2>();
for (int i = 0; i < pathPoints.Count - 1; i++)
{
Vector2 segStart = pathPoints[i];
string dir = arrow.path[i];
float offX = 0, offY = 0;
switch (dir)
{
case "right": offX = 10; break;
case "left": offX = -10; break;
case "down": offY = 10; break;
case "up": offY = -10; break;
}
Vector2 cur = segStart;
for (int k = 0; k < 6; k++)
{
tilePositions.Add(cur);
cur.x += offX;
cur.y += offY;
}
}
if (tilePositions.Count > 0)
tilePositions.RemoveAt(tilePositions.Count - 1);
ColorUtility.TryParseHtmlString(arrow.color, out Color lineColor);
for (int i = 0; i < tilePositions.Count; i++)
{
Vector2 pos = tilePositions[i];
LineTile unit = LineTile.CreateInstance();
unit.SetPosition(pos.x, pos.y, 0);
unit.GetChild("line").asGraph.color = lineColor;
unit.visible = true;
unit.touchable = true;
unit.onClick.Add(() => OnPathClick(arrow));
_viewContainer.AddChild(unit);
arrow.lineUnits.Add(unit);
arrow.originalLinePos.Add(pos);
arrow.lineTrackIndex.Add(i);
}
// 创建箭头
CreateArrowOnly(arrow, pathPoints[pathPoints.Count - 1]);
arrow.originalArrowPos = new Vector2(arrow.arrowObj.x, arrow.arrowObj.y);
arrow.arrowTrackIndex = arrow.totalTrack.Count - 1;
}
// 线条/箭头 统一点击回调
private void OnPathClick(ArrowConfig clickArrow)
{
if (clickArrow.isMoving)
{
Debug.Log($"[arrow] {clickArrow.id}正在移动,请勿点击其他箭头");
return;
}
// 点击的是当前提示箭头,直接隐藏手指
if (_currentTipArrow == clickArrow)
{
HideFingerTip();
}
// ========= 删除模式:直接删除箭头,不走移动逻辑 =========
if (_isDeleteMode)
{
DeleteArrowCompletely(clickArrow);
return;
}
clickArrow.isMoving = true;
bool hasBlock = CheckBlockByGrid(clickArrow);
Debug.Log("[arrow] 点击箭头:" + clickArrow.id +$"hasBlock==={hasBlock}");
if (hasBlock)
{
MoveThenReset(clickArrow);
}
else
{
MoveToDisappear(clickArrow);
}
}
#region 道具
/// <summary>点击删除道具,开启/关闭删除模式</summary>
private void OnClickDeleteItem()
{
_isDeleteMode = !_isDeleteMode;
// 可选:加提示,区分状态
if (_isDeleteMode)
{
Debug.Log("已开启删除模式,点击箭头即可删除");
// 可加UI提示:如图标高亮、文字提示
}
else
{
Debug.Log("已关闭删除模式");
}
}
private void OnClickHint()
{
ShowRandomArrowTip();
}
/// <summary>随机选中有效箭头,显示静态手指提示(不跟随移动)</summary>
private void ShowRandomArrowTip()
{
// 先隐藏上一次提示
HideFingerTip();
if (_levelConfig == null || _levelConfig.arrows == null || _levelConfig.arrows.Count == 0)
{
Debug.Log("暂无可提示的箭头");
return;
}
// 筛选有效箭头
List<ArrowConfig> validArrows = _levelConfig.arrows
.Where(arrow => arrow != null && arrow.arrowObj != null)
.ToList();
if (validArrows.Count == 0)
{
Debug.Log("没有有效箭头可提示");
return;
}
// 随机选一个
int randomIndex = Random.Range(0, validArrows.Count);
_currentTipArrow = validArrows[randomIndex];
// 设置手指位置(仅初始化一次,不再刷新)
float x = _currentTipArrow.arrowObj.x + _fingerOffset.x;
float y = _currentTipArrow.arrowObj.y + _fingerOffset.y;
_fingerTipObj.SetPosition(x, y, 0);
_fingerTipObj.visible = true;
}
/// <summary>隐藏手指提示并清空标记</summary>
private void HideFingerTip()
{
if (_fingerTipObj != null)
{
_fingerTipObj.visible = false;
}
_currentTipArrow = null;
}
#endregion
#region 缩放
/// <summary>根据缩放值,计算滑动条 value(0~1)</summary>
private float GetSliderValueByScale(float scale)
{
float validScale = Mathf.Clamp(scale, _minScale, _maxScale);
float ratio = (validScale - _minScale) / (_maxScale - _minScale);
float sliderVal = ratio * 100f;
return Mathf.Clamp(sliderVal, 0f, 100f);
}
/// <summary>根据滑动条 value(0~1),计算实际缩放值</summary>
private float GetScaleBySliderValue(float sliderValue)
{
// 先把滑块值钳位在 0~100 内
float val = Mathf.Clamp(sliderValue, 0f, 100f);
// 线性映射:0→0.6100→1.5
float ratio = val / 100f;
float scale = _minScale + ratio * (_maxScale - _minScale);
return Mathf.Clamp(scale, _minScale, _maxScale);
}
/// <summary>滑动条数值变化回调</summary>
private void OnZoomSliderChange()
{
float scale = GetScaleBySliderValue((float)_zoomSlider.value);
_viewContainer.scaleX = scale;
_viewContainer.scaleY = scale;
}
/// <summary>移动容器 + 边界限制,防止完全拖出屏幕</summary>
private void MoveContainer(Vector2 offset)
{
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
// 基于网格行列的阻挡检测
private bool CheckBlockByGrid(ArrowConfig curArrow)
{
if (_gridOccupied == null || _levelConfig == null) return false;
// 当前箭头网格行列
Vector2 arrowPos = new Vector2(curArrow.arrowObj.x, curArrow.arrowObj.y);
int curPointId = GetPointIdByPos(arrowPos, _levelConfig);
PointIdToRowCol(curPointId, _levelConfig.gridCols, out int curRow, out int curCol);
// 超出网格直接判定无阻挡
if (curRow < 0 || curRow >= _levelConfig.gridRows || curCol < 0 || curCol >= _levelConfig.gridCols)
return false;
// 获取移动方向
string moveDir;
if (curArrow.path != null && curArrow.path.Count > 0)
{
moveDir = curArrow.path[curArrow.path.Count - 1];
}
else if (!string.IsNullOrEmpty(curArrow.defaultDir))
{
moveDir = curArrow.defaultDir;
}
else
{
moveDir = "up";
}
bool isCollide = false;
// 沿方向遍历前方网格
switch (moveDir)
{
case "up":
for (int r = curRow - 1; r >= 0; r--)
{
if (_gridOccupied[r, curCol])
{
isCollide = true;
break;
}
}
break;
case "down":
for (int r = curRow + 1; r < _levelConfig.gridRows; r++)
{
if (_gridOccupied[r, curCol])
{
isCollide = true;
break;
}
}
break;
case "left":
for (int c = curCol - 1; c >= 0; c--)
{
if (_gridOccupied[curRow, c])
{
isCollide = true;
break;
}
}
break;
case "right":
for (int c = curCol + 1; c < _levelConfig.gridCols; c++)
{
if (_gridOccupied[curRow, c])
{
isCollide = true;
break;
}
}
break;
}
// ========== 新增:碰撞判重 + 扣爱心 ==========
if (isCollide)
{
// 这条线第一次碰撞,才扣血
if (!_collidedArrows.Contains(curArrow))
{
_collidedArrows.Add(curArrow);
_heartCount--;
if (_heartCount > 0) ui.HeartsPanel.GetChild($"xin_{_heartCount+1}").visible = false;
// 爱心归零 → 失败结算
if (_heartCount <= 0)
{
ShowResult(false);
}
}
}
return isCollide;
}
/// <summary>彻底删除指定箭头:销毁UI+清数据+停逻辑+移除列表</summary>
private void DeleteArrowCompletely(ArrowConfig arrow)
{
if (arrow == null) return;
// 被删除的是提示箭头,隐藏手指
if (_currentTipArrow == arrow)
{
HideFingerTip();
}
// 1. 停止移动标记,拦截Update和协程
arrow.isMoving = false;
// 2. 销毁箭头中所有线条UI
if (arrow.lineUnits != null)
{
foreach (var line in arrow.lineUnits)
{
if (line != null)
{
_viewContainer.RemoveChild(line);
line.Dispose();
}
}
arrow.lineUnits.Clear();
}
// 3. 销毁箭头本体UI
if (arrow.arrowObj != null)
{
_viewContainer.RemoveChild(arrow.arrowObj);
arrow.arrowObj.Dispose();
arrow.arrowObj = null;
}
// 4. 释放网格占用数据(和飞出边界逻辑保持一致)
ReleaseArrowOccupied(arrow);
// 5. 存活箭头计数-1
_remainArrowCount--;
// 6. 从总列表移除该箭头(核心:删除数据源)
if (_levelConfig != null && _levelConfig.arrows != null)
{
_levelConfig.arrows.Remove(arrow);
}
// 7. 可选:判断是否全部删除完成,触发结算(按需开启)
if (_remainArrowCount <= 0)
{
ShowResult(true);
}
// 8. 退出删除模式
_isDeleteMode = false;
Debug.Log("[arrow] 箭头已彻底删除");
}
// 创建箭头 + 绑定点击
private void CreateArrowOnly(ArrowConfig arrow, Vector2 arrowPos)
{
ArrowEnd arrowIns = ArrowEnd.CreateInstance();
arrowIns.SetPosition(arrowPos.x, arrowPos.y, 0);
arrowIns.touchable = true;
if (arrow.path != null && arrow.path.Count > 0)
{
string lastDir = arrow.path[arrow.path.Count - 1];
arrowIns.rotation = DirToRotation(lastDir);
}
else if (!string.IsNullOrEmpty(arrow.defaultDir))
{
// 纯单点箭头 使用默认方向旋转
arrowIns.rotation = DirToRotation(arrow.defaultDir);
}
ColorUtility.TryParseHtmlString(arrow.color, out Color arrowColor);
GImage icon = arrowIns.GetChild("icon").asImage;
if (icon != null) icon.color = arrowColor;
arrowIns.visible = true;
arrowIns.onClick.Add(() => OnPathClick(arrow));
_viewContainer.AddChild(arrowIns);
arrow.arrowObj = arrowIns;
arrow.isMoving = false;
}
// 移动后复位动画(回弹,不释放占用)
private void MoveThenReset(ArrowConfig arrow)
{
RunCoroutine(MoveAndResetCoroutine(arrow));
}
private IEnumerator MoveAndResetCoroutine(ArrowConfig arrow)
{
float frameDelay = 0.01f;
int stepCount = 2;
int moved = 0;
int trackMaxIndex = arrow.totalTrack.Count - 1;
string dir;
if (arrow.path != null && arrow.path.Count > 0)
{
dir = arrow.path[arrow.path.Count - 1];
}
else if (!string.IsNullOrEmpty(arrow.defaultDir))
{
dir = arrow.defaultDir;
}
else
{
dir = "up";
}
arrow.finalMoveDir = dir;
while (moved < stepCount)
{
// 新增空对象防护
if (arrow == null || arrow.arrowObj == null)
{
Debug.Log($"[arrow] MoveAndResetCoroutine arrow 为空, 协程结束");
yield break;
}
for (int i = 0; i < arrow.lineUnits.Count; i++)
{
int curIdx = arrow.lineTrackIndex[i];
if (curIdx < trackMaxIndex)
{
curIdx++;
arrow.lineUnits[i].SetPosition(arrow.totalTrack[curIdx].x, arrow.totalTrack[curIdx].y, 0);
arrow.lineTrackIndex[i] = curIdx;
}
else
{
float dx = 0, dy = 0;
switch (arrow.finalMoveDir)
{
case "right": dx = 10; break;
case "left": dx = -10; break;
case "down": dy = 10; break;
case "up": dy = -10; break;
}
arrow.lineUnits[i].x += dx;
arrow.lineUnits[i].y += dy;
}
}
int arrowIdx = arrow.arrowTrackIndex;
if (arrowIdx < trackMaxIndex)
{
arrowIdx++;
arrow.arrowObj.SetPosition(arrow.totalTrack[arrowIdx].x, arrow.totalTrack[arrowIdx].y, 0);
arrow.arrowTrackIndex = arrowIdx;
}
else
{
float dx = 0, dy = 0;
switch (arrow.finalMoveDir)
{
case "right": dx = 10; break;
case "left": dx = -10; break;
case "down": dy = 10; break;
case "up": dy = -10; break;
}
arrow.arrowObj.x += dx;
arrow.arrowObj.y += dy;
}
moved++;
yield return new WaitForSeconds(frameDelay);
}
// 复位位置
for (int i = 0; i < arrow.lineUnits.Count; i++)
{
arrow.lineUnits[i].SetPosition(arrow.originalLinePos[i].x, arrow.originalLinePos[i].y, 0);
arrow.lineTrackIndex[i] = i;
}
if(arrow.arrowObj != null)
{
arrow.arrowObj.SetPosition(arrow.originalArrowPos.x, arrow.originalArrowPos.y, 0);
}
arrow.arrowTrackIndex = arrow.totalTrack.Count - 1;
arrow.isMoving = false;
}
private void MoveToDisappear(ArrowConfig arrow)
{
RunCoroutine(MoveAndDisappearCoroutine(arrow));
}
private IEnumerator MoveAndDisappearCoroutine(ArrowConfig arrow)
{
Debug.Log($"[arrow] arrow.totalTrack.Count =={arrow.totalTrack.Count}");
float frameDelay = 0.01f;
int trackMaxIndex = arrow.totalTrack != null ? arrow.totalTrack.Count - 1 : 0;
string dir;
if (arrow.path != null && arrow.path.Count > 0)
{
dir = arrow.path[arrow.path.Count - 1];
}
else if (!string.IsNullOrEmpty(arrow.defaultDir))
{
dir = arrow.defaultDir;
}
else
{
dir = "up";
}
arrow.finalMoveDir = dir;
while (true)
{
// 兜底:对象已销毁,直接退出
if (arrow == null || arrow.arrowObj == null)
{
Debug.Log($"arrow 为空, 协程结束");
yield break;
}
// 线条移动
for (int i = 0; i < arrow.lineUnits.Count; i++)
{
int curIdx = arrow.lineTrackIndex[i];
if (curIdx < trackMaxIndex)
{
curIdx++;
arrow.lineUnits[i].SetPosition(arrow.totalTrack[curIdx].x, arrow.totalTrack[curIdx].y, 0);
arrow.lineTrackIndex[i] = curIdx;
}
else
{
float dx = 0, dy = 0;
switch (arrow.finalMoveDir)
{
case "right": dx = 10; break;
case "left": dx = -10; break;
case "down": dy = 10; break;
case "up": dy = -10; break;
}
arrow.lineUnits[i].x += dx;
arrow.lineUnits[i].y += dy;
}
}
// 箭头移动
int arrowIdx = arrow.arrowTrackIndex;
if (arrowIdx < trackMaxIndex)
{
arrowIdx++;
arrow.arrowObj.SetPosition(arrow.totalTrack[arrowIdx].x, arrow.totalTrack[arrowIdx].y, 0);
arrow.arrowTrackIndex = arrowIdx;
}
else
{
float dx = 0, dy = 0;
switch (arrow.finalMoveDir)
{
case "right": dx = 10; break;
case "left": dx = -10; break;
case "down": dy = 10; break;
case "up": dy = -10; break;
}
arrow.arrowObj.x += dx;
arrow.arrowObj.y += dy;
}
// ========= 边界判断:有线箭头取尾部轨迹,单点箭头取自身 =========
// 直接取箭头实时坐标,不再使用totalTrack静态点位
bool isOut = false;
float checkX = 0;
float checkY = 0;
// 线段从起点生成,第 0 个元素才是尾巴
if (arrow.lineUnits != null && arrow.lineUnits.Count > 0)
{
var tailLine = arrow.lineUnits[0];
checkX = tailLine.x;
checkY = tailLine.y;
}
else if (arrow.arrowObj != null)
{
// 纯单点箭头,使用自身坐标
checkX = arrow.arrowObj.x;
checkY = arrow.arrowObj.y;
}
Debug.Log($"[arrow] arrow.finalMoveDir =={arrow.finalMoveDir}");
switch (arrow.finalMoveDir)
{
case "right":
Debug.Log($"[arrow] checkX={checkX} | gridRight={gridRight} | gridRight + outOffset={gridRight + outOffset}");
if (checkX > gridRight + outOffset) isOut = true;
break;
case "left":
Debug.Log($"[arrow] checkX={checkX} | gridLeft={gridLeft} | gridLeft - outOffset={gridLeft - outOffset}");
if (checkX < gridLeft - outOffset) isOut = true;
break;
case "down":
Debug.Log($"[arrow] checkY={checkY} | gridBottom={gridBottom} | gridBottom + outOffset={gridBottom + outOffset}");
if (checkY > gridBottom + outOffset) isOut = true;
break;
case "up":
Debug.Log($"[arrow] checkY={checkY} | gridTop={gridTop} | gridTop - outOffset={gridTop - outOffset}");
if (checkY < gridTop - outOffset) isOut = true;
break;
}
// ========= 超出边界:隐藏 + 计数 + 终止协程 =========
Debug.Log($"[arrow] isOut =={isOut}");
if (isOut)
{
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);
}
yield break;
}
yield return new WaitForSeconds(frameDelay);
}
}
#endregion
#region 点击 & 移动逻辑
private void OnArrowClick(ArrowConfig arrow)
{
if (arrow.isMoving) return;
arrow.isMoving = true;
}
private void UpdateArrowMove(ArrowConfig arrow)
{
// 关键拦截:不允许移动 / 对象已销毁 → 直接退出
if (!arrow.isMoving || arrow.arrowObj == null)
return;
if (!arrow.isMoving) return;
List<Vector2> path = arrow.pathPointList;
if (arrow.targetIndex < 0)
{
arrow.isMoving = false;
arrow.arrowObj.visible = false;
foreach (var line in arrow.lineUnits) line.visible = false;
return;
}
Vector2 targetPos = path[arrow.targetIndex];
Vector2 curPos = new Vector2(arrow.arrowObj.x, arrow.arrowObj.y);
float distance = Vector2.Distance(curPos, targetPos);
if (distance < 2f)
{
arrow.targetIndex--;
}
else
{
Vector2 dir = (targetPos - curPos).normalized;
Vector2 newPos = curPos + dir * _moveSpeed * Time.deltaTime;
arrow.arrowObj.SetPosition(newPos.x, newPos.y, 0);
}
RefreshLineVisible(arrow);
}
private void RefreshLineVisible(ArrowConfig arrow)
{
int segIndex = 0;
int totalSeg = arrow.pathPointList.Count - 1;
for (int s = 0; s < totalSeg; s++)
{
bool isShow = s < arrow.targetIndex;
for (int i = 0; i < 6; i++)
{
arrow.lineUnits[segIndex + i].visible = isShow;
}
segIndex += 6;
}
}
#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 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
/// <summary>
/// 弹出结算
/// isSuccess: true=成功,false=失败
/// </summary>
private void ShowResult(bool isSuccess)
{
// 暂停所有交互/移动(可选)
foreach (var arrow in _levelConfig.arrows)
{
arrow.isMoving = false;
}
uiCtrlDispatcher.Dispatch(UICtrlMsg.LevelSuccessUI_Open, isSuccess);
}
// 重新开始本局
private void OnRestartGame(object a = null)
{
if (a!=null && (bool)a)
{
_heartCount = 3;
for (int i = _heartCount; i > 0; i--)
{
ui.HeartsPanel.GetChild($"xin_{i}").visible = true;
}
return;
}
// 1. 恢复默认缩放 1.0
float defaultScale = 1f;
_viewContainer.scaleX = defaultScale;
_viewContainer.scaleY = defaultScale;
// 2. 设置滑块对应位置
_zoomSlider.value = GetSliderValueByScale(defaultScale);
// 3. 容器屏幕居中
float startX = (Screen.width - _viewContainer.width) * 0.5f;
float startY = (Screen.height - _viewContainer.height) * 0.5f;
_viewContainer.SetPosition(startX, startY,0);
// 4. 清空手势状态
_isTwoFingerTouch = false;
_isDraging = false;
// 先清空当前所有动态物体
ClearAllDynamicObj();
// 重新走初始化流程
InitGridBounds();
InitGridOccupied();
_heartCount = 3;
_collidedArrows.Clear();
_remainArrowCount = _levelConfig.arrows.Count;
DataMgr.ArrowResultLevel.Value = 1;
InitView();
}
#region 消息监听
protected override void AddListener()
{
GameDispatcher.Instance.AddListener(GameMsg.reset_game, OnRestartGame);
}
protected override void RemoveListener()
{
GameDispatcher.Instance.RemoveListener(GameMsg.reset_game, OnRestartGame);
}
#endregion
}
#region 配置解析 & 数据结构
public static class JsonHelper
{
public static LevelConfig LoadLevel(string fileName)
{
string path = Path.Combine(Application.streamingAssetsPath, fileName);
return JsonUtility.FromJson<LevelConfig>(File.ReadAllText(path));
}
}
[System.Serializable]
public class LevelConfig
{
public int levelId;
public string levelName;
public int gridRows;
public int gridCols;
public int pointSpacing;
public List<ArrowConfig> arrows;
}
[System.Serializable]
public class ArrowConfig
{
public int id;
public int startPoint;
public int endPoint;
public string color;
public List<string> path;
public string defaultDir;
public List<Vector2> pathPointList;
public List<GComponent> lineUnits;
public GComponent arrowObj;
public bool isMoving;
public List<Vector2> originalLinePos;
public Vector2 originalArrowPos;
public int targetIndex;
public List<Vector2> totalTrack;
public List<int> lineTrackIndex;
public int arrowTrackIndex;
public string finalMoveDir;
}
#endregion
}