gameplay手游触屏支持,一些小优化和修复

This commit is contained in:
FloatGaming
2026-07-15 01:32:14 +08:00
parent 020ca473c7
commit d9990d6fdb
17 changed files with 1489 additions and 50 deletions
@@ -0,0 +1,32 @@
using System;
using System.Collections;
using UnityEngine;
public static class AllyHeroSkillSelectionValidator
{
public static IEnumerator ValidateAllHeroesAsync(int heroesPerFrame = 2, Action<bool> onCompleted = null)
{
AllyHero_SO[] heroes = RuntimeResourcesCache.LoadAllAllyHeroes();
bool anyChanged = false;
int processedThisFrame = 0;
int batchSize = Mathf.Max(1, heroesPerFrame);
for (int i = 0; i < heroes.Length; i++)
{
AllyHero_SO hero = heroes[i];
if (hero != null && hero.ValidateEquippedSkillSelections())
{
anyChanged = true;
}
processedThisFrame++;
if (processedThisFrame >= batchSize)
{
processedThisFrame = 0;
yield return null;
}
}
onCompleted?.Invoke(anyChanged);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aebb9d085d6f9c14fa7ff27d1b4e6e03
+110
View File
@@ -189,6 +189,77 @@ public class AllyHero_SO : ScriptableObject
return EquipmentSkillGroupLibrary.ResolveSkillGroup(groupID);
}
public bool ValidateEquippedSkillSelections(bool persistIfChanged = true)
{
LoadEquippedSkillsFromLocal();
LoadEquippedEquipmentFromLocal();
if (equippedSkillGroupIDs == null)
{
equippedSkillGroupIDs = Array.Empty<int>();
}
int currentLevelId = GetEffectiveLevelForCurrentEXP()?.levelID ?? 0;
int maxSkillSlots = Mathf.Max(0, GetEffectiveSkillSlotLimit());
List<int> validatedGroupIds = new List<int>(Mathf.Min(equippedSkillGroupIDs.Length, maxSkillSlots));
HashSet<int> seenGroupIds = new HashSet<int>();
for (int i = 0; i < equippedSkillGroupIDs.Length; i++)
{
int groupId = equippedSkillGroupIDs[i];
if (groupId <= 0 || !seenGroupIds.Add(groupId))
{
continue;
}
if (validatedGroupIds.Count >= maxSkillSlots)
{
continue;
}
SkillGroup group = GetOwnedSkillGroupByID(groupId);
if (group == null)
{
continue;
}
int requiredLevelId = Mathf.Clamp(group.thisSkill_levelLimit, 1, 4);
if (currentLevelId < requiredLevelId)
{
continue;
}
validatedGroupIds.Add(groupId);
}
int[] validatedArray = validatedGroupIds.ToArray();
bool changed = !AreSkillGroupIdArraysEqual(equippedSkillGroupIDs, validatedArray);
if (!changed)
{
return false;
}
equippedSkillGroupIDs = validatedArray;
if (persistIfChanged)
{
SaveEquippedSkillsToLocal();
}
#if UNITY_EDITOR
if (!Application.isPlaying)
{
EditorUtility.SetDirty(this);
if (persistIfChanged)
{
AssetDatabase.SaveAssets();
}
}
#endif
return true;
}
public int[] GetEffectiveEquippedSkillGroupIDs()
{
var result = new List<int>();
@@ -365,6 +436,45 @@ public class AllyHero_SO : ScriptableObject
result.Add(groupId);
}
private static bool AreSkillGroupIdArraysEqual(int[] left, int[] right)
{
int leftLength = left != null ? left.Length : 0;
int rightLength = right != null ? right.Length : 0;
if (leftLength != rightLength)
{
return false;
}
for (int i = 0; i < leftLength; i++)
{
if (left[i] != right[i])
{
return false;
}
}
return true;
}
private SkillGroup GetOwnedSkillGroupByID(int groupId)
{
if (skillGroups == null || skillGroups.Length == 0)
{
return null;
}
for (int i = 0; i < skillGroups.Length; i++)
{
SkillGroup group = skillGroups[i];
if (group != null && group.skillGroupID == groupId)
{
return group;
}
}
return null;
}
public int GetUnlockedLevelIndex()
{
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
+13 -2
View File
@@ -47,6 +47,7 @@ public class newTeamSelector : MonoBehaviour
private static AllyHero_SO[] cachedHeroes;
private static Dictionary<int, AllyHero_SO> cachedHeroesById;
private Coroutine refreshSkillsRoutine;
private Coroutine startupSkillValidationRoutine;
void Awake()
{
@@ -65,14 +66,24 @@ public class newTeamSelector : MonoBehaviour
{
RefreshTeamShareCodeDisplay();
if (instantiateOnStart)
{
CreateSlots();
StartCoroutine(LoadSelectedHeroesNextFrame());
}
if (startupSkillValidationRoutine != null)
{
StopCoroutine(startupSkillValidationRoutine);
}
startupSkillValidationRoutine = StartCoroutine(StartupLoadRoutine());
}
private IEnumerator LoadSelectedHeroesNextFrame()
private IEnumerator StartupLoadRoutine()
{
yield return null;
yield return AllyHeroSkillSelectionValidator.ValidateAllHeroesAsync();
LoadSelectedHeroes();
startupSkillValidationRoutine = null;
}
public void CreateSlots()
+2
View File
@@ -33,6 +33,8 @@ public class PauseManager : MonoBehaviour
[Header("Buttons")]
[SerializeField] private string pauseButtonScenePath = "artworks/NEW UI/zhuangshi (3)/Pause";
[Tooltip("手游暂停按钮:把场景里的暂停 Button 拖到这里,点击效果等同 ESC。\n" +
"留空则按 pauseButtonScenePath 或名为 \"Pause\" 的按钮自动查找。")]
[SerializeField] private Button pauseTriggerButton;
[SerializeField] private Button continueButton;
[SerializeField] private Button replayButton;
@@ -0,0 +1,182 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 手游触屏输入分发器:每帧遍历所有触摸点(及编辑器鼠标),用 Physics2D.OverlapPoint
/// 命中带 TrackTouchZone 的透明 Collider2D,把按下/抬起转发给 InputManager。
///
/// 为什么用这种方式而不是 UI 的 IPointerDownHandler
/// - 轨道会平移+旋转晃动,点击区必须跟随晃动的 ColliderOverlapPoint 支持旋转过的碰撞体)。
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 fingerId 配对按下/抬起)。
///
/// 判定逻辑完全不变:这里只调用现成的 PressTrack/ReleaseTrack
/// 判定时间戳仍由 Note.HandlePress 读取 GameplayClock.NowSongTimedspTime)。
///
/// 用法:场景里放一个空物体挂本脚本,把主相机拖到 gameplayCamera(留空则自动取 Camera.main)。
/// 5 块透明轨道点击区各挂 TrackTouchZone 并设置 trackIndex。
/// </summary>
public class TrackTouchInput : MonoBehaviour
{
[Tooltip("渲染 gameplay 的相机。留空则自动使用 Camera.main。")]
[SerializeField] private Camera gameplayCamera;
[Tooltip("是否在编辑器/PC 上用鼠标模拟单点触摸(方便调试)。")]
[SerializeField] private bool enableMouseFallback = true;
[Tooltip("命中检测使用的层。默认 Everything;如需只检测点击区层可在此限制。")]
[SerializeField] private LayerMask hitLayers = ~0;
[Tooltip("透视相机必填:轨道点击区所在平面的世界 Z 坐标(轨道 Collider 的 Z)。\n" +
"透视相机下屏幕点转世界点依赖深度,用它把射线交到轨道平面,相机怎么晃/推拉都精确。\n" +
"正交相机可忽略此项。")]
[SerializeField] private float trackPlaneWorldZ = 0f;
[Tooltip("相机是否为正交投影。正交相机忽略深度,命中转换更简单。\n" +
"留空自动按相机 orthographic 判断;一般不用手动改。")]
[SerializeField] private bool forceOrthographicMode = false;
// fingerId -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
private readonly Dictionary<int, int> _activeTouches = new Dictionary<int, int>();
// 鼠标模拟用的"手指 id",取一个不会和真实 fingerId 冲突的值。
private const int MouseFingerId = -100;
private int _mouseHeldTrack = -1;
private Camera Cam
{
get
{
if (gameplayCamera == null) gameplayCamera = Camera.main;
return gameplayCamera;
}
}
private void Update()
{
var im = InputManager.Instance;
if (im == null) return;
Camera cam = Cam;
if (cam == null) return;
ProcessTouches(im, cam);
if (enableMouseFallback && Input.touchCount == 0)
{
ProcessMouse(im, cam);
}
}
private void ProcessTouches(InputManager im, Camera cam)
{
for (int i = 0; i < Input.touchCount; i++)
{
Touch t = Input.GetTouch(i);
switch (t.phase)
{
case TouchPhase.Began:
{
int track = ResolveTrack(cam, t.position);
if (track >= 0)
{
_activeTouches[t.fingerId] = track;
im.PressTrack(track);
}
break;
}
case TouchPhase.Ended:
case TouchPhase.Canceled:
{
if (_activeTouches.TryGetValue(t.fingerId, out int track))
{
_activeTouches.Remove(t.fingerId);
im.ReleaseTrack(track);
}
break;
}
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属,
// 避免手指轻微滑动跨到相邻轨道时反复 Press/Release 造成误判。
}
}
}
private void ProcessMouse(InputManager im, Camera cam)
{
if (Input.GetMouseButtonDown(0))
{
int track = ResolveTrack(cam, Input.mousePosition);
if (track >= 0)
{
_mouseHeldTrack = track;
im.PressTrack(track);
}
}
else if (Input.GetMouseButtonUp(0))
{
if (_mouseHeldTrack >= 0)
{
im.ReleaseTrack(_mouseHeldTrack);
_mouseHeldTrack = -1;
}
}
}
/// <summary>
/// 把屏幕坐标转换为轨道平面上的世界点,用 OverlapPoint 命中轨道点击区,返回轨道索引;无命中返回 -1。
///
/// 相机移动/晃动/推拉时,每帧都用当前相机状态转换,所以命中自动跟随相机。
/// - 正交相机:ScreenToWorldPoint 忽略深度,直接取 x/y。
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点,
/// 避免因相机 Z 变化(如 cameraDash)导致命中点偏移。
/// </summary>
private int ResolveTrack(Camera cam, Vector2 screenPos)
{
Vector2 point;
if (forceOrthographicMode || cam.orthographic)
{
Vector3 world = cam.ScreenToWorldPoint(screenPos);
point = new Vector2(world.x, world.y);
}
else
{
// 透视:射线与轨道平面(法线 +Z,过 z = trackPlaneWorldZ)求交。
Ray ray = cam.ScreenPointToRay(screenPos);
float denom = ray.direction.z;
if (Mathf.Abs(denom) < 1e-6f)
{
return -1; // 射线与平面近乎平行,无有效交点
}
float t = (trackPlaneWorldZ - ray.origin.z) / denom;
if (t < 0f)
{
return -1; // 交点在相机背后
}
Vector3 world = ray.origin + ray.direction * t;
point = new Vector2(world.x, world.y);
}
Collider2D hit = Physics2D.OverlapPoint(point, hitLayers);
if (hit == null) return -1;
TrackTouchZone zone = hit.GetComponent<TrackTouchZone>();
if (zone == null) zone = hit.GetComponentInParent<TrackTouchZone>();
return zone != null ? zone.trackIndex : -1;
}
private void OnDisable()
{
// 组件被关闭/切场景时,释放所有还按住的轨道,避免音符卡在 held 状态。
var im = InputManager.Instance;
if (im != null)
{
foreach (var kv in _activeTouches)
{
im.ReleaseTrack(kv.Value);
}
if (_mouseHeldTrack >= 0) im.ReleaseTrack(_mouseHeldTrack);
}
_activeTouches.Clear();
_mouseHeldTrack = -1;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 16d03d0d613217b4ebdba1b8b63bf735
@@ -0,0 +1,118 @@
using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// Binds five pre-authored UI touch regions to the existing gameplay input path.
/// This script does not create runtime objects. You assign the region RectTransforms
/// in the inspector, and it ensures each one has an Image raycast target plus a
/// TrackTouchRegion configured with the matching track index.
/// </summary>
[DisallowMultipleComponent]
public class TrackTouchRegionLayoutController : MonoBehaviour
{
[Serializable]
public class RegionBinding
{
[Range(0, 4)]
public int trackIndex;
[Tooltip("Pre-authored UI region to bind. Assign in inspector.")]
public RectTransform regionObject;
[Tooltip("Optional debug tint. Alpha 0 keeps the region invisible while preserving raycasts.")]
public Color debugTint = new Color(1f, 1f, 1f, 0f);
}
[Header("Behavior")]
public bool bindOnStart = true;
public bool mobileOnly = true;
[Header("Prebound Regions")]
public RegionBinding[] regions = new RegionBinding[5];
private void Reset()
{
EnsureDefaultBindings();
}
private void Awake()
{
EnsureDefaultBindings();
}
private void Start()
{
if (!bindOnStart)
{
return;
}
if (mobileOnly && !Application.isMobilePlatform)
{
return;
}
ApplyBindings();
}
[ContextMenu("Apply Touch Region Bindings")]
public void ApplyBindings()
{
EnsureDefaultBindings();
for (int i = 0; i < regions.Length; i++)
{
BindRegion(regions[i], i);
}
}
private void EnsureDefaultBindings()
{
if (regions == null || regions.Length != 5)
{
regions = new RegionBinding[5];
}
for (int i = 0; i < regions.Length; i++)
{
if (regions[i] == null)
{
regions[i] = new RegionBinding();
}
regions[i].trackIndex = Mathf.Clamp(i, 0, 4);
}
}
private static void BindRegion(RegionBinding binding, int fallbackTrackIndex)
{
if (binding == null || binding.regionObject == null)
{
return;
}
int trackIndex = Mathf.Clamp(binding.trackIndex, 0, 4);
if (trackIndex != fallbackTrackIndex)
{
trackIndex = Mathf.Clamp(trackIndex, 0, 4);
}
Image image = binding.regionObject.GetComponent<Image>();
if (image == null)
{
image = binding.regionObject.gameObject.AddComponent<Image>();
}
image.color = binding.debugTint;
image.raycastTarget = true;
TrackTouchRegion touchRegion = binding.regionObject.GetComponent<TrackTouchRegion>();
if (touchRegion == null)
{
touchRegion = binding.regionObject.gameObject.AddComponent<TrackTouchRegion>();
}
touchRegion.trackIndex = trackIndex;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4d4eeea52b79c624bbd827fba1ce5029
@@ -0,0 +1,60 @@
using UnityEngine;
/// <summary>
/// 挂在一块"透明可点击区域"物体上,标记它对应哪条轨道(0-4)。
/// 该物体需带一个 Collider2D(推荐 BoxCollider2D,可旋转)。
///
/// 两种跟随晃动的方式:
/// A) 直接把本物体作为会晃动的轨道 GameObject 的子物体 —— Unity 自动跟随平移+旋转,
/// 不需要 followTarget,留空即可。
/// B) 无法作为子物体时,把会晃动的轨道 Transform 拖到 followTarget
/// 本脚本会在 LateUpdate 中把自身对齐到它(位置+旋转),保证点击区跟着晃。
///
/// 判定完全不经过这里:命中检测由 TrackTouchInput 遍历触摸完成,
/// 只调用 InputManager.PressTrack/ReleaseTrack,判定时机仍走 dspTime。
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class TrackTouchZone : MonoBehaviour
{
[Tooltip("本区域控制的轨道:0=红 1=绿 2=黄 3=紫 4=蓝")]
[Range(0, 4)]
public int trackIndex = 0;
[Tooltip("可选。若本物体不是轨道的子物体,把会晃动的轨道 Transform 拖到这里,区域会每帧对齐到它(位置+旋转)。")]
public Transform followTarget;
[Tooltip("followTarget 生效时,是否同步旋转(轨道会旋转时勾上)。")]
public bool followRotation = true;
private Collider2D _collider;
public Collider2D Collider
{
get
{
if (_collider == null) _collider = GetComponent<Collider2D>();
return _collider;
}
}
private void Awake()
{
_collider = GetComponent<Collider2D>();
}
private void LateUpdate()
{
// 在所有晃动逻辑(Update)之后对齐,避免落后一帧。
if (followTarget == null) return;
transform.position = new Vector3(
followTarget.position.x,
followTarget.position.y,
transform.position.z);
if (followRotation)
{
transform.rotation = followTarget.rotation;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 51fe12fce9a3cec41bb85f2ed4d0e5e3
@@ -35,10 +35,22 @@ public sealed class StartupSettingsApplier : MonoBehaviour
private void ApplyStartupSettings()
{
ApplyAndroidLandscapeOrientation();
graphicSettings.ApplySavedDisplaySettingsAtStartup(this);
ApplySavedAudioSettings();
}
private void ApplyAndroidLandscapeOrientation()
{
#if UNITY_ANDROID && !UNITY_EDITOR
Screen.autorotateToPortrait = false;
Screen.autorotateToPortraitUpsideDown = false;
Screen.autorotateToLandscapeLeft = true;
Screen.autorotateToLandscapeRight = true;
Screen.orientation = ScreenOrientation.AutoRotation;
#endif
}
private void ApplySavedAudioSettings()
{
audioSettingsPreloader[] preloaders = FindObjectsByType<audioSettingsPreloader>(FindObjectsInactive.Include, FindObjectsSortMode.None);