提交小游戏项目

This commit is contained in:
2026-07-06 16:27:14 +08:00
commit 2a717c6a2f
7575 changed files with 710847 additions and 0 deletions
@@ -0,0 +1,42 @@
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 让 Image 的透明区域不拦截点击事件
/// 挂在需要透明穿透的 Image 上即可
/// </summary>
[RequireComponent(typeof(Image))]
public class AlphaRaycastFilter : MonoBehaviour, ICanvasRaycastFilter
{
private Image image;
public float alphaThreshold = 0.1f; // 小于这个透明度就不拦截
void Awake()
{
image = GetComponent<Image>();
}
public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (image == null || image.sprite == null) return true;
RectTransform rt = image.rectTransform;
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, sp, eventCamera, out local);
Rect rect = rt.rect;
Vector2 normalized = new Vector2(
(local.x - rect.x) / rect.width,
(local.y - rect.y) / rect.height);
// 如果点在范围外,直接返回 false
if (normalized.x < 0 || normalized.x > 1 || normalized.y < 0 || normalized.y > 1)
return false;
Texture2D tex = image.sprite.texture;
// 使用 GetPixelBilinear,性能更好且避免越界
Color color = tex.GetPixelBilinear(normalized.x, normalized.y);
return color.a > alphaThreshold;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c745ea6e0254d434abe68e51a81b549b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,467 @@
using System.Collections.Generic;
using IgnoreOPS;
using Spine.Unity;
using UnityEngine;
using TowerClimberChronicles;
using FairyGUI;
using UnityEngine.UI;
using System.Collections;
using DG.Tweening;
public class CreatPuzzle : MonoBehaviour
{
public static CreatPuzzle instance;
private List<Sprite> img_list;
private List<Sprite> line_list;
private void Awake()
{
instance = this;
img_list = new List<Sprite>();
line_list = new List<Sprite>();
puzzle_parent = GameObject.Find("puzzle_parent");
GameDispatcher.Instance.AddListener(GameMsg.PuzzleIn, PuzzlenInFunc);
GameDispatcher.Instance.AddListener(GameMsg.PuzzleReset, PuzzlenResetFunc);
puzzle_list = new List<GameObject>();
}
private int rows = 6;
private int cols = 4;
List<List<float>> offsetList_x = new List<List<float>>()
{
new List<float>()
{
-229, -83, 87, 233
},
new List<float>()
{
-255f, -137f, -1.2f, 133.6f, 251.6f
},
new List<float>()
{
-250f, -86f, 81f, 243.3f
},
new List<float>()
{
-260f, -130.6f, 3.3f, 136f, 264.5f
},
new List<float>()
{
-225f, -119.3f, 82.4f, 223.9f
},
new List<float>()
{
-248f, -151.6f, -0.2f, 151.5f, 249f
},
new List<float>()
{
-223f, -89.9f, 80.2f, 217.1f
},
new List<float>()
{
-242f, -132.9f, 3.3f, 139f, 245.7f
},
new List<float>()
{
-225f, -84.5f, 83.8f, 224.3f
},
new List<float>()
{
-246f, -133.8f, 1.5f, 135.8f, 248f
},
};
List<List<float>> offsetList_y = new List<List<float>>()
{
new List<float>()
{
391, 250.9f, 82.5f, -89.5f, -259, -404.6f
},
new List<float>()
{
421f, 310.7f, 184.2f, 57.1f, -69.5f, -195.9f, -322.2f, -433.3f
},
new List<float>()//2
{
391, 228.1f, 60.3f, -108f, -274.5f, -438.2f
},
new List<float>()//3
{
421f, 299.4f, 175.2f, 50f, -75.6f, -200.7f, -326f, -447.6f
},
new List<float>()
{
413f, 271.5f, 87.2f, -97.6f, -281.8f, -421.3f
},
new List<float>()
{
428f, 324.3f, 190.2f, 56.4f, -77.3f, -211.4f, -344.9f, -451.3f
},
new List<float>()
{
381f, 244.3f, 74.6f, -95.7f, -265.5f, -398.2f
},
new List<float>()
{
413f, 312.6f, 185f, 57.7f, -69.8f, -197.2f, -324.4f, -425.4f
},
new List<float>()//8
{
391, 250.5f, 81.8f, -87.1f, -255.8f, -396.2f
},
new List<float>()
{
415f, 310f, 183.1f, 56.1f, -70.7f, -197.3f, -324f, -429.3f
},
};
List<List<Vector2>> image_offset_list = new List<List<Vector2>>()
{
new List<Vector2>()//0
{
new Vector2(1051, -1806), new Vector2(386, -1806), new Vector2(-386, -1806), new Vector2(-1051, -1806),
new Vector2(1051, -1169), new Vector2(386, -1169), new Vector2(-386, -1169), new Vector2(-1051, -1169),
new Vector2(1051, -401.2f), new Vector2(386, -401.2f), new Vector2(-386, -401.2f), new Vector2(-1051, -401.2f),
new Vector2(1051, 380), new Vector2(386, 380), new Vector2(-386, 380), new Vector2(-1051, 380),
new Vector2(1051, 1150.5f), new Vector2(386, 1150.5f), new Vector2(-386, 1150.5f), new Vector2(-1051, 1150.5f),
new Vector2(1051, 1810.5f), new Vector2(386, 1810.5f), new Vector2(-386, 1810.5f), new Vector2(-1051, 1810.5f),
},
new List<Vector2>()//1
{
new Vector2(1151, -1940), new Vector2(615, -1940), new Vector2(-3, -1940), new Vector2(-616.3f, -1940), new Vector2(-1153, -1940),
new Vector2(1151, -1439), new Vector2(615, -1439), new Vector2(-3, -1439), new Vector2(-616.3f, -1439), new Vector2(-1153, -1439),
new Vector2(1151, -864), new Vector2(615, -864), new Vector2(-3, -864), new Vector2(-616.3f, -864), new Vector2(-1153, -864),
new Vector2(1151, -287), new Vector2(615, -287), new Vector2(-3, -287), new Vector2(-616.3f, -287), new Vector2(-1153, -287),
new Vector2(1151, 288), new Vector2(615, 288), new Vector2(-3, 288), new Vector2(-616.3f, 288), new Vector2(-1153, 288),
new Vector2(1151, 862), new Vector2(615, 862), new Vector2(-3, 862), new Vector2(-616.3f, 862), new Vector2(-1153, 862),
new Vector2(1151, 1437), new Vector2(615,1437), new Vector2(-3, 1437), new Vector2(-616.3f, 1437), new Vector2(-1153,1437),
new Vector2(1151, 1941), new Vector2(615, 1941), new Vector2(-3, 1941), new Vector2(-616.3f, 1941), new Vector2(-1153, 1941),
},
new List<Vector2>()//2
{
new Vector2(1117, -1888), new Vector2(371, -1888), new Vector2(-389.5f, -1888), new Vector2(-1125, -1888),
new Vector2(1117, -1149), new Vector2(371, -1149), new Vector2(-389.5f, -1149), new Vector2(-1125, -1149),
new Vector2(1117, -386), new Vector2(371, -386), new Vector2(-389.5f, -386), new Vector2(-1125, -386),
new Vector2(1117, 378), new Vector2(371, 378), new Vector2(-389.5f, 378), new Vector2(-1125, 378),
new Vector2(1117, 1136), new Vector2(371, 1136), new Vector2(-389.5f, 1136), new Vector2(-1125, 1136),
new Vector2(1117, 1881), new Vector2(371, 1881), new Vector2(-389.5f, 1881), new Vector2(-1125, 1881),
},
new List<Vector2>()//3
{
new Vector2(1194, -1976), new Vector2(606.3f, -1976), new Vector2(-3, -1976), new Vector2(-606.5f, -1976), new Vector2(-1188, -1976),
new Vector2(1194, -1423), new Vector2(606.3f, -1423), new Vector2(-3, -1423), new Vector2(-606.5f, -1423), new Vector2(-1188, -1423),
new Vector2(1194, -858.4f), new Vector2(606.3f, -858.4f), new Vector2(-3, -858.4f), new Vector2(-606.5f, -858.4f), new Vector2(-1188, -858.4f),
new Vector2(1194, -291.9f), new Vector2(606.3f, -291.9f), new Vector2(-3, -291.9f), new Vector2(-606.5f, -291.9f), new Vector2(-1188, -291.9f),
new Vector2(1194, 280.8f), new Vector2(606.3f, 280.8f), new Vector2(-3, 280.8f), new Vector2(-606.5f, 280.8f), new Vector2(-1188, 280.8f),
new Vector2(1194, 848), new Vector2(606.3f, 848), new Vector2(-3, 848), new Vector2(-606.5f, 848), new Vector2(-1188, 848),
new Vector2(1194, 1419), new Vector2(606.3f,1419), new Vector2(-3, 1419), new Vector2(-606.5f, 1419), new Vector2(-1188,1419),
new Vector2(1194, 1971), new Vector2(606.3f, 1971), new Vector2(-3, 1971), new Vector2(-606.5f, 1971), new Vector2(-1188, 1971),
},
new List<Vector2>()//4
{
new Vector2(1020, -1890), new Vector2(539, -1890), new Vector2(-377.6f, -1890), new Vector2(-1018.6f, -1890),
new Vector2(1020, -1245), new Vector2(539, -1245), new Vector2(-377.6f, -1245), new Vector2(-1018.6f, -1245),
new Vector2(1020, -409), new Vector2(539, -409), new Vector2(-377.6f, -409), new Vector2(-1018.6f, -409),
new Vector2(1020, 430.7f), new Vector2(539, 430.7f), new Vector2(-377.6f, 430.7f), new Vector2(-1018.6f, 430.7f),
new Vector2(1020, 1267.9f), new Vector2(539, 1267.9f), new Vector2(-377.6f, 1267.9f), new Vector2(-1018.6f, 1267.9f),
new Vector2(1020, 1903), new Vector2(539, 1903), new Vector2(-377.6f, 1903), new Vector2(-1018.6f, 1903),
},
new List<Vector2>()//5
{
new Vector2(1132, -2006), new Vector2(693.7f, -2006), new Vector2(4.3f, -2006), new Vector2(-686.7f, -2006), new Vector2(-1127.6f, -2006),
new Vector2(1132, -1535), new Vector2(693.7f, -1535), new Vector2(4.3f, -1535), new Vector2(-686.7f, -1535), new Vector2(-1127.6f, -1535),
new Vector2(1132, -926), new Vector2(693.7f, -926), new Vector2(4.3f, -926), new Vector2(-686.7f, -926), new Vector2(-1127.6f, -926),
new Vector2(1132, -316.5f), new Vector2(693.7f, -316.5f), new Vector2(4.3f, -316.5f), new Vector2(-686.7f, -316.5f), new Vector2(-1127.6f, -316.5f),
new Vector2(1132, 290.5f), new Vector2(693.7f, 290.5f), new Vector2(4.3f, 290.5f), new Vector2(-686.7f, 290.5f), new Vector2(-1127.6f, 290.5f),
new Vector2(1132, 899.8f), new Vector2(693.7f, 899.8f), new Vector2(4.3f, 899.8f), new Vector2(-686.7f, 899.8f), new Vector2(-1127.6f, 899.8f),
new Vector2(1132, 1508), new Vector2(693.7f,1508), new Vector2(4.3f, 1508), new Vector2(-686.7f, 1508), new Vector2(-1127.6f,1508),
new Vector2(1132, 1990), new Vector2(693.7f, 1990), new Vector2(4.3f, 1990), new Vector2(-686.7f, 1990), new Vector2(-1127.6f, 1990),
},
new List<Vector2>()//6
{
new Vector2(1007, -1761), new Vector2(402, -1761), new Vector2(-370f, -1761), new Vector2(-991.3f, -1761),
new Vector2(1007, -1138), new Vector2(402, -1138), new Vector2(-370f, -1138), new Vector2(-991.3f, -1138),
new Vector2(1007, -369), new Vector2(402, -369), new Vector2(-370f, -369), new Vector2(-991.3f, -369),
new Vector2(1007, 405), new Vector2(402, 405), new Vector2(-370f, 405), new Vector2(-991.3f, 405),
new Vector2(1007, 1179), new Vector2(402, 1179), new Vector2(-370f, 1179), new Vector2(-991.3f, 1179),
new Vector2(1007, 1781), new Vector2(402, 1781), new Vector2(-370f, 1781), new Vector2(-991.3f, 1781),
},
new List<Vector2>()//7
{
new Vector2(1104, -1905), new Vector2(610, -1905), new Vector2(-3, -1905), new Vector2(-616, -1905), new Vector2(-1111.9f, -1905),
new Vector2(1104, -1451), new Vector2(610, -1451), new Vector2(-3, -1451), new Vector2(-616, -1451), new Vector2(-1111.9f, -1451),
new Vector2(1104, -871.3f), new Vector2(610, -871.3f), new Vector2(-3, -871.3f), new Vector2(-616, -871.3f), new Vector2(-1111.9f, -871.3f),
new Vector2(1104, -292.9f), new Vector2(610, -292.9f), new Vector2(-3, -292.9f), new Vector2(-616, -292.9f), new Vector2(-1111.9f, -292.9f),
new Vector2(1104, 288), new Vector2(610, 288), new Vector2(-3, 288), new Vector2(-616, 288), new Vector2(-1111.9f, 288),
new Vector2(1104, 866), new Vector2(610, 866), new Vector2(-3, 866), new Vector2(-616, 866), new Vector2(-1111.9f, 866),
new Vector2(1104, 1445), new Vector2(610, 1445), new Vector2(-3, 1445), new Vector2(-616, 1445), new Vector2(-1111.9f, 1445),
new Vector2(1104, 1904), new Vector2(610, 1904), new Vector2(-3, 1904), new Vector2(-616, 1904), new Vector2(-1111.9f, 1904),
},
new List<Vector2>()//8
{
new Vector2(1019, -1791), new Vector2(377.8f, -1791), new Vector2(-386f, -1791), new Vector2(-1026, -1791),
new Vector2(1019, -1153.4f), new Vector2(377.8f, -1153.4f), new Vector2(-386f, -1153.4f), new Vector2(-1026,-1153.4f),
new Vector2(1019, -386), new Vector2(377.8f, -386), new Vector2(-386f, -386), new Vector2(-1026, -386),
new Vector2(1019, 381.6f), new Vector2(377.8f, 381.6f), new Vector2(-386f, 381.6f), new Vector2(-1026, 381.6f),
new Vector2(1019, 1149), new Vector2(377.8f, 1149), new Vector2(-386f, 1149), new Vector2(-1026, 1149),
new Vector2(1019, 1787), new Vector2(377.8f, 1787), new Vector2(-386f, 1787), new Vector2(-1026, 1787),
},
new List<Vector2>()
{
new Vector2(1121, -1917), new Vector2(612, -1917), new Vector2(-3, -1917), new Vector2(-615, -1917), new Vector2(-1126, -1917),
new Vector2(1121, -1440), new Vector2(612, -1440), new Vector2(-3, -1440), new Vector2(-615, -1440), new Vector2(-1126, -1440),
new Vector2(1121, -863), new Vector2(612, -863), new Vector2(-3, -863), new Vector2(-615, -863), new Vector2(-1126, -863),
new Vector2(1121, -286), new Vector2(612, -286), new Vector2(-3, -286), new Vector2(-615, -286), new Vector2(-1126, -286),
new Vector2(1121, 292.8f), new Vector2(612, 292.8f), new Vector2(-3, 292.8f), new Vector2(-615, 292.8f), new Vector2(-1126, 292.8f),
new Vector2(1121, 868.5f), new Vector2(612, 868.5f), new Vector2(-3, 868.5f), new Vector2(-615, 868.5f), new Vector2(-1126, 868.5f),
new Vector2(1121, 1442), new Vector2(612, 1442), new Vector2(-3, 1442), new Vector2(-615, 1442), new Vector2(-1126, 1442),
new Vector2(1121, 1922), new Vector2(612, 1922), new Vector2(-3, 1922), new Vector2(-615, 1922), new Vector2(-1126, 1922),
},
};
private List<GameObject> puzzle_list;
public void PuzzlenResetFunc(object a = null)
{
Debug.Log("脚本里的reset");
for (int i = 0; i < puzzle_list.Count; i++)
{
GameObject.Destroy(puzzle_list[i]);
}
puzzle_list.Clear();
}
private void PuzzlenInFunc(object a = null)
{
remain_puzzle_numbers -= 1;
if (remain_puzzle_numbers <= 0)
{
// for (int i = 0; i < puzzle_list.Count; i++)
// {
// GameObject.Destroy(puzzle_list[i]);
// }
// puzzle_list.Clear();
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleSuccess);
AudioManager.Instance.PlayDynamicEffect(AudioConst.puzzle_success);
}
}
private GameObject puzzle_parent;
private int remain_puzzle_numbers;
public void CreatPuzzleItem(int type, string imageName, int puzzleNumbers, int puzzle_level)
{
GameObject prefab = Resources.Load<GameObject>("puzzle/item/item");
if (prefab == null) return;
GameObject canvas = GameObject.Find("Canvas");
if (canvas == null) return;
int all_puzzle_numbers = 0;
if (type % 2 == 0)
{
rows = 6;
cols = 4;
}
else
{
rows = 8;
cols = 5;
}
all_puzzle_numbers = rows * cols;
img_list.Clear();
line_list.Clear();
for (int i = 1; i <= all_puzzle_numbers; i++)
{
img_list.Add(Resources.Load<Sprite>("puzzle/mask/" + type + "/" + i));
line_list.Add(Resources.Load<Sprite>("puzzle/line/" + type + "/" + i + "-line"));
}
int index = 0;
// puzzleNumbers=1;
remain_puzzle_numbers = puzzleNumbers;
Debug.Log(type);
List<int> random_list = GetRandomNumbers(puzzleNumbers, 0, all_puzzle_numbers - 1);
List<UnityEngine.UI.Image> image_list = new List<UnityEngine.UI.Image>();
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
GameObject piece = Instantiate(prefab, canvas.transform);
RectTransform rt = piece.GetComponent<RectTransform>();
puzzle_list.Add(piece);
float x = offsetList_x[type][c];
float y = offsetList_y[type][r];
Vector2 basePos = new Vector2(x, y);
rt.anchoredPosition = basePos;
// 转换到根 Canvas 的局部坐标
Canvas rootCanvas = GameObject.Find("Canvas").GetComponent<Canvas>();
RectTransform canvasRect = rootCanvas.transform as RectTransform;
Vector3 worldPos = rt.position;
Vector2 canvasLocalPos = canvasRect.InverseTransformPoint(worldPos);
DragPuzzle dp = piece.GetComponent<DragPuzzle>();
dp.snapTarget = canvasLocalPos; // ✅ 用根 Canvas 的局部坐标
dp.snapDistance = 80f;
if (random_list.Contains(index))
{
piece.SetParent(puzzle_parent);
dp.locked = false;
}
else
{
}
// 设置图片
Transform mask = piece.transform.Find("mask_obj");
UnityEngine.UI.Image img = mask.GetComponent<UnityEngine.UI.Image>();
img.sprite = img_list[index];
img.SetNativeSize();
Transform image_obj = mask.transform.Find("image_obj");
UnityEngine.UI.Image img_1 = image_obj.GetComponent<UnityEngine.UI.Image>();
UnityEngine.UI.Image line_image = piece.transform.Find("line_obj").GetComponent<UnityEngine.UI.Image>();
line_image.sprite = line_list[index];
line_image.SetNativeSize();
// Sprite source = Resources.Load<Sprite>("puzzle/image/test_img1");
// img_1.sprite = source;
// img_1.SetNativeSize();
// img_1.rectTransform.anchoredPosition = image_offset_list[type][index];
image_list.Add(img_1);
index++;
}
}
Shuffle();
TextureHelper.setPuzzle(imageName, image_list, image_offset_list[type]);
RefereshList();
}
private ManualHorizontalList manualHorizontalList;
private void RefereshList()
{
if (manualHorizontalList == null) manualHorizontalList = GameObject.Find("puzzle_parent").GetComponent<ManualHorizontalList>();
Debug.Log("refreshhhhhhhhhhhhhh");
manualHorizontalList.hasSortedOnce = false;
DOVirtual.DelayedCall(0, () =>
{
manualHorizontalList.Refresh();
});
}
public void Shuffle()
{
if (puzzle_parent == null) return;
int count = puzzle_parent.transform.childCount;
// 把所有子物体存到数组里
Transform[] children = new Transform[count];
for (int i = 0; i < count; i++)
{
children[i] = puzzle_parent.transform.GetChild(i);
}
// 随机打乱数组
for (int i = 0; i < count; i++)
{
int randIndex = Random.Range(i, count);
Transform temp = children[i];
children[i] = children[randIndex];
children[randIndex] = temp;
}
// 重新设置 sibling 顺序
for (int i = 0; i < count; i++)
{
children[i].SetSiblingIndex(i);
}
}
List<int> GetRandomNumbers(int count, int min, int maxInclusive)
{
// 生成完整区间
List<int> numbers = new List<int>();
for (int i = min; i <= maxInclusive; i++)
{
numbers.Add(i);
}
// Fisher-Yates 洗牌
for (int i = numbers.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
int temp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = temp;
}
// 取前 count 个
return numbers.GetRange(0, count);
}
public ScrollRect scrollRect; // 指向你的 ScrollView
private float interval = 0.5f; // 每个拼图之间的间隔
private float flyDuration = 0.4f; // 每个拼图的飞行时长
// 倒数 n 个依次飞向各自的吸附点(调用各自 DragPuzzle.FlyAndSnap
public void FlyLastN(int n)
{
StartCoroutine(FlyLastNCoroutine(n));
}
private IEnumerator FlyLastNCoroutine(int n)
{
if (scrollRect == null) scrollRect = GameObject.Find("ScrollView").GetComponent<ScrollRect>();
RectTransform content = scrollRect.content;
int count = content.childCount;
if (count == 0 || n <= 0) yield break;
// 从后往前选 n 个
for (int i = 0; i < n; i++)
{
int index = count - 1 - i;
if (index < 0) break;
DragPuzzle piece = content.GetChild(index).GetComponent<DragPuzzle>();
if (piece != null)
{
piece.FlyAndSnap(flyDuration); // 调用“每个脚本自身”的飞方法
}
if (i < n - 1) // 最后一个不再等待
{
yield return new WaitForSeconds(interval);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df253e41f23a744f4b9bf720a76843bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,285 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Collections;
using TowerClimberChronicles;
using DG.Tweening;
[RequireComponent(typeof(RectTransform))]
public class DragPuzzle : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerDownHandler
{
[Header("Snap config")]
public Vector2 snapTarget;
public float snapDistance;
private bool lockOnSnap = true;
[Header("State")]
public bool locked = false; // 默认可交互,若要初始不可拖动改为 true
private RectTransform rect;
private Canvas rootCanvas;
private Camera uiCamera;
private ScrollRect scrollRect;
private bool isFlying = false;
private Vector2 pointerOffset; // 在当前父Rect空间的偏移
private Vector3 cachedLocalScale;
private Image overlay;
void Awake()
{
rect = GetComponent<RectTransform>();
rootCanvas = GetRootCanvas(transform);
scrollRect = GameObject.Find("ScrollView")?.GetComponent<ScrollRect>();
// 必须存在:EventSystem + Canvas(含GraphicRaycaster) + 本物体或子物体上有Graphic且RaycastTarget启用
uiCamera = rootCanvas && rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay
? null
: (rootCanvas ? rootCanvas.worldCamera : null);
cachedLocalScale = rect.localScale;
GameDispatcher.Instance.AddListener(GameMsg.PuzzleIn, PlayHighlight);
overlay = transform.Find("mask_obj/light_obj").GetComponent<Image>();
}
void OnDestroy()
{
GameDispatcher.Instance.RemoveListener(GameMsg.PuzzleIn, PlayHighlight);
}
private Canvas GetRootCanvas(Transform t)
{
Canvas c = t.GetComponentInParent<Canvas>();
return c ? c.rootCanvas : null;
}
public void OnPointerDown(PointerEventData eventData)
{
if (locked || isFlying) return;
// 不在点击阶段切换父物体,避免点击瞬间跳位/消失
// 仅计算当前父Rect空间的指针偏移
Vector2 parentLocalPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rect.parent as RectTransform,
eventData.position,
uiCamera,
out parentLocalPoint
);
pointerOffset = parentLocalPoint - rect.anchoredPosition;
}
// 替换你当前的 OnBeginDrag(保留核心逻辑,补回“中心锚点”)
public void OnBeginDrag(PointerEventData eventData)
{
if (locked || isFlying) return;
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleTimeStart);
// 提升到根 Canvas,保持世界坐标
if (rootCanvas && rect.parent != rootCanvas.transform)
{
Vector3 worldPos = rect.position;
rect.SetParent(rootCanvas.transform, true);
rect.position = worldPos;
rect.localScale = cachedLocalScale;
// 关键:开始拖拽时把锚点/枢轴改为中心,但保持世界位置不变
SetAnchorPivotCenterKeepWorld(rect);
// 重新计算偏移(父变成根 Canvas)
Vector2 canvasLocalPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rootCanvas.transform as RectTransform,
eventData.position,
uiCamera,
out canvasLocalPoint
);
pointerOffset = canvasLocalPoint - rect.anchoredPosition;
}
transform.SetAsLastSibling();
}
public void OnDrag(PointerEventData eventData)
{
if (locked || isFlying) return;
// 在当前父Rect空间移动(通常是根Canvas)
RectTransform parentRect = rect.parent as RectTransform;
Vector2 parentLocalPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parentRect,
eventData.position,
uiCamera,
out parentLocalPoint
);
rect.anchoredPosition = parentLocalPoint - pointerOffset;
}
// 在类里新增一个辅助方法:将锚点/枢轴改为中心,但保持世界位置不变
private static void SetAnchorPivotCenterKeepWorld(RectTransform rt)
{
// 记录世界位置与旋转
Vector3 worldPos = rt.position;
Quaternion worldRot = rt.rotation;
// 改锚点与枢轴为中心
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
// 恢复世界位置与旋转(避免位置跳变)
rt.position = worldPos;
rt.rotation = worldRot;
}
public void PlayHighlight(object obj)
{
Vector2 vec2 = (Vector2)obj;
float dist = Vector2.Distance(rect.position, vec2);
if (!locked && (dist > 0.1f)) return;
float delay = dist * 0.005f;
DOVirtual.DelayedCall(delay, () =>
{
// 清理旧动画
overlay.DOKill(true);
rect.DOKill(true);
// overlay 从透明开始
var c = overlay.color;
overlay.color = new Color(c.r, c.g, c.b, 0f);
// 高亮动画:淡入再淡出
overlay.DOFade(0.4f, 0.15f).SetEase(Ease.InOutSine)
.OnComplete(() =>
{
overlay.DOFade(0f, 0.15f).SetEase(Ease.InOutSine);
});
// 缩放动画:先缩小再放大
rect.localScale = cachedLocalScale;
rect.DOScale(cachedLocalScale * 0.99f, 0.3f).SetEase(Ease.InOutSine)
.OnComplete(() =>
{
rect.DOScale(cachedLocalScale, 0.3f).SetEase(Ease.InOutSine);
});
});
}
private ManualHorizontalList manualHorizontalList;
private void RefereshList()
{
if (manualHorizontalList == null) manualHorizontalList = GameObject.Find("puzzle_parent").GetComponent<ManualHorizontalList>();
manualHorizontalList.Refresh();
}
public void OnEndDrag(PointerEventData eventData)
{
if (locked || isFlying) return;
// 近吸附
float dist = Vector2.Distance(rect.anchoredPosition, snapTarget);
if (dist <= snapDistance)
{
rect.anchoredPosition = snapTarget;
Vector2 snapPos = rect.position;
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleIn, snapPos);
rect.SetSiblingIndex(2);
if (lockOnSnap) locked = true;
AudioManager.Instance.PlayDynamicEffect(AudioConst.puzzle_click);
RefereshList();
return;
}
// 若释放在 ScrollRect viewport 内,归还到 content 并插入排序
if (scrollRect)
{
RectTransform viewport = scrollRect.viewport ? scrollRect.viewport : scrollRect.GetComponent<RectTransform>();
if (RectTransformUtility.RectangleContainsScreenPoint(viewport, eventData.position, eventData.pressEventCamera))
{
RectTransform content = scrollRect.content;
Vector3 worldPos = rect.position;
rect.SetParent(content, true); // 保持世界坐标再转本地
Vector2 localPoint = content.InverseTransformPoint(worldPos);
rect.anchoredPosition = localPoint;
int insertIndex = content.childCount;
for (int i = 0; i < content.childCount; i++)
{
RectTransform child = content.GetChild(i) as RectTransform;
if (localPoint.x < child.localPosition.x)
{
insertIndex = i;
break;
}
}
rect.SetSiblingIndex(insertIndex);
rect.localScale = cachedLocalScale;
}
RefereshList();
}
}
// ---------------- 飞行吸附(忽略锁定即返回) ----------------
public void FlyAndSnap(float duration = 0.5f)
{
if (locked || isFlying) return;
isFlying = true;
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleTimeStart);
FlyToSnapTarget(duration);
}
private void FlyToSnapTarget(float duration)
{
// 在根Canvas中移动
if (rootCanvas && rect.parent != rootCanvas.transform)
{
Vector3 worldPos = rect.position;
rect.SetParent(rootCanvas.transform, true);
rect.position = worldPos;
rect.localScale = cachedLocalScale;
}
// 关键:自动飞行时也改锚点/枢轴为中心,但保持世界位置不变
Vector3 savedWorldPos = rect.position;
Quaternion savedWorldRot = rect.rotation;
rect.anchorMin = rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.position = savedWorldPos;
rect.rotation = savedWorldRot;
Vector2 startPos = rect.anchoredPosition;
Vector2 target = snapTarget;
// 如果不需要动画,直接完成
if (duration <= 0f || startPos == target)
{
rect.anchoredPosition = target;
DoneFly();
return;
}
// 使用 DOTween 动画
rect.DOAnchorPos(target, duration)
.SetEase(Ease.Linear)
.OnComplete(() =>
{
rect.anchoredPosition = target;
DoneFly();
});
}
private void DoneFly()
{
if (lockOnSnap) locked = true;
float dist = Vector2.Distance(rect.anchoredPosition, snapTarget);
rect.anchoredPosition = snapTarget;
Vector2 snapPos = rect.position;
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleIn, snapPos);
AudioManager.Instance.PlayDynamicEffect(AudioConst.puzzle_click);
RefereshList();
isFlying = false;
}
// 外部可显式设置锁定
public void SetLocked(bool value) => locked = value;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c538d1715a88d41709ba05450edc24d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,156 @@
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 计算 Sprite 的非透明可见区域(基于 sprite.rect),
/// 将其中心与 Image 矩形中心对齐,并把偏移应用到「父物体」的 anchoredPosition.y。
/// 同时缓存上下左右边界,避免重复扫描,并提供 UI 尺寸下的边界/宽度。
/// </summary>
[DisallowMultipleComponent]
public class ImageAlphaBounds : MonoBehaviour
{
[Range(0f, 1f)]
public float alphaThreshold = 0.1f;
private Sprite cachedSprite;
private int cachedTop;
private int cachedBottom;
private int cachedLeft;
private int cachedRight;
private bool cachedValid;
private void EnsureBounds(Image img)
{
Sprite sp = img.sprite;
if (cachedValid && cachedSprite == sp) return;
cachedSprite = sp;
cachedValid = false;
if (sp == null) return;
Texture2D tex = sp.texture;
if (tex == null || !tex.isReadable)
{
Debug.LogWarning($"{name}: Texture not readable. Enable Read/Write in import settings.");
return;
}
Rect r = sp.rect;
int sx = Mathf.RoundToInt(r.x);
int sy = Mathf.RoundToInt(r.y);
int sw = Mathf.RoundToInt(r.width);
int sh = Mathf.RoundToInt(r.height);
Color[] pixels = tex.GetPixels(sx, sy, sw, sh);
int top = -1, bottom = -1, left = -1, right = -1;
for (int y = 0; y < sh; y++)
{
int rowStart = y * sw;
for (int x = 0; x < sw; x++)
{
if (pixels[rowStart + x].a > alphaThreshold)
{
if (top == -1) top = y;
bottom = y;
if (left == -1 || x < left) left = x;
if (right == -1 || x > right) right = x;
}
}
}
if (top == -1 || bottom == -1)
{
cachedTop = cachedBottom = cachedLeft = cachedRight = -1;
}
else
{
cachedTop = top;
cachedBottom = bottom;
cachedLeft = left;
cachedRight = right;
}
cachedValid = true;
}
public void AdjustY()
{
Image img = GetComponent<Image>();
if (img == null || img.sprite == null) return;
EnsureBounds(img);
if (!cachedValid || cachedBottom < cachedTop) return;
RectTransform imgRT = img.rectTransform;
float fullHeightUI = imgRT.sizeDelta.y;
float spriteHeightPx = img.sprite.rect.height;
float scaleY = fullHeightUI / spriteHeightPx;
float centerVisibleFromTopPx = (cachedTop + cachedBottom + 1) * 0.5f;
float centerVisibleFromTopUI = centerVisibleFromTopPx * scaleY;
float imageCenterFromTopUI = fullHeightUI * 0.5f;
float deltaY = centerVisibleFromTopUI - imageCenterFromTopUI;
RectTransform parentRT = img.transform.parent as RectTransform;
if (parentRT != null)
{
Vector2 p = parentRT.anchoredPosition;
float parentScaleY = parentRT.localScale.y;
parentRT.anchoredPosition = new Vector2(p.x, -deltaY * parentScaleY);
}
}
public float GetOpaqueHeight()
{
if (!cachedValid || cachedBottom < cachedTop) return 0f;
return cachedBottom - cachedTop + 1;
}
public float GetOpaqueWidth()
{
if (!cachedValid || cachedRight < cachedLeft) return 0f;
return cachedRight - cachedLeft + 1;
}
public float GetOpaqueLeftUI()
{
Image img = GetComponent<Image>();
if (img == null || !cachedValid || cachedRight < cachedLeft) return 0f;
RectTransform rt = img.rectTransform;
float spriteWidthPx = img.sprite.rect.width;
float scaleX = rt.sizeDelta.x / spriteWidthPx;
return cachedLeft * scaleX;
}
public float GetOpaqueRightUI()
{
Image img = GetComponent<Image>();
if (img == null || !cachedValid || cachedRight < cachedLeft) return 0f;
RectTransform rt = img.rectTransform;
float spriteWidthPx = img.sprite.rect.width;
float scaleX = rt.sizeDelta.x / spriteWidthPx;
return cachedRight * scaleX;
}
/// <summary>
/// 返回不透明宽度的 UI 尺寸(换算自 sprite.rect 像素宽度)
/// </summary>
public float GetOpaqueWidthUI()
{
Image img = GetComponent<Image>();
if (img == null || !cachedValid || cachedRight < cachedLeft) return 0f;
RectTransform rt = img.rectTransform;
float spriteWidthPx = img.sprite.rect.width;
float scaleX = rt.sizeDelta.x / spriteWidthPx;
return (cachedRight - cachedLeft + 1) * scaleX;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93558a1f718494b5ab16c7712a7f5779
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,103 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// 手动横向列表布局,适合 ScrollRect 的 Content。
/// 自动把子物体的锚点和 pivot 改成左边,避免偏移。
/// 先按不透明像素高度(从小到大)排序,然后让相邻元素的「不透明右边界」与下一个的「不透明左边界」之间保持一致的间隔。
/// </summary>
[RequireComponent(typeof(RectTransform))]
public class ManualHorizontalList : MonoBehaviour
{
[Header("布局参数")]
[SerializeField] private float spacing = 0; // 可见内容之间的统一间隔(显示空间)
[SerializeField] private float paddingLeft = 120f; // 左边留白(显示空间)
[SerializeField] private float paddingRight = 120f; // 右边留白(显示空间)
// 在类里加一个字段
public bool hasSortedOnce = false;
public void Refresh()
{
RectTransform content = GetComponent<RectTransform>();
// 收集子物体与不透明高度
List<(RectTransform child, float height)> items = new List<(RectTransform, float)>();
for (int i = 0; i < transform.childCount; i++)
{
RectTransform child = transform.GetChild(i) as RectTransform;
if (child == null) continue;
ImageAlphaBounds bounds = child.Find("mask_obj")?.GetComponent<ImageAlphaBounds>();
float height = 0f;
if (bounds != null)
{
bounds.AdjustY(); // 纵向居中修正(不覆盖 Y
height = bounds.GetOpaqueHeight(); // 不透明高度(像素)
}
items.Add((child, height));
}
// 只在第一次时按高度排序,之后保持原有层级顺序
if (!hasSortedOnce)
{
// 第一次:按高度排序
items = items.OrderBy(item => item.height).ToList();
hasSortedOnce = true;
// 同步到场景层级
for (int i = 0; i < items.Count; i++)
{
items[i].child.SetSiblingIndex(i);
}
}
else
{
// 后续:按 Hierarchy 的顺序
items = items.OrderBy(item => item.child.GetSiblingIndex()).ToList();
}
// 后面你的横向排布逻辑保持不动……
float contentScaleX = content.localScale.x;
float currentRightDisplay = 200;
foreach (var item in items)
{
RectTransform child = item.child;
child.anchorMin = new Vector2(0f, child.anchorMin.y);
child.anchorMax = new Vector2(0f, child.anchorMax.y);
child.pivot = new Vector2(0f, child.pivot.y);
ImageAlphaBounds bounds = child.Find("mask_obj")?.GetComponent<ImageAlphaBounds>();
float opaqueLeftUI = 0f;
float opaqueWidthUI = child.sizeDelta.x;
if (bounds != null)
{
opaqueLeftUI = bounds.GetOpaqueLeftUI();
opaqueWidthUI = bounds.GetOpaqueWidthUI();
}
float scaleXTotal = 0.22f;
float opaqueLeftDisplay = opaqueLeftUI * scaleXTotal;
float opaqueWidthDisplay = opaqueWidthUI * scaleXTotal;
float targetOpaqueLeftDisplay = currentRightDisplay + spacing;
Vector2 pos = child.anchoredPosition;
child.anchoredPosition = new Vector2(targetOpaqueLeftDisplay - opaqueLeftDisplay, pos.y);
currentRightDisplay = targetOpaqueLeftDisplay + opaqueWidthDisplay;
}
float totalWidthDisplay = currentRightDisplay;
float totalWidthUnscaled = totalWidthDisplay;
content.sizeDelta = new Vector2(totalWidthUnscaled, content.sizeDelta.y);
content.pivot = new Vector2(0f, content.pivot.y);
content.anchorMin = new Vector2(0f, content.anchorMin.y);
content.anchorMax = new Vector2(0f, content.anchorMax.y);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d22e85d4ccc044fdd905c464c311aded
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using SGModule.ConfigLoader;
namespace TowerClimberChronicles
{
[ConfigKey("JigsawPuzzle")]
public class JigsawPuzzle
{
public int id;
public int level;
public int Type;
public int PlayTime;
public string ResourceImage;
public int Pieces;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1c609871e1464c639f7d0e3464cd7e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: