修复一堆bug 加入粒子系统代替原击打特效 焕新长音符逻辑 修复多重计分 更新判定管理和音符生成器 搞了一个免费包

This commit is contained in:
FloatGaming
2025-12-18 03:26:52 +08:00
parent 1dd6c95ec9
commit a7d8d71d10
1572 changed files with 1658779 additions and 527 deletions
+58 -10
View File
@@ -20,26 +20,58 @@ public class ScoreManager : MonoBehaviour
private int[] pmScoreSums = new int[5];
// per-track idol score sums (red, green, yellow, purple, blue)
public int red_idolScore_sum = 0;
public int green_idolScore_sum = 0;
public int yellow_idolScore_sum = 0;
public int purple_idolScore_sum = 0;
public int blue_idolScore_sum = 0;
// aggregate of the five track idol sums
public int allSum_idolScore = 0;
private int[] idolScoreSums = new int[5];
[Header("Judgement Statistics")]
public int countPerfect = 0;
public int countGreat = 0;
public int countGood = 0;
public int countMiss = 0;
public void ResetStatistics()
{
countPerfect = countGreat = countGood = countMiss = 0;
}
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
for (int i = 0; i < pmScoreSums.Length; i++) pmScoreSums[i] = 0;
for (int i = 0; i < idolScoreSums.Length; i++) idolScoreSums[i] = 0;
ResetStatistics();
}
/// <summary>
/// Add a per-note pm score delta to the specified track (0..4) and update named fields + aggregate.
/// Also calculates idol score as pmDelta * scoreEfficiency.
/// </summary>
public void AddPmScoreForTrack(int trackIndex, int delta)
public void AddPmScoreForTrack(int trackIndex, int pmDelta, float scoreEfficiency)
{
if (trackIndex < 0 || trackIndex >= pmScoreSums.Length) return;
if (delta == 0) return;
pmScoreSums[trackIndex] += delta;
if (pmDelta == 0) return;
pmScoreSums[trackIndex] += pmDelta;
// clamp at int.MaxValue-1 to avoid overflow
if (pmScoreSums[trackIndex] < 0) pmScoreSums[trackIndex] = 0;
if (pmScoreSums[trackIndex] > int.MaxValue - 1) pmScoreSums[trackIndex] = int.MaxValue - 1;
// calculate idol score: pmDelta * scoreEfficiency
int idolDelta = Mathf.FloorToInt(pmDelta * scoreEfficiency);
idolScoreSums[trackIndex] += idolDelta;
if (idolScoreSums[trackIndex] < 0) idolScoreSums[trackIndex] = 0;
if (idolScoreSums[trackIndex] > int.MaxValue - 1) idolScoreSums[trackIndex] = int.MaxValue - 1;
// update named fields for easy access
red_pmScore_sum = pmScoreSums[0];
green_pmScore_sum = pmScoreSums[1];
@@ -47,12 +79,21 @@ public class ScoreManager : MonoBehaviour
purple_pmScore_sum = pmScoreSums[3];
blue_pmScore_sum = pmScoreSums[4];
// recompute aggregate
long agg = 0;
for (int i = 0; i < pmScoreSums.Length; i++) agg += pmScoreSums[i];
allSum_pmScore = (int)Mathf.Min((float)agg, (float)int.MaxValue - 1f);
red_idolScore_sum = idolScoreSums[0];
green_idolScore_sum = idolScoreSums[1];
yellow_idolScore_sum = idolScoreSums[2];
purple_idolScore_sum = idolScoreSums[3];
blue_idolScore_sum = idolScoreSums[4];
Debug.Log($"[ScoreManager] Added {delta} to track {trackIndex} pm sum. New per-track sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSum={allSum_pmScore}");
// recompute aggregates
long aggPm = 0;
long aggIdol = 0;
for (int i = 0; i < pmScoreSums.Length; i++) aggPm += pmScoreSums[i];
for (int i = 0; i < idolScoreSums.Length; i++) aggIdol += idolScoreSums[i];
allSum_pmScore = (int)Mathf.Min((float)aggPm, (float)int.MaxValue - 1f);
allSum_idolScore = (int)Mathf.Min((float)aggIdol, (float)int.MaxValue - 1f);
Debug.Log($"[ScoreManager] Added {pmDelta} to track {trackIndex} pm sum, idol delta {idolDelta}. New pm sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSumPm={allSum_pmScore}. Idol sums: R{red_idolScore_sum} G{green_idolScore_sum} Y{yellow_idolScore_sum} P{purple_idolScore_sum} B{blue_idolScore_sum} -> allSumIdol={allSum_idolScore}");
// Update UI in teamUIController if available (per-track pm sums + aggregate)
var ui = teamUIController.Instance;
@@ -67,10 +108,17 @@ public class ScoreManager : MonoBehaviour
if (ui.purple_pmScore_sum != null) ui.purple_pmScore_sum.text = purple_pmScore_sum.ToString();
if (ui.blue_pmScore_sum != null) ui.blue_pmScore_sum.text = blue_pmScore_sum.ToString();
if (ui.allSum_pmScore != null) ui.allSum_pmScore.text = allSum_pmScore.ToString();
if (ui.red_idolScore_sum != null) ui.red_idolScore_sum.text = red_idolScore_sum.ToString();
if (ui.green_idolScore_sum != null) ui.green_idolScore_sum.text = green_idolScore_sum.ToString();
if (ui.yellow_idolScore_sum != null) ui.yellow_idolScore_sum.text = yellow_idolScore_sum.ToString();
if (ui.purple_idolScore_sum != null) ui.purple_idolScore_sum.text = purple_idolScore_sum.ToString();
if (ui.blue_idolScore_sum != null) ui.blue_idolScore_sum.text = blue_idolScore_sum.ToString();
if (ui.allSum_idolScore != null) ui.allSum_idolScore.text = allSum_idolScore.ToString();
}
catch (System.Exception ex)
{
Debug.LogWarning($"[ScoreManager] Failed to write pm sums to teamUIController fields: {ex}");
Debug.LogWarning($"[ScoreManager] Failed to write sums to teamUIController fields: {ex}");
}
}
@@ -187,4 +235,4 @@ public class ScoreManager : MonoBehaviour
Debug.LogWarning("[ScoreManager] teamUIController.Instance is null; cannot update UI.");
}
}
}
}
@@ -1,12 +1,19 @@
using UnityEngine;
using TMPro;
using System.Collections; // 保持对 Coroutine 的支持
using System.Collections.Generic; // For List
public class AnimationController : MonoBehaviour
{
public GameObject redEffect; // 红色击打动画对象
public GameObject greenEffect; // 绿色击打动画对象
public GameObject yellowEffect; // 黄色击打动画对象
public GameObject purpleEffect; // 紫色击打动画对象
public GameObject blueEffect; // 色击打动画对象
[HideInInspector] public bool isGlobalController = false;
public static AnimationController Global;
public GameObject redEffect; // 色击打动画对象
public GameObject greenEffect; // 绿色击打动画对象
public GameObject yellowEffect; // 黄色击打动画对象
public GameObject purpleEffect; // 紫色击打动画对象
public GameObject blueEffect; // 蓝色击打动画对象
private Animator redAnimator;
private Animator greenAnimator;
@@ -14,42 +21,434 @@ public class AnimationController : MonoBehaviour
private Animator purpleAnimator;
private Animator blueAnimator;
// Scene object used as particle source (must be assigned in scene or found automatically)
public GameObject hit_particular_object;
public GameObject hit_ring_object;
[Header("轨道颜色 (Hex Color Codes)")]
// F86F64 (浅红色/珊瑚色)
public string redColorHex = "#F86F64";
// 40EC90 (亮绿色)
public string greenColorHex = "#40EC90";
// F7CB6C (金黄色)
public string yellowColorHex = "#F7CB6C";
// F888E7 (亮粉色/洋红色)
public string purpleColorHex = "#F888E7";
// 4FACFE (天蓝色)
public string blueColorHex = "#4FACFE";
[Header("Hold 粒子配置")]
public float holdParticleInterval = 0.2f;
private Coroutine holdParticleCoroutine;
private bool holdActive = false;
private string currentHoldColor;
private bool isHolding = false;
// Track active particle instances for immediate cleanup
private List<GameObject> activeParticles = new List<GameObject>();
[Header("判定飘字prefabs")]
public GameObject perfect_judge_prefab;
public GameObject great_judge_prefab;
public GameObject good_judge_prefab;
public GameObject miss_judge_prefab;
[Header("轨道关联的判定文字 (Legacy Text)")]
// 请在 Inspector 中将对应的 UI Text 拖入
public TextMeshProUGUI red_track_judgementText;
public TextMeshProUGUI green_track_judgementText;
public TextMeshProUGUI yellow_track_judgementText;
public TextMeshProUGUI purple_track_judgementText;
public TextMeshProUGUI blue_track_judgementText;
public void StartHoldParticles(string color)
{
// Prefer the Global controller if available so Start/Stop always affect the same instance
if (AnimationController.Global != null && AnimationController.Global != this)
{
AnimationController.Global.InternalStartHoldParticles(color);
return;
}
InternalStartHoldParticles(color);
}
public void StopHoldParticles()
{
if (AnimationController.Global != null && AnimationController.Global != this)
{
AnimationController.Global.InternalStopHoldParticles();
return;
}
InternalStopHoldParticles();
}
// Internal implementations operate on this instance
private void InternalStartHoldParticles(string color)
{
if (holdActive) return;
holdActive = true;
currentHoldColor = color;
if (holdParticleCoroutine != null)
{
StopCoroutine(holdParticleCoroutine);
}
holdParticleCoroutine = StartCoroutine(HoldParticleRoutine(color));
}
private void InternalStopHoldParticles()
{
holdActive = false;
if (holdParticleCoroutine != null)
{
try { StopCoroutine(holdParticleCoroutine); } catch { }
holdParticleCoroutine = null;
}
// Immediately destroy all active particle instances
foreach (var p in activeParticles)
{
if (p != null)
{
Destroy(p);
}
}
activeParticles.Clear();
}
private IEnumerator HoldParticleRoutine(string color)
{
while (holdActive)
{
yield return new WaitForSeconds(Mathf.Max(0.01f, holdParticleInterval));
if (!holdActive) break;
PlayDestroyAnimation(color);
}
}
private void Awake()
{
// 获取每个动画对象的 Animator 组件
redAnimator = redEffect.GetComponent<Animator>();
greenAnimator = greenEffect.GetComponent<Animator>();
yellowAnimator = yellowEffect.GetComponent<Animator>();
purpleAnimator = purpleEffect.GetComponent<Animator>();
blueAnimator = blueEffect.GetComponent<Animator>();
if (isGlobalController)
{
if (Global != null && Global != this)
{
Debug.LogWarning("[AnimationController] Duplicate global detected, destroying self");
Destroy(this);
return;
}
Global = this;
DontDestroyOnLoad(gameObject);
Debug.Log("[AnimationController] Global AnimationController registered");
}
// Animator cache
redAnimator = redEffect != null ? redEffect.GetComponent<Animator>() : null;
greenAnimator = greenEffect != null ? greenEffect.GetComponent<Animator>() : null;
yellowAnimator = yellowEffect != null ? yellowEffect.GetComponent<Animator>() : null;
purpleAnimator = purpleEffect != null ? purpleEffect.GetComponent<Animator>() : null;
blueAnimator = blueEffect != null ? blueEffect.GetComponent<Animator>() : null;
// If the scene object reference was not assigned on this instance (common when this script is on a prefab),
// try to locate a scene object automatically so runtime instances can still use a shared particle source.
if (hit_particular_object == null)
{
// First try a tag-based lookup. Designer should assign the scene particle source a tag "HitParticleSource".
try
{
var byTag = GameObject.FindWithTag("HitParticleSource");
if (byTag != null)
{
hit_particular_object = byTag;
Debug.Log("AnimationController: assigned hit_particular_object via tag 'HitParticleSource'.");
}
}
catch { }
// Next try common names if tag not used
if (hit_particular_object == null)
{
var byName = GameObject.Find("HitParticleSource") ?? GameObject.Find("hit_particular_object") ?? GameObject.Find("Hit_Particle_Source");
if (byName != null)
{
hit_particular_object = byName;
Debug.Log("AnimationController: assigned hit_particular_object via GameObject.Find by name.");
}
}
if (hit_particular_object == null)
{
Debug.LogWarning("AnimationController: hit_particular_object not assigned in Inspector and automatic lookup failed.\n" +
"You can either assign a scene object to 'hit_particular_object' on the prefab instance in the scene,\n" +
"or tag the scene particle source with 'HitParticleSource', or place a GameObject named 'HitParticleSource' in the scene.");
}
}
}
public void PlayDestroyAnimation(string color)
{
// Only use the scene object as the template. No prefab fallback.
if (hit_particular_object == null)
{
Debug.LogError("hit_particular_object is null. Assign a scene particle source to AnimationController.hit_particular_object or tag a GameObject 'HitParticleSource'.");
// As a fallback, trigger the Animator-based effects
TriggerAnimatorEffect(color);
return;
}
GameObject source = hit_particular_object;
// choose spawn position: prefer per-color effect transform if available
Vector3 spawnPos = transform.position;
Transform parentTransform = null;
switch (color)
{
case "red":
// 重置之前的触发器
redAnimator.ResetTrigger("PlayRedDestroy");
redAnimator.SetTrigger("PlayRedDestroy");
break;
case "green":
greenAnimator.ResetTrigger("PlayGreenDestroy");
greenAnimator.SetTrigger("PlayGreenDestroy");
break;
case "yellow":
yellowAnimator.ResetTrigger("PlayYellowDestroy");
yellowAnimator.SetTrigger("PlayYellowDestroy");
break;
case "purple":
purpleAnimator.ResetTrigger("PlayPurpleDestroy");
purpleAnimator.SetTrigger("PlayPurpleDestroy");
break;
case "blue":
blueAnimator.ResetTrigger("PlayBlueDestroy");
blueAnimator.SetTrigger("PlayBlueDestroy");
break;
case "red": if (redEffect != null) { spawnPos = redEffect.transform.position; parentTransform = redEffect.transform; } break;
case "green": if (greenEffect != null) { spawnPos = greenEffect.transform.position; parentTransform = greenEffect.transform; } break;
case "yellow": if (yellowEffect != null) { spawnPos = yellowEffect.transform.position; parentTransform = yellowEffect.transform; } break;
case "purple": if (purpleEffect != null) { spawnPos = purpleEffect.transform.position; parentTransform = purpleEffect.transform; } break;
case "blue": if (blueEffect != null) { spawnPos = blueEffect.transform.position; parentTransform = blueEffect.transform; } break;
}
// 获取并解析颜色
Color trackColor;
string hexCode = GetHexCodeForColor(color);
// 尝试解析 16 进制颜色代码。如果失败,使用白色作为默认值。
if (!ColorUtility.TryParseHtmlString(hexCode, out trackColor))
{
trackColor = Color.white;
Debug.LogWarning($"Failed to parse hex color for '{color}' ({hexCode}). Using white.");
}
SpawnJudgePrefabByTrackText(color);
// Instantiate a copy of the scene object
GameObject particleInstance = Instantiate(source, spawnPos, Quaternion.identity);
if (particleInstance == null)
{
Debug.LogError("Failed to instantiate hit_particular_object");
TriggerAnimatorEffect(color);
return;
}
particleInstance.SetActive(true);
// Find all ParticleSystems on the instance (root + children) and set their startColor
var systems = particleInstance.GetComponentsInChildren<ParticleSystem>(true);
if (systems != null && systems.Length > 0)
{
float maxLifetime = 0f;
foreach (var ps in systems)
{
var main = ps.main;
// Set start color (works for most setups)
main.startColor = trackColor;
// Play the system
ps.Play();
// determine lifetime (use constantMax for safety)
var lifetime = main.startLifetime;
float life = lifetime.constantMax;
if (life <= 0f) life = lifetime.constant; // fallback
if (life > maxLifetime) maxLifetime = life;
}
// Optionally parent the instance under the effect object so it moves with UI/slot
if (parentTransform != null)
{
particleInstance.transform.SetParent(parentTransform, true);
}
// Destroy after max lifetime + small buffer
Destroy(particleInstance, Mathf.Max(0.5f, maxLifetime + 0.1f));
// Track for immediate cleanup
activeParticles.Add(particleInstance);
}
else
{
Debug.LogError("No ParticleSystem components found on hit_particular_object instance");
Destroy(particleInstance);
TriggerAnimatorEffect(color);
}
// NEW: instantiate and play ring effect (overlay) if assigned
if (hit_ring_object != null)
{
GameObject ringInstance = Instantiate(hit_ring_object, spawnPos, Quaternion.identity);
if (ringInstance != null)
{
Debug.Log($"AnimationController: instantiated hit_ring_object '{ringInstance.name}' at {spawnPos}. parentTransform={(parentTransform != null ? parentTransform.name : "null")} ");
// Parent before activation to avoid transform surprises when Simulation Space = Local
if (parentTransform != null)
{
ringInstance.transform.SetParent(parentTransform, false);
// keep world position at spawnPos
ringInstance.transform.position = spawnPos;
Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name);
}
// Activate after parenting
ringInstance.SetActive(true);
var ringSystems = ringInstance.GetComponentsInChildren<ParticleSystem>(true);
Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0));
if (ringSystems != null && ringSystems.Length > 0)
{
float ringMaxLife = 0f;
foreach (var rps in ringSystems)
{
var rmain = rps.main;
// log important runtime properties for debugging
Debug.Log($"ring PS: {rps.gameObject.name} simulationSpace={rmain.simulationSpace} startLifetime={rmain.startLifetime.constant} startColor={rmain.startColor.color}");
// reuse same track color
rmain.startColor = trackColor;
rps.Play();
var rlifetime = rmain.startLifetime;
float rlife = rlifetime.constantMax;
if (rlife <= 0f) rlife = rlifetime.constant;
if (rlife > ringMaxLife) ringMaxLife = rlife;
// Also log emission rate if available
try
{
var emission = rps.emission;
var rate = emission.rateOverTime;
Debug.Log($"ring PS emission rateOverTime.constant (approx) = {rate.constant}");
}
catch { }
}
Destroy(ringInstance, Mathf.Max(0.5f, ringMaxLife + 0.1f));
// Track for immediate cleanup
activeParticles.Add(ringInstance);
}
else
{
Debug.LogWarning("hit_ring_object has no ParticleSystem components");
Destroy(ringInstance);
}
}
else
{
Debug.LogError("Failed to instantiate hit_ring_object");
}
}
}
}
// 用于根据颜色名称获取对应的 16 进制代码
private string GetHexCodeForColor(string color)
{
switch (color)
{
case "red": return redColorHex;
case "green": return greenColorHex;
case "yellow": return yellowColorHex;
case "purple": return purpleColorHex;
case "blue": return blueColorHex;
default: return "#FFFFFF"; // 默认返回白色
}
}
private void TriggerAnimatorEffect(string color)
{
// Fallback to original Animator triggers if particle source missing
switch (color)
{
case "red":
if (redAnimator != null)
{
redAnimator.ResetTrigger("PlayRedDestroy");
redAnimator.SetTrigger("PlayRedDestroy");
}
break;
case "green":
if (greenAnimator != null)
{
greenAnimator.ResetTrigger("PlayGreenDestroy");
greenAnimator.SetTrigger("PlayGreenDestroy");
}
break;
case "yellow":
if (yellowAnimator != null)
{
yellowAnimator.ResetTrigger("PlayYellowDestroy");
yellowAnimator.SetTrigger("PlayYellowDestroy");
}
break;
case "purple":
if (purpleAnimator != null)
{
purpleAnimator.ResetTrigger("PlayPurpleDestroy");
purpleAnimator.SetTrigger("PlayPurpleDestroy");
}
break;
case "blue":
if (blueAnimator != null)
{
blueAnimator.ResetTrigger("PlayBlueDestroy");
blueAnimator.SetTrigger("PlayBlueDestroy");
}
break;
}
}
/// <summary>
/// 根据传入的颜色频道,定位到对应的 UI Text 并读取其内容
/// </summary>
private void SpawnJudgePrefabByTrackText(string color)
{
TextMeshProUGUI targetText = null;
Transform spawnPoint = null;
// 1. 匹配对应的文本组件和位置
switch (color)
{
case "red": targetText = red_track_judgementText; spawnPoint = redEffect != null ? redEffect.transform : null; break;
case "green": targetText = green_track_judgementText; spawnPoint = greenEffect != null ? greenEffect.transform : null; break;
case "yellow": targetText = yellow_track_judgementText; spawnPoint = yellowEffect != null ? yellowEffect.transform : null; break;
case "purple": targetText = purple_track_judgementText; spawnPoint = purpleEffect != null ? purpleEffect.transform : null; break;
case "blue": targetText = blue_track_judgementText; spawnPoint = blueEffect != null ? blueEffect.transform : null; break;
}
if (targetText != null && spawnPoint != null)
{
// 打印调试信息,看程序到底读到了什么文字
Debug.LogError($"轨道 {color} 当前读到的文字是: [{targetText.text}]");
}
// 2. 如果找到了文本且文本不为空,则执行喷出逻辑
if (targetText != null && spawnPoint != null && !string.IsNullOrEmpty(targetText.text))
{
DoExecutePrefabSpawn(targetText.text, spawnPoint);
}
}
/// <summary>
/// 最终执行实例化的函数
/// </summary>
private void DoExecutePrefabSpawn(string judgeResult, Transform spawnTransform)
{
GameObject prefabToUse = null;
// 字符串匹配(需确保与 InputManager 中传入的字符串一致)
if (judgeResult.Contains("Perfect")) prefabToUse = perfect_judge_prefab;
else if (judgeResult.Contains("Great")) prefabToUse = great_judge_prefab;
else if (judgeResult.Contains("Good")) prefabToUse = good_judge_prefab;
else if (judgeResult.Contains("Miss")) prefabToUse = miss_judge_prefab;
if (prefabToUse != null)
{
// 在对应特效点生成判定 Prefab
GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity);
// 自动销毁,防止堆积
Destroy(instance, 1.0f);
}
}
}
@@ -0,0 +1,163 @@
using UnityEngine;
using System.Collections;
public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
{
public static Animation_GenerateJudgementSituationPrefab Instance;
[Header("生成位置 (五个轨道)")]
public GameObject redEffect;
public GameObject greenEffect;
public GameObject yellowEffect;
public GameObject purpleEffect;
public GameObject blueEffect;
[Header("判定飘字 Prefabs")]
public GameObject perfect_judge_prefab;
public GameObject great_judge_prefab;
public GameObject good_judge_prefab;
public GameObject miss_judge_prefab;
[Header("缩放与随机")]
public float baseScale = 1.0f;
public bool useRandomScale = true;
public float minScaleMultiplier = 0.8f;
public float maxScaleMultiplier = 1.2f;
[Header("物理跳跃设置")]
public float jumpForce = 5.0f; // 向上弹起的初始速度
public float gravity = -12.0f; // 重力
[Header("时间与透明度控制 (秒)")]
public float fadeInTime = 0.1f; // 生成后多少秒完成渐显 (0->1)
public float fadeOutStartTime = 0.6f; // 生成后第几秒开始渐隐
public float fadeOutDuration = 0.3f; // 渐隐动画持续时长
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
public void SpawnJudgePrefab(string color, string judgeResult)
{
Transform spawnPoint = GetSpawnPoint(color);
if (spawnPoint == null) return;
GameObject prefabToUse = GetJudgePrefab(judgeResult);
if (prefabToUse == null) return;
GameObject instance = Instantiate(prefabToUse, spawnPoint);
instance.transform.localPosition = Vector3.zero;
// 处理缩放
float finalScale = baseScale;
if (useRandomScale) finalScale *= Random.Range(minScaleMultiplier, maxScaleMultiplier);
instance.transform.localScale = new Vector3(finalScale, finalScale, 1f);
// 开启运动逻辑
StartCoroutine(AnimateSprite(instance));
}
private IEnumerator AnimateSprite(GameObject obj)
{
if (obj == null) yield break;
SpriteRenderer sr = obj.GetComponent<SpriteRenderer>();
float elapsed = 0f;
float vVelocity = jumpForce;
Vector3 currentLocalPos = Vector3.zero;
// 计算总生命周期:开始渐隐的时间 + 渐隐持续的时长
float totalLifeTime = fadeOutStartTime + fadeOutDuration;
// 初始透明度
if (sr != null)
{
Color c = sr.color;
c.a = 0f;
sr.color = c;
}
// 只要没到总寿命,就持续执行
while (elapsed < totalLifeTime)
{
if (obj == null) yield break;
float dt = Time.deltaTime;
elapsed += dt;
// --- 1. 物理位移 ---
vVelocity += gravity * dt;
currentLocalPos.y += vVelocity * dt;
obj.transform.localPosition = currentLocalPos;
// --- 2. 透明度逻辑 (基于秒数) ---
if (sr != null)
{
float alpha = 1f;
// 渐显阶段:当前时间 < fadeInTime
if (elapsed < fadeInTime)
{
alpha = Mathf.InverseLerp(0f, fadeInTime, elapsed);
}
// 渐隐阶段:当前时间 > fadeOutStartTime
else if (elapsed > fadeOutStartTime)
{
// 在 fadeOutStartTime 到 totalLifeTime 之间从 1 变到 0
alpha = Mathf.InverseLerp(totalLifeTime, fadeOutStartTime, elapsed);
}
// 中间完全显示阶段
else
{
alpha = 1f;
}
Color c = sr.color;
c.a = Mathf.Clamp01(alpha);
sr.color = c;
}
yield return null;
}
// --- 确保最后一帧是完全透明的 ---
if (obj != null && sr != null)
{
Color c = sr.color;
c.a = 0f;
sr.color = c;
}
// 等待渲染完成,防止闪除
yield return new WaitForEndOfFrame();
if (obj != null) Destroy(obj);
}
private Transform GetSpawnPoint(string color)
{
switch (color.ToLower())
{
case "red": return redEffect?.transform;
case "green": return greenEffect?.transform;
case "yellow": return yellowEffect?.transform;
case "purple": return purpleEffect?.transform;
case "blue": return blueEffect?.transform;
default: return null;
}
}
private GameObject GetJudgePrefab(string result)
{
switch (result.ToLower())
{
case "perfect": return perfect_judge_prefab;
case "great": return great_judge_prefab;
case "good": return good_judge_prefab;
case "miss": return miss_judge_prefab;
default: return null;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cb6998cd4a7dca042832c2544ca33c76
@@ -9,6 +9,18 @@ public class NoteData
public string color; // 音符颜色(如 "red", "blue"
public string type; // 音符类型("tap" = 单击,"hold" = 长按)
public float length; // 音符长度(仅用于长按音符,单位:秒)
// New fields for judgement recording
// judgeOffsetMs: signed offset in milliseconds recorded at judgement time
// positive = early (玩家按在目标时间之前), negative = late (玩家按在目标时间之后)
// For tap notes this stores the single judgement offset. For hold notes, start judgement stored in judgeOffsetMs,
// and end judgement stored in judgeOffsetMsEnd. Misses do not write offsets (left as NaN).
public float judgeOffsetMs = float.NaN;
public float judgeOffsetMsEnd = float.NaN;
// Optionally record result strings for debugging/analysis
public string judgeResult = null;
public string judgeResultEnd = null;
}
[Serializable]
+156 -182
View File
@@ -4,14 +4,13 @@ using UnityEngine.UI;
using System.Collections.Generic;
public class BeatmapManager : MonoBehaviour
{
public Beatmap beatmap; // 当前谱面数据
// 音符生成器引用
public NoteSpawner noteSpawner;
// 新增:teamUIController 引用,直接拖拽赋值
// teamUIController 引用,直接拖拽赋值
public teamUIController uiController;
// Extra fields from beatmap JSON (stored temporarily)
@@ -40,15 +39,23 @@ public class BeatmapManager : MonoBehaviour
[HideInInspector] public TrackStates parsedTrackStates;
// Multipliers for difficulty levels
[Header("Difficulty Multipliers")]
public float ezMultiplier = 1f;
public float hdMultiplier = 1.5f;
public float inMultiplier = 2f;
public float imMultiplier = 3f;
// 新增:基础生命值缩放因子(Base HP Unit Scale
// 用于将 Note 数量(例如 709)缩放到 40000 左右的范围。
// 计算公式:NoteAmount * DifficultyMult * BaseScale ≈ TotalHP
[Header("HP Calculation Settings")]
[Tooltip("Scales the note amount to enemy HP. Example: NoteAmount * MaxMultiplier * BaseScale ≈ TotalHP")]
public float baseHpUnitScale = 1f;
// Total chart score and per-note score fields
public int totalChartScore = 1000000; // Default total score for a chart
public int perNoteScore;
public int leftoverScore;
public int totalChartScore = 1000000; // Default total score for a chart
public int perNoteScore;
public int leftoverScore;
// 从 JSON 文件加载谱面数据
public void LoadBeatmap(string fileName)
@@ -66,7 +73,7 @@ public class BeatmapManager : MonoBehaviour
}
}
// 新增:从原始 JSON 字符串加载谱面(允许任意磁盘路径先读取后传入)
// 从原始 JSON 字符串加载谱面(允许任意磁盘路径先读取后传入)
public void LoadBeatmapFromJsonString(string json)
{
if (string.IsNullOrEmpty(json))
@@ -77,7 +84,7 @@ public class BeatmapManager : MonoBehaviour
ProcessJsonAndLoad(json);
}
// 新增:解析 JSON 但不启动 NoteSpawner(用于 test mode 的延迟开始)
// 解析 JSON 但不启动 NoteSpawner(用于 test mode 的延迟开始)
public bool ParseJsonOnly(string json)
{
if (string.IsNullOrEmpty(json))
@@ -168,67 +175,8 @@ public class BeatmapManager : MonoBehaviour
beatmap = parsed;
Debug.Log("ParseJsonOnly: parsed beatmap " + beatmap.title);
// Process colorSegments if not empty (for both test and normal mode)
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
{
// Determine multiplier based on difficulty
float multiplier = ezMultiplier;
if (parsedDifficulty == 1) multiplier = hdMultiplier;
else if (parsedDifficulty == 2) multiplier = inMultiplier;
else if (parsedDifficulty == 3) multiplier = imMultiplier;
// Calculate total HP
float totalHP = parsedNoteAmount * multiplier;
// Set enemy slot IDs and calculate HP for each enemy
List<int> enemyIds = new List<int>();
foreach (var segment in parsedColorSegments)
{
Debug.Log($"Processing colorSegment: enemyID='{segment.enemyID}', percentage={segment.percentage}");
int enemyId;
if (!int.TryParse(segment.enemyID, out enemyId))
{
Debug.Log($"Failed to parse enemyID '{segment.enemyID}' to int");
continue;
}
Debug.Log($"Parsed enemyId: {enemyId}");
enemyIds.Add(enemyId);
// Calculate individual enemy HP
int enemyHP = Mathf.RoundToInt(totalHP * segment.percentage);
// Find and update the corresponding EnemyData_SO
EnemyData_SO[] allEnemies = Resources.LoadAll<EnemyData_SO>("");
foreach (var so in allEnemies)
{
if (so != null && so.enemyID == enemyId)
{
so.enemy_maxHP = enemyHP;
Debug.Log(string.Format("Set enemy {0} maxHP to {1}", enemyId, enemyHP));
break;
}
}
}
// Update teamUIController
if (uiController != null)
{
Debug.Log($"enemyIds before padding: {string.Join(",", enemyIds)}");
// Ensure enemySlotIds has exactly 5 elements, padding with 0 if necessary
while (enemyIds.Count < 5)
{
enemyIds.Add(0);
}
Debug.Log($"enemyIds after padding: {string.Join(",", enemyIds)}");
uiController.enemySlotIds = enemyIds;
Debug.LogWarning($"Assigned enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
uiController.PopulateEnemySOsFromIds();
}
else
{
Debug.LogError("teamUIController not assigned in the Inspector");
}
}
// 调用统一的敌人处理逻辑(包含 HP 计算和设置)
SetupEnemiesAndHP();
return true;
}
@@ -270,73 +218,90 @@ public class BeatmapManager : MonoBehaviour
// 加载默认谱面
LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
}
}
// Helper class used to parse extra fields from JSON that Beatmap doesn't define
[System.Serializable]
private class BeatmapExtra
// 获取当前难度乘数
private float GetCurrentDifficultyMultiplier()
{
public string title;
public string composer;
public string illustrator;
public string charter;
public string beatmapId;
public int duration; // as int per request
public int bpm; // as int per request
public int difficulty;
public string difficultyName;
public string createdDate;
public string lastSavedTime;
public string musicFile;
public string backgroundFile;
public int noteAmount;
public float globalDelaySeconds;
// New fields
public bool enemyList_isEmpty;
public ColorSegment[] colorSegments;
public NoteStatistic[] noteStatistics;
public TrackStates trackStates;
// 假设 1=EZ, 2=HD, 3=IN, 4=IM
if (parsedDifficulty == 1) return ezMultiplier;
if (parsedDifficulty == 2) return hdMultiplier;
if (parsedDifficulty == 3) return inMultiplier;
if (parsedDifficulty == 4) return imMultiplier;
return 1f; // 默认值
}
// New serializable classes for the extra fields
[System.Serializable]
public class ColorSegment
// 统一处理敌人 ID 提取、发送给 UI 以及 HP 计算的逻辑
private void SetupEnemiesAndHP()
{
public string enemyID;
public float percentage;
}
if (uiController == null)
{
Debug.LogError("teamUIController not assigned in BeatmapManager Inspector");
return;
}
[System.Serializable]
public class NoteStatistic
{
public string colorType;
public int count;
}
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
{
// 1. 提取敌人 IDs
List<int> enemyIds = new List<int>();
foreach (var segment in parsedColorSegments)
{
Debug.Log($"Processing colorSegment: enemyID='{segment.enemyID}', percentage={segment.percentage}");
int enemyId;
if (!int.TryParse(segment.enemyID, out enemyId))
{
Debug.LogError($"Failed to parse enemyID '{segment.enemyID}' to int");
continue;
}
enemyIds.Add(enemyId);
}
[System.Serializable]
public class TrackStates
{
public bool trackFading_active;
public TrackState red;
public TrackState green;
public TrackState yellow;
public TrackState purple;
public TrackState blue;
}
// 2. 补齐 5 个槽位
while (enemyIds.Count < 5)
{
enemyIds.Add(0);
}
[System.Serializable]
public class TrackState
{
public bool isOn;
public float fadeTime;
// A. 传递 ID 列表给 uiController
Debug.Log($"Assigning enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
uiController.enemySlotIds = enemyIds;
// B. 让 UI 控制器加载 SO (此时 SO 的 HP 仍是默认值)
uiController.PopulateEnemySOsFromIds();
// C. 计算并应用 HP 逻辑
if (parsedNoteAmount > 0)
{
float currentMultiplier = GetCurrentDifficultyMultiplier();
// 计算总 HP:音符数 * 难度乘数 * 基础缩放
float totalCalculatedHP = parsedNoteAmount * currentMultiplier * baseHpUnitScale;
// 计算活跃敌人数量(非0 ID
int activeEnemyCount = enemyIds.FindAll(id => id != 0).Count;
if (activeEnemyCount == 0) activeEnemyCount = 1; // 防止除零
// 计算单个敌人 HP (总血量 / 敌人数量,均匀分配)
int individualMaxHP = Mathf.RoundToInt(totalCalculatedHP / activeEnemyCount);
Debug.Log($"[BeatmapManager] HP Calc -> Notes: {parsedNoteAmount}, Multiplier: {currentMultiplier}, Total: {totalCalculatedHP:F0}, ActiveEnemies: {activeEnemyCount}, Per Enemy: {individualMaxHP}");
// D. 【关键调用】将计算出的 HP 回填到 teamUIController 中的 SO 中
uiController.ApplyCalculatedEnemyHP(individualMaxHP);
}
}
else
{
// 如果没有敌人,也要通知 UI 控制器清空列表
Debug.Log("Parsed enemy list is empty, clearing UI.");
uiController.enemySlotIds = new List<int> { 0, 0, 0, 0, 0 };
uiController.PopulateEnemySOsFromIds();
}
}
// Parse JSON string to Beatmap and extras, then pass to NoteSpawner
private void ProcessJsonAndLoad(string json)
{
// Debug.LogError("ProcessJsonAndLoad called");
if (string.IsNullOrEmpty(json))
{
Debug.LogError("ProcessJsonAndLoad: json is empty");
@@ -433,69 +398,18 @@ public class BeatmapManager : MonoBehaviour
// assign and hand off to spawner
beatmap = parsed;
Debug.Log("谱面已加载:" + beatmap.title);
noteSpawner.LoadBeatmap(beatmap);
// Process colorSegments if not empty
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
if (noteSpawner != null)
{
// Determine multiplier based on difficulty
float multiplier = ezMultiplier;
if (parsedDifficulty == 1) multiplier = hdMultiplier;
else if (parsedDifficulty == 2) multiplier = inMultiplier;
else if (parsedDifficulty == 3) multiplier = imMultiplier;
// Calculate total HP
float totalHP = parsedNoteAmount * multiplier;
// Set enemy slot IDs and calculate HP for each enemy
List<int> enemyIds = new List<int>();
foreach (var segment in parsedColorSegments)
{
Debug.Log($"Processing colorSegment: enemyID='{segment.enemyID}', percentage={segment.percentage}");
int enemyId;
if (!int.TryParse(segment.enemyID, out enemyId))
{
Debug.LogError($"Failed to parse enemyID '{segment.enemyID}' to int");
continue;
}
Debug.LogError($"Parsed enemyId: {enemyId}");
enemyIds.Add(enemyId);
// Calculate individual enemy HP
int enemyHP = Mathf.RoundToInt(totalHP * segment.percentage);
// Find and update the corresponding EnemyData_SO
EnemyData_SO[] allEnemies = Resources.LoadAll<EnemyData_SO>("");
foreach (var so in allEnemies)
{
if (so != null && so.enemyID == enemyId)
{
so.enemy_maxHP = enemyHP;
Debug.Log(string.Format("Set enemy {0} maxHP to {1}", enemyId, enemyHP));
break;
}
}
}
// Update teamUIController
if (uiController != null)
{
Debug.Log($"enemyIds before padding: {string.Join(",", enemyIds)}");
// Ensure enemySlotIds has exactly 5 elements, padding with 0 if necessary
while (enemyIds.Count < 5)
{
enemyIds.Add(0);
}
Debug.Log($"enemyIds after padding: {string.Join(",", enemyIds)}");
uiController.enemySlotIds = enemyIds;
Debug.LogError($"Assigned enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
uiController.PopulateEnemySOsFromIds();
}
else
{
Debug.LogError("teamUIController not assigned in the Inspector");
}
noteSpawner.LoadBeatmap(beatmap);
}
else
{
Debug.LogError("NoteSpawner is null in BeatmapManager.");
}
// 调用统一的敌人处理逻辑(包含 HP 计算和设置)
SetupEnemiesAndHP();
}
// Calculate per-note and leftover scores based on parsedNoteAmount
@@ -514,4 +428,64 @@ public class BeatmapManager : MonoBehaviour
Debug.LogWarning("parsedNoteAmount is zero or less; perNoteScore and leftoverScore set to 0.");
}
}
}
// Helper class used to parse extra fields from JSON that Beatmap doesn't define
[System.Serializable]
private class BeatmapExtra
{
public string title;
public string composer;
public string illustrator;
public string charter;
public string beatmapId;
public int duration; // as int per request
public int bpm; // as int per request
public int difficulty;
public string difficultyName;
public string createdDate;
public string lastSavedTime;
public string musicFile;
public string backgroundFile;
public int noteAmount;
public float globalDelaySeconds;
// New fields
public bool enemyList_isEmpty;
public ColorSegment[] colorSegments;
public NoteStatistic[] noteStatistics;
public TrackStates trackStates;
}
// New serializable classes for the extra fields
[System.Serializable]
public class ColorSegment
{
public string enemyID;
public float percentage;
}
[System.Serializable]
public class NoteStatistic
{
public string colorType;
public int count;
}
[System.Serializable]
public class TrackStates
{
public bool trackFading_active;
public TrackState red;
public TrackState green;
public TrackState yellow;
public TrackState purple;
public TrackState blue;
}
[System.Serializable]
public class TrackState
{
public bool isOn;
public float fadeTime;
}
}
+250 -119
View File
@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@@ -32,21 +32,37 @@ public class HoldNote : BaseNote
public KeyCode key;
public string type; // "start", "middle", or "end"
private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定
private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开
private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定
private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开
// 新增变量
private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定)
// 新增变量
private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定)
[Header("判定区间配置")]
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
[Header("判定区间配置")]
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
private NoteData noteData;
private bool _hasTriggeredOnThisHold = false; // ensure single trigger per hold note
// private bool _hasTriggeredOnThisHold = false; // ensure single trigger per hold note
private void Awake()
{
controller = GetComponent<HoldNoteController>();
anim = GetComponent<AnimationController>();
// Prefer the global shared AnimationController if available, otherwise fallback to local or scene instance
anim = AnimationController.Global;
if (anim == null)
{
anim = GetComponent<AnimationController>();
if (anim == null)
{
anim = FindObjectOfType<AnimationController>();
}
}
if (anim == null)
{
Debug.LogWarning("HoldNote: No AnimationController found (Global/local/scene). Particle effects will be unavailable.");
}
if (controller != null)
{
controller.OnJudgeZoneChanged += OnJudgeZoneChanged;
@@ -73,30 +89,26 @@ public class HoldNote : BaseNote
private void PlayHitAnimation()
{
if (anim != null)
var controller = AnimationController.Global ?? anim;
if (controller != null)
{
// 激活动画效果 GameObject
anim.gameObject.SetActive(true);
anim.PlayDestroyAnimation(noteColor);
// 延迟禁用动画效果 GameObject,确保动画播放完毕
StartCoroutine(DelayedDisableAnimation(anim.gameObject, 0.5f)); // 0.5秒后禁用
controller.PlayDestroyAnimation(noteColor);
}
}
private IEnumerator DelayedDisableAnimation(GameObject animationObject, float delay)
{
yield return new WaitForSeconds(delay);
animationObject.SetActive(false);
// No-op: we used to disable the shared AnimationController here which stopped coroutines.
// Keep as no-op to avoid side effects.
yield return null;
}
public void Setup(int id, int trackIndex, float speed, float time, float delay,
bool isEnd, float scheduledEnd, string color, KeyCode key, string type, NoteJudgeConfig judgeConfig)
bool isEnd, float scheduledEnd, string color, KeyCode key, string type, NoteJudgeConfig judgeConfig, NoteData noteData)
{
this.id = id;
this.trackIndex = trackIndex;
// also set BaseNote.TrackIndex so other systems using TrackIndex property work consistently
this.TrackIndex = trackIndex;
this.speed = speed;
this.startTime = time;
this.delay = delay;
@@ -117,19 +129,21 @@ public class HoldNote : BaseNote
this.hasEnteredLine = false;
this.hasReleased = false;
// 判定区间配置检查
// 判定区间配置检查
if (judgeConfig == null)
Debug.LogError($"HoldNoteJudgeConfig is null! 判定区间配置未传入!track={trackIndex}");
Debug.LogError($"HoldNoteJudgeConfig is null! 判定区间配置未传入!track={trackIndex}");
else
Debug.Log($"HoldNoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
this.noteData = noteData;
// Debug info to help investigate end note visibility
Debug.Log($"[HoldNote.Setup] id={noteID} segment={segment} type={type} hitTime={hitTime:F2} scheduledEnd={scheduledEndTime:F2} color={color} track={trackIndex}");
// 在 Setup 时注册初始状态
// 在 Setup 时注册初始状态
JudgeManager.Instance?.RegisterNoteReleased(noteID, false);
if (segment == NoteSegment.Start)
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定
if (segment == NoteSegment.End)
JudgeManager.Instance?.RegisterScheduledEndTime(noteID, scheduledEndTime);
@@ -140,33 +154,50 @@ public class HoldNote : BaseNote
private void Update()
{
// 如果当前片段已经判定过,直接返回
// 如果当前片段已经判定过,直接返回
if (isJudged) return;
// 自动 Miss 判定:当音符经过判定线且未被判定时
// 重新启动长按粒子协程,如果长按状态恢复且协程未运行
if (!isHoldActive && JudgeManager.Instance.IsStartJudged(noteID) && !JudgeManager.Instance.HasNoteReleased(noteID) && Input.GetKey(keyToPress))
{
isHoldActive = true;
AnimationController.Global?.StartHoldParticles(noteColor);
}
// 自动 Miss 判定:当音符经过判定线且未被判定时
if (hasEnteredLine && !isJudged)
{
if (segment == NoteSegment.Start && Time.time > hitTime + judgeConfig?.missRange)
if(segment == NoteSegment.Start && Time.time > hitTime + judgeConfig?.missRange)
{
Debug.Log($"[HoldNote] START段超时自动Miss: {noteColor}");
HandleStart();
Debug.Log($"[HoldNote] START段超时自动Miss(自动): {noteColor}");
// 只做 Miss 结果展示与登记
JudgeManager.Instance.RegisterStartJudged(noteID, false);
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
teamUIController.Instance?.OnJudgeResult("Miss");
isJudged = true;
ReturnToPool();
}
else if (segment == NoteSegment.End && Time.time > scheduledEndTime + (judgeConfig?.missRange ?? 0.3f))
{
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
HandleEnd(true);
isJudged = true;
}
}
// **通用长按状态更新:**
// **通用长按状态更新:**
if (isHoldActive && Input.GetKeyUp(keyToPress))
{
isHoldActive = false;
Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}");
AnimationController.Global?.StopHoldParticles();
Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}");
// 当玩家松开按键时,向 JudgeManager 注册该长音符已被释放,确保尾部能正确检查释放状态
// 当玩家松开按键时,向 JudgeManager 注册该长音符已被释放,确保尾部能正确检查释放状态
if (!hasReleased)
{
hasReleased = true;
@@ -176,21 +207,21 @@ public class HoldNote : BaseNote
}
}
if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress))
/*if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress))
{
isHoldActive = true;
}
}*/
// Start段判定
// Start段判定
if (segment == NoteSegment.Start && hasEnteredLine)
{
if (Input.GetKeyDown(keyToPress))
{
HandleStart();
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
}
}
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
else if (segment == NoteSegment.Middle
&& JudgeManager.Instance.IsStartJudged(noteID)
&& isHoldActive
@@ -200,16 +231,16 @@ public class HoldNote : BaseNote
{
if (hasEnteredLine)
{
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
PlayHitAnimation();
// 把通过事件上报给 JudgeManager(记录中段通过)
// 把通过事件上报给 JudgeManager(记录中段通过)
JudgeManager.Instance?.RegisterMiddlePassed(noteID);
ReturnToPool();
isJudged = true;
}
}
}
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
else if (segment == NoteSegment.End
&& JudgeManager.Instance.IsStartJudged(noteID)
&& !JudgeManager.Instance.HasNoteReleased(noteID))
@@ -231,7 +262,7 @@ public class HoldNote : BaseNote
if (segment == NoteSegment.Middle)
{
hasBeenHeldFromStart = Input.GetKey(keyToPress);
Debug.Log($"[HoldNote] Middle段进入判定线: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
Debug.Log($"[HoldNote] Middle段进入判定线: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
if (autoReturnCoroutine != null)
StopCoroutine(autoReturnCoroutine);
@@ -241,10 +272,10 @@ public class HoldNote : BaseNote
if (autoReturnCoroutine != null)
StopCoroutine(autoReturnCoroutine);
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isJudged)
{
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
HandleEnd();
isJudged = true;
// ReturnToPool handled inside HandleEnd
@@ -263,8 +294,8 @@ public class HoldNote : BaseNote
{
if (!JudgeManager.Instance.IsStartJudged(noteID))
{
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
// 确保未判定的 Start 段在离开判定线时登记为 Miss
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
// 确保未判定的 Start 段在离开判定线时登记为 Miss
HandleStart();
isJudged = true;
ReturnToPool();
@@ -272,7 +303,7 @@ public class HoldNote : BaseNote
}
else if (segment == NoteSegment.Middle)
{
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
ReturnToPool();
}
else if (segment == NoteSegment.End)
@@ -283,15 +314,15 @@ public class HoldNote : BaseNote
autoReturnCoroutine = null;
}
// 在尾部离开判定线时,按如下规则处理:
// - 如果头部未判定:补偿 Miss 并回收
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
// 在尾部离开判定线时,按如下规则处理:
// - 如果头部未判定:补偿 Miss 并回收
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
if (!isJudged)
{
if (!JudgeManager.Instance.IsStartJudged(noteID))
{
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
if (!JudgeManager.Instance.HasNoteReleased(noteID))
HandleEnd(true);
ReturnToPool();
@@ -300,15 +331,15 @@ public class HoldNote : BaseNote
{
if (JudgeManager.Instance.HasNoteReleased(noteID))
{
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
HandleEnd();
isJudged = true;
ReturnToPool();
}
else
{
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
}
}
}
@@ -341,19 +372,28 @@ public class HoldNote : BaseNote
private void HandleStart()
{
if (!JudgeManager.Instance.TryResolveStart(noteID))
{
Debug.Log($"[HoldNote] START 已被判定,跳过: {noteColor}");
return;
}
float pressTime = Time.time;
float rawOffsetMs = (hitTime - pressTime) * 1000f;
float offset = Mathf.Abs(pressTime - hitTime);
string result;
if (offset < 0.1f) // 判定成功
if (offset < (judgeConfig?.perfectRange ?? 0.1f)) // 判定成功
{
result = "Perfect";
Debug.Log($"[HoldNote] START判定成功(偏差={offset:F2}秒): {noteColor}");
ScoreManager.Instance.countPerfect += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定成功(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true; // 成功判定Start,长按状态激活
isHoldActive = true; // 成功判定Start,长按状态激活
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
// Trigger skills configured to fire on note hits for this slot when the Start is successfully hit.
if (!_hasTriggeredOnThisHold)
if (/*!_hasTriggeredOnThisHold*/JudgeManager.Instance.TryTriggerSkill(noteID))
{
try
{
@@ -371,15 +411,21 @@ public class HoldNote : BaseNote
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Start) threw: {ex}");
}
_hasTriggeredOnThisHold = true; // avoid triggering again at End
// _hasTriggeredOnThisHold = true; // avoid triggering again at End
}
}
else
{
result = "Miss";
Debug.Log($"[HoldNote] START Miss(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, false); // 明确标记未判定
isHoldActive = false; // Start Miss,长按状态不激活
if (JudgeManager.Instance.TryResolveStart(noteID)) // 或 TryResolveEnd
{
ScoreManager.Instance.countMiss += 1;
}
// if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START Miss(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, false); // 明确标记未判定
isHoldActive = false; // Start Miss,长按状态不激活
// For Miss we should still notify shared logic once so it behaves like short notes
try
@@ -392,15 +438,39 @@ public class HoldNote : BaseNote
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Start Miss) threw: {ex}");
}
_hasTriggeredOnThisHold = true; // ensure we don't trigger again at End
// _hasTriggeredOnThisHold = true; // ensure we don't trigger again at End
}
// show judge result and play sound / update combo like short notes
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
JudgeSoundManager.Instance?.PlayJudgeSound(result);
teamUIController.Instance?.OnJudgeResult(result);
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
StartCoroutine(DelayedReturn());
// --- Add scoring for Start like short notes ---
try
{
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
if (allyGo != null)
{
var ally = allyGo.GetComponent<AllyCombatant>();
if (ally != null)
{
int added = ally.AddScoreForJudge(result);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[HoldNote] Failed to add per-track score for START: {ex}");
}
if (segment != NoteSegment.Start)
{
StartCoroutine(DelayedReturn());
}
}
private IEnumerator DelayedReturn()
@@ -414,7 +484,12 @@ public class HoldNote : BaseNote
private void HandleEnd(bool forceMiss = false)
{
// 记录释放时间并在 JudgeManager 中登记已释放状态
if (!JudgeManager.Instance.TryResolveEnd(noteID))
{
Debug.Log($"[HoldNote] END 已被判定,跳过: {noteColor}");
return;
}
// 记录释放时间并在 JudgeManager 中登记已释放状态
if (!hasReleased)
{
releaseTime = Time.time;
@@ -423,15 +498,17 @@ public class HoldNote : BaseNote
}
string result;
float rawOffsetMsEnd = 0f;
// 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 Miss
// 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 Miss
if (!hasEnteredLine && !forceMiss)
{
Debug.Log($"[HoldNote] HandleEnd: 提前松手,记录释放但等待尾部进入判定区再判定. NoteID={noteID}, color={noteColor}");
Debug.Log($"[HoldNote] HandleEnd: 提前松手,记录释放但等待尾部进入判定区再判定. NoteID={noteID}, color={noteColor}");
return;
}
// 现在进行正常判定逻辑
// 现在进行正常判定逻辑
releaseTime = Time.time;
hasReleased = true;
JudgeManager.Instance.RegisterNoteReleased(noteID, true);
@@ -439,56 +516,101 @@ public class HoldNote : BaseNote
if (forceMiss || !JudgeManager.Instance.IsStartJudged(noteID))
{
result = "Miss";
Debug.LogWarning($"[HoldNote] END判定失败({noteColor}):{(forceMiss ? "Miss" : "")}");
}
else if (judgeConfig != null)
{
float diff = Mathf.Abs(releaseTime - scheduledEndTime);
if (diff <= judgeConfig.perfectRange)
// if (JudgeManager.Instance.TryResolveEnd(noteID))
{
result = "Perfect";
}
else if (diff <= judgeConfig.greatRange)
{
result = "Great";
}
else if (diff <= judgeConfig.goodRange)
{
result = "Good";
}
else if (diff <= judgeConfig.missRange)
{
result = "Miss";
}
else
{
result = "Miss";
if (result == "Perfect") ScoreManager.Instance.countPerfect += 1;
else if (result == "Great") ScoreManager.Instance.countGreat += 1;
else if (result == "Good") ScoreManager.Instance.countGood += 1;
else ScoreManager.Instance.countMiss += 1;
}
Debug.LogWarning($"[HoldNote] END判定失败({noteColor}):{(forceMiss ? "Miss" : "")}");
}
else
{
float diff = Mathf.Abs(releaseTime - scheduledEndTime);
if (diff <= 0.1f)
rawOffsetMsEnd = (scheduledEndTime - releaseTime) * 1000f;
if (judgeConfig != null)
{
result = "Perfect";
}
else if (diff < 0.2f)
{
result = "Great";
}
else if (diff < 0.3f)
{
result = "Good";
if (diff <= judgeConfig.perfectRange)
{
result = "Perfect";
}
else if (diff <= judgeConfig.greatRange)
{
result = "Great";
}
else if (diff <= judgeConfig.goodRange)
{
result = "Good";
}
else if (diff <= judgeConfig.missRange)
{
result = "Miss";
}
else
{
result = "Miss";
}
}
else
{
result = "Bad";
if (diff <= 0.1f)
{
result = "Perfect";
}
else if (diff < 0.2f)
{
result = "Great";
}
else if (diff < 0.3f)
{
result = "Good";
}
else
{
result = "Bad";
}
}
// --- 新增:根据最终确定的 result 统计 ---
if (result == "Perfect") ScoreManager.Instance.countPerfect += 1;
else if (result == "Great") ScoreManager.Instance.countGreat += 1;
else if (result == "Good") ScoreManager.Instance.countGood += 1;
else ScoreManager.Instance.countMiss += 1;
// --- 新增:记录偏差数据到 NoteData ---
if (noteData != null && result != "Miss")
{
noteData.judgeOffsetMsEnd = rawOffsetMsEnd;
}
}
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
teamUIController.Instance?.OnJudgeResult(result); // 新增:更新combo计数
teamUIController.Instance?.OnJudgeResult(result); // 新增:更新combo计数
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
// --- Add scoring for End like short notes ---
try
{
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
if (allyGo != null)
{
var ally = allyGo.GetComponent<AllyCombatant>();
if (ally != null)
{
int added = ally.AddScoreForJudge(result);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[HoldNote] Failed to add per-track score for END: {ex}");
}
// Notify SkillBuilder so Hold notes can trigger skills configured for Hold
if (!_hasTriggeredOnThisHold)
if (JudgeManager.Instance.TryTriggerSkill(noteID))
{
var sb = SkillBuilder.Instance;
if (sb == null)
@@ -499,7 +621,7 @@ public class HoldNote : BaseNote
{
var so = sb.GetAllyHeroSOBySlot(TrackIndex);
var def = so?.GetPrimarySkill();
Debug.Log($"[HoldNote] About to NotifyNoteHit: TrackIndex={TrackIndex} trackIndexField={trackIndex} so={(so!=null?so.name:"null")} skill={(def!=null?def.skillId:"null")} trigger={(def!=null?def.triggerCondition.ToString():"-")}");
Debug.Log($"[HoldNote] About to NotifyNoteHit: TrackIndex={TrackIndex} trackIndexField={trackIndex} so={(so != null ? so.name : "null")} skill={(def != null ? def.skillId : "null")} trigger={(def != null ? def.triggerCondition.ToString() : "-")}");
}
bool triggeredHold = false;
@@ -566,15 +688,20 @@ public class HoldNote : BaseNote
}
}
_hasTriggeredOnThisHold = true;
// _hasTriggeredOnThisHold = true;
Debug.Log($"[HoldNote] Hold end NotifyNoteHit called: TrackIndex={TrackIndex} trackIndexField={trackIndex} result={result} triggeredHold={triggeredHold} triggeredTap={triggeredTap}");
}
Debug.Log($"[HoldNote] END判定结果: {noteColor} {result} (release={releaseTime:F2}, target={scheduledEndTime:F2})");
Debug.Log($"[HoldNote] END判定结果: {noteColor} {result} (release={releaseTime:F2}, target={scheduledEndTime:F2})");
AnimationController.Global?.StopHoldParticles();
ReturnToPool();
}
private void OnDisable()
{
if (segment == NoteSegment.End)
{
AnimationController.Global?.StopHoldParticles();
}
if (autoReturnCoroutine != null)
{
StopCoroutine(autoReturnCoroutine);
@@ -582,19 +709,23 @@ public class HoldNote : BaseNote
}
if (segment == NoteSegment.End &&
!isJudged && // 尚未判定
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
!isJudged && // 尚未判定
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
{
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isHoldActive)
{
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
HandleEnd(); // 正常判定(基于 releaseTime
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
if (JudgeManager.Instance.TryResolveEnd(noteID))
{
HandleEnd();
}
isJudged = true;
}
else
{
Debug.Log($"[HoldNote] OnDisableEnd段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
Debug.Log($"[HoldNote] OnDisableEnd段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
}
}
@@ -603,11 +734,11 @@ public class HoldNote : BaseNote
private void ReturnToPool()
{
if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用
if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用
if (segment == NoteSegment.Start && !hasEnteredLine)
{
Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}");
Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}");
return;
}
@@ -632,9 +763,9 @@ public class HoldNote : BaseNote
scheduledEndTime = 0f;
hasEnteredLine = false;
isJudged = false;
isHoldActive = false; // 重置长按标记
hasBeenHeldFromStart = false; // 重置
_hasTriggeredOnThisHold = false; // 重置
isHoldActive = false; // 重置长按标记
hasBeenHeldFromStart = false; // 重置
// _hasTriggeredOnThisHold = false; // 重置
if (autoReturnCoroutine != null)
{
@@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: 86977693d5a2d364bb086303f60ca1b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
guid: 86977693d5a2d364bb086303f60ca1b3
@@ -5,40 +5,74 @@ public class JudgeManager : MonoBehaviour
{
public static JudgeManager Instance { get; private set; }
// ===== 原有数据 =====
private Dictionary<KeyCode, Queue<Note>> judgeQueues = new Dictionary<KeyCode, Queue<Note>>();
private Dictionary<string, bool> startJudgedNotes = new Dictionary<string, bool>();
private Dictionary<string, bool> releasedNotes = new Dictionary<string, bool>();
// 记录 End 段的 scheduledEndTime(防止 End 被回收后仍然能判定)
private Dictionary<string, float> noteEndTimes = new Dictionary<string, float>();
// 记录每条长音符中段通过的计数
private Dictionary<string, int> middlePassedCounts = new Dictionary<string, int>();
// ===== 新增:长音符全局互斥状态 =====
private class HoldJudgeState
{
public bool startResolved; // Start 是否已经判定过(成功 or 失败)
public bool endResolved; // End 是否已经判定过
public bool skillTriggered; // 技能是否已触发
}
private Dictionary<string, HoldJudgeState> holdStates = new Dictionary<string, HoldJudgeState>();
private HoldJudgeState GetHoldState(string noteID)
{
if (!holdStates.ContainsKey(noteID))
holdStates[noteID] = new HoldJudgeState();
return holdStates[noteID];
}
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
return;
}
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
/// <summary>
/// 记录某个颜色的长音符 End 段的 scheduledEndTime
/// </summary>
// ===== 新增:判定互斥闸门 =====
public bool TryResolveStart(string noteID)
{
var s = GetHoldState(noteID);
if (s.startResolved) return false;
s.startResolved = true;
return true;
}
public bool TryResolveEnd(string noteID)
{
var s = GetHoldState(noteID);
if (s.endResolved) return false;
s.endResolved = true;
return true;
}
public bool TryTriggerSkill(string noteID)
{
var s = GetHoldState(noteID);
if (s.skillTriggered) return false;
s.skillTriggered = true;
return true;
}
public void ClearHoldState(string noteID)
{
if (holdStates.ContainsKey(noteID))
holdStates.Remove(noteID);
}
// ===== 原有接口(保持不变) =====
public void RegisterScheduledEndTime(string noteID, float endTime)
{
noteEndTimes[noteID] = endTime;
}
/// <summary>
/// 获取某个颜色的 End 段的 scheduledEndTime
/// </summary>
public float GetScheduledEndTime(string noteID)
{
return noteEndTimes.ContainsKey(noteID) ? noteEndTimes[noteID] : 0f;
@@ -65,23 +99,15 @@ public class JudgeManager : MonoBehaviour
return releasedNotes.ContainsKey(noteID) && releasedNotes[noteID];
}
/// <summary>
/// 当音符进入判定区域时调用
/// </summary>
public void RegisterNote(KeyCode key, Note note)
{
if (!judgeQueues.ContainsKey(key))
judgeQueues[key] = new Queue<Note>();
if (!judgeQueues[key].Contains(note))
{
judgeQueues[key].Enqueue(note);
}
}
/// <summary>
/// 当音符离开判定区域时调用
/// </summary>
public void UnregisterNote(KeyCode key, Note note)
{
if (!judgeQueues.ContainsKey(key)) return;
@@ -91,39 +117,23 @@ public class JudgeManager : MonoBehaviour
{
Note n = judgeQueues[key].Dequeue();
if (n != note)
{
newQueue.Enqueue(n);
}
else
{
// **如果音符未被判定,则触发 Miss**
if (!n.IsJudged())
{
n.JudgeMiss();
}
}
else if (!n.IsJudged())
n.JudgeMiss();
}
judgeQueues[key] = newQueue;
}
/// <summary>
/// 按键按下时,判定队列中最早进入的音符
/// </summary>
public void JudgeEarliestNote(KeyCode key)
{
if (judgeQueues.ContainsKey(key) && judgeQueues[key].Count > 0)
{
Note note = judgeQueues[key].Dequeue(); // 只取最早的音符
Note note = judgeQueues[key].Dequeue();
if (note != null && !note.IsJudged())
{
note.Judge();
}
}
}
/// <summary>
/// 记录中段通过,用于调试或进一步判定策略
/// </summary>
public void RegisterMiddlePassed(string noteID)
{
if (!middlePassedCounts.ContainsKey(noteID))
@@ -139,9 +149,10 @@ public class JudgeManager : MonoBehaviour
public void ClearNoteRecord(string noteID)
{
if (startJudgedNotes.ContainsKey(noteID)) startJudgedNotes.Remove(noteID);
if (releasedNotes.ContainsKey(noteID)) releasedNotes.Remove(noteID);
if (noteEndTimes.ContainsKey(noteID)) noteEndTimes.Remove(noteID);
if (middlePassedCounts.ContainsKey(noteID)) middlePassedCounts.Remove(noteID);
startJudgedNotes.Remove(noteID);
releasedNotes.Remove(noteID);
noteEndTimes.Remove(noteID);
middlePassedCounts.Remove(noteID);
ClearHoldState(noteID);
}
}
}
+26 -9
View File
@@ -8,6 +8,8 @@ public class Note : BaseNote
private NoteController controller;
private bool isJudged = false;
private NoteData noteData;
[Header("判定区间配置")]
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
@@ -17,7 +19,7 @@ public class Note : BaseNote
controller = GetComponent<NoteController>();
}
public void Setup(KeyCode key, int trackIndex, float speed, float hitTime, string color, NoteJudgeConfig judgeConfig)
public void Setup(KeyCode key, int trackIndex, float speed, float hitTime, string color, NoteJudgeConfig judgeConfig, NoteData data)
{
keyToPress = key;
noteColor = color;
@@ -26,6 +28,7 @@ public class Note : BaseNote
this.hitTime = hitTime;
isJudged = false;
this.judgeConfig = judgeConfig;
this.noteData = data;
if (controller != null)
{
@@ -53,6 +56,8 @@ public class Note : BaseNote
return;
float timeDifference = Mathf.Abs(Time.time - hitTime);
// 计算正负偏差:正值提前,负值落后
float rawOffsetMs = (hitTime - Time.time) * 1000f;
string judgeResult = null;
if (judgeConfig != null)
@@ -61,22 +66,29 @@ public class Note : BaseNote
{
Debug.Log($"{keyToPress}: Perfect");
judgeResult = "Perfect";
ScoreManager.Instance.countPerfect += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
}
else if (timeDifference <= judgeConfig.greatRange)
{
Debug.Log($"{keyToPress}: Great");
judgeResult = "Great";
ScoreManager.Instance.countGreat += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
}
else if (timeDifference <= judgeConfig.goodRange)
{
Debug.Log($"{keyToPress}: Good");
judgeResult = "Good";
ScoreManager.Instance.countGood += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
}
else if (timeDifference <= judgeConfig.missRange)
{
Debug.Log($"{keyToPress}: Miss");
judgeResult = "Miss";
// Do not return here - let shared post-judge logic run so NotifyNoteHit is invoked for Miss as well
/*ScoreManager.Instance.countMiss += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;*/
}
else
{
@@ -112,6 +124,7 @@ public class Note : BaseNote
InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult);
JudgeSoundManager.Instance?.PlayJudgeSound(judgeResult);
teamUIController.Instance?.OnJudgeResult(judgeResult); // 更新combo计数
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, judgeResult);
// Calculate score addition via AllyCombatant and add to per-track pm sums
try
@@ -123,7 +136,8 @@ public class Note : BaseNote
if (ally != null)
{
int added = ally.AddScoreForJudge(judgeResult);
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
}
@@ -145,32 +159,34 @@ public class Note : BaseNote
public void Judge()
{
if (isJudged)
if (isJudged)
return;
isJudged = true;
if (controller != null)
{
controller.StopMovement();
controller.PlayHitEffect();
controller.PlayHitEffect();
}
ReturnToPool();
if (anim !=null)
if (anim != null)
{
anim.PlayDestroyAnimation(noteColor);
}
}
public void JudgeMiss()
{
if (isJudged) return;
isJudged = true;
ScoreManager.Instance.countMiss += 1;
Debug.Log($"{keyToPress} Miss");
// Ensure UI shows Miss and combo is updated when a note auto-misses
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
teamUIController.Instance?.OnJudgeResult("Miss");
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
// Add score for Miss as well (some systems may give 0)
try
@@ -182,7 +198,8 @@ public class Note : BaseNote
if (ally != null)
{
int added = ally.AddScoreForJudge("Miss");
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
}
@@ -233,4 +250,4 @@ public class Note : BaseNote
JudgeMiss();
}
}
}
}
@@ -8,11 +8,11 @@ public class NoteJudgeConfig : ScriptableObject
{
[Header("判定区间(单位:秒)")]
[Tooltip("Perfect")]
public float perfectRange = 0.05f;
public float perfectRange = 0.1f;
[Tooltip("Great")]
public float greatRange = 0.1f;
public float greatRange = 0.2f;
[Tooltip("Good")]
public float goodRange = 0.2f;
public float goodRange = 0.3f;
[Tooltip("Miss")]
public float missRange = 0.3f;
public float missRange = 0.5f;
}
@@ -115,7 +115,7 @@ public class NoteSpawner : MonoBehaviour
// pass realtime hit time to Note.Setup, include globalHitDelay
float rawHit = startTime + noteData.time + globalHitDelay;
float realtimeHit = Mathf.Max(0f, rawHit); // clamp to non-negative
noteScript.Setup(key, noteData.trackIndex, CalculateSpeed(), realtimeHit, noteData.color, judgeConfig);
noteScript.Setup(key, noteData.trackIndex, CalculateSpeed(), realtimeHit, noteData.color, judgeConfig, noteData);
if (GameConfig.verboseLogs) Debug.Log($"到达时间:{noteSpawnTime + (60f / bpm) * 4}");
}
else
@@ -179,7 +179,7 @@ public class NoteSpawner : MonoBehaviour
// pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay
float rawStartHit = startTime + noteData.time + globalHitDelay;
float startHit = Mathf.Max(0f, rawStartHit);
holdNote.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig);
holdNote.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData);
}
else
{
@@ -208,7 +208,7 @@ public class NoteSpawner : MonoBehaviour
// 中段都标记为 middlepass realtime base time and segmentDelay
float rawBase = startTime + noteData.time + globalHitDelay;
float baseHit = Mathf.Max(0f, rawBase);
holdSeg.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig);
holdSeg.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig, noteData);
}
else
{
@@ -238,7 +238,7 @@ public class NoteSpawner : MonoBehaviour
float endDelay = segmentCount * actualSegmentInterval; // 通常等于 noteData.length
float rawBase = startTime + noteData.time + globalHitDelay;
float baseHit = Mathf.Max(0f, rawBase);
holdEnd.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig);
holdEnd.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig, noteData);
}
else
{
@@ -0,0 +1,35 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class settlementController : MonoBehaviour
{
[Header("文本")]
public Text songName_Text;
[Tooltip("当前关卡的进度百分比")]
public Text thisLevel_currentPercentage_Text;
public Text finalScore_Text;
public Text pmScoreSum_Text;
public Text idolScoreSum_Text;
[Header("奖励内容信息")]
public Text reward_playerEXP_Text;
public Text reward_money_Text;
public Text reward_idolEXP_bottle_Text;
[Header("给多少钱")]
[SerializeField] private long moneyToGive_thisLevel;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 30f4ec0bd8dd71543ac8f08d62dcb010
@@ -603,13 +603,12 @@ public class teamUIController : MonoBehaviour
// Calculate total max HP for all recognized enemies
// Note: totalMaxHP is a class member and is intended to be the fixed maximum HP for the entire encounter.
totalMaxHP = 0;
if (recognizedEnemySOs != null)
RecalculateTotalMaxHP();
if (recognizedEnemySOs.Length > 0)
{
foreach (var so in recognizedEnemySOs)
{
if (so != null) totalMaxHP += so.enemy_maxHP;
}
enemyCurrentCount = 0;
SpawnNextEnemy();
}
// If enemies were populated and we have some, start spawning from the beginning
@@ -681,7 +680,9 @@ public class teamUIController : MonoBehaviour
[Tooltip("当前总分")]
public TextMeshProUGUI currentTotalScore;
[Header("谱面总分")]
public TextMeshProUGUI allSum_pmScore;
public TextMeshProUGUI allSum_pmScore;
[Header("爱豆总分")]
public TextMeshProUGUI allSum_idolScore;
// new: runtime enemy instance and UI sync fields
private EnemyCombatant enemyCombatantInstance;
@@ -743,6 +744,8 @@ public class teamUIController : MonoBehaviour
[SerializeField] private int teammate01_maxMana;
[Header("pmScore")]
public TextMeshProUGUI red_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI red_idolScore_sum;
[Header("teammate02")]
[Tooltip("父物体-控制整体显隐")]
@@ -779,6 +782,8 @@ public class teamUIController : MonoBehaviour
[SerializeField] private int teammate02_maxMana;
[Header("pmScore")]
public TextMeshProUGUI green_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI green_idolScore_sum;
[Header("teammate03")]
[Tooltip("父物体-控制整体显隐")]
@@ -815,6 +820,8 @@ public class teamUIController : MonoBehaviour
[SerializeField] private int teammate03_maxMana;
[Header("pmScore")]
public TextMeshProUGUI yellow_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI yellow_idolScore_sum;
[Header("teammate04")]
[Tooltip("父物体-控制整体显隐")]
@@ -851,6 +858,8 @@ public class teamUIController : MonoBehaviour
[SerializeField] private int teammate04_maxMana;
[Header("pmScore")]
public TextMeshProUGUI purple_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI purple_idolScore_sum;
[Header("teammate05")]
[Tooltip("父物体-控制整体显隐")]
@@ -887,6 +896,8 @@ public class teamUIController : MonoBehaviour
[SerializeField] private int teammate05_maxMana;
[Header("pmScore")]
public TextMeshProUGUI blue_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI blue_idolScore_sum;
[Header("currentEnemy")]
[Tooltip("父物体-控制整体显隐")]
@@ -1388,6 +1399,70 @@ public class teamUIController : MonoBehaviour
UpdateEnemyUIImmediate();
}
// teamUIController.cs - 【新增】
// =========================================================
// 供 BeatmapManager 调用的方法:设置单个敌人 HP
// =========================================================
/// <summary>
/// 将计算得到的单个敌人最大生命值写入到所有已识别的敌人 Scriptable Object 中。
/// </summary>
public void ApplyCalculatedEnemyHP(int individualMaxHP)
{
// 确保 recognizedEnemySOs 数组不为空
if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0)
{
Debug.Log($"[teamUIController] Applying calculated HP: {individualMaxHP} to {recognizedEnemySOs.Length} recognized enemies.");
// 1. 修改 SO 数据:遍历所有已识别的敌人 SO,并设置其最大生命值
foreach (var so in recognizedEnemySOs)
{
// 假设 EnemyData_SO 包含 public int enemy_maxHP; 字段
if (so != null)
{
so.enemy_maxHP = individualMaxHP;
}
}
// 2. 重新计算总血量并更新总血条
RecalculateTotalMaxHP();
// 3. 强制刷新当前生成的敌人实例
// 因为 Populate 已经在 Apply 之前生成了旧数据的敌人,所以需要重置
if (enemyCombatantInstance != null && enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length)
{
var currentSO = recognizedEnemySOs[enemyCurrentCount];
if (currentSO != null)
{
Debug.Log($"[teamUIController] Re-initializing current enemy instance with new HP: {individualMaxHP}");
enemyCombatantInstance.InitializeFromSO(currentSO); // 重新使用新的 SO 数据初始化实例
UpdateEnemyUIImmediate(); // 立即刷新 UI
}
}
}
}
// =========================================================
// 【新增】总生命值重新计算方法(封装原有的累加逻辑)
// =========================================================
/// <summary>
/// 遍历所有已识别敌人的 SO,累加它们的生命值,更新 totalMaxHP,并立即刷新总血条。
/// </summary>
private void RecalculateTotalMaxHP()
{
totalMaxHP = 0;
if (recognizedEnemySOs != null)
{
foreach (var so in recognizedEnemySOs)
{
if (so != null) totalMaxHP += so.enemy_maxHP;
}
}
Debug.Log($"[teamUIController] Total Max HP has been recalculated to: {totalMaxHP}");
// 刷新总血条上限显示,防止出现延迟
UpdateAllEnemyTotalHealthUIImmediate();
}
// 新增:在当前敌人 HP 变化时,更新总血条 UI
private void UpdateAllEnemyTotalHealthUIOnHpChange(int newCurrentHP)
{