特效新内容。修复gameplay致命bug和分数保存bug
This commit is contained in:
@@ -2,6 +2,7 @@ using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
|
||||
public class BeatmapManager : MonoBehaviour
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
assignedSongData = song;
|
||||
assignedDifficulty = difficulty;
|
||||
Debug.LogWarning($"BeatmapManager.AcceptSongData: accepted song={(song==null?"NULL":song.songName)}, difficulty={difficulty}");
|
||||
UpdateGameplayUI();
|
||||
}
|
||||
|
||||
public Beatmap beatmap; // Documentation text normalized.
|
||||
@@ -44,10 +46,14 @@ public class BeatmapManager : MonoBehaviour
|
||||
// Documentation text normalized.
|
||||
public teamUIController uiController;
|
||||
|
||||
public Image bgSpriteImage; // Documentation text normalized.
|
||||
[Header("UI Display")]
|
||||
public Text gameplay_songname;
|
||||
public TextMeshProUGUI gameplay_difficultyID;
|
||||
public TextMeshProUGUI gameplay_difficultyName;
|
||||
|
||||
public GameObject bgSpriteObject;
|
||||
public Image bgMainImage; // 场景主背景图 (UI Image)
|
||||
public Image startCanvas_image;
|
||||
public SpriteRenderer bgSpriteRenderer; // 场景 3D 背景图 (SpriteRenderer)
|
||||
|
||||
// Extra fields from beatmap JSON (stored temporarily)
|
||||
[HideInInspector] public string parsedTitle;
|
||||
@@ -347,38 +353,36 @@ public class BeatmapManager : MonoBehaviour
|
||||
Debug.LogWarning("GameManager or its musicSource not found; audio not assigned");
|
||||
}
|
||||
|
||||
// Assign fullscreen image from SongData to bgSprite if available
|
||||
// Assign fullscreen image from SongData to background images if available
|
||||
if (assignedSongData != null && assignedSongData.fullscreen_songPicture != null)
|
||||
{
|
||||
// Prefer setting a Scene GameObject's SpriteRenderer if provided
|
||||
if (bgSpriteObject != null)
|
||||
Sprite bgSprite = assignedSongData.fullscreen_songPicture;
|
||||
|
||||
if (bgMainImage != null)
|
||||
{
|
||||
var sr = bgSpriteObject.GetComponentInChildren<SpriteRenderer>();
|
||||
if (sr != null)
|
||||
{
|
||||
sr.sprite = assignedSongData.fullscreen_songPicture;
|
||||
// ensure fully visible
|
||||
sr.color = new Color(sr.color.r, sr.color.g, sr.color.b, 1f);
|
||||
if (startCanvas_image != null)
|
||||
{
|
||||
startCanvas_image.sprite = assignedSongData.fullscreen_songPicture;
|
||||
}
|
||||
sr = null;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to SpriteRenderer on bgSpriteObject for song {assignedSongData.songName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("bgSpriteObject has no SpriteRenderer in children; cannot assign fullscreen sprite");
|
||||
}
|
||||
bgMainImage.sprite = bgSprite;
|
||||
// ensure fully visible
|
||||
bgMainImage.color = new Color(bgMainImage.color.r, bgMainImage.color.g, bgMainImage.color.b, 1f);
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgMainImage for song {assignedSongData.songName}");
|
||||
}
|
||||
else if (bgSpriteImage != null)
|
||||
|
||||
if (bgSpriteRenderer != null)
|
||||
{
|
||||
bgSpriteImage.sprite = assignedSongData.fullscreen_songPicture;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgSpriteImage for song {assignedSongData.songName}");
|
||||
bgSpriteRenderer.sprite = bgSprite;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgSpriteRenderer for song {assignedSongData.songName}");
|
||||
// 释放引用以节省内存(按要求:完毕后令 sprite renderer = null)
|
||||
bgSpriteRenderer = null;
|
||||
}
|
||||
else
|
||||
|
||||
if (startCanvas_image != null)
|
||||
{
|
||||
Debug.LogWarning("No bg target (bgSpriteObject or bgSpriteImage) assigned; fullscreen sprite not applied");
|
||||
startCanvas_image.sprite = bgSprite;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to startCanvas_image for song {assignedSongData.songName}");
|
||||
}
|
||||
|
||||
if (bgMainImage == null && startCanvas_image == null)
|
||||
{
|
||||
Debug.LogWarning("No background Image targets assigned in BeatmapManager; fullscreen sprite not applied");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,8 +412,52 @@ public class BeatmapManager : MonoBehaviour
|
||||
else
|
||||
{
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
Debug.LogWarning("Default automatic demo beatmap load is commented out to prioritize SO-provided charts.");
|
||||
// Documentation text normalized.
|
||||
Debug.LogWarning("Default automatic demo beatmap load is commented out to prioritize SO-provided charts.");
|
||||
}
|
||||
|
||||
UpdateGameplayUI();
|
||||
|
||||
}
|
||||
|
||||
private void UpdateGameplayUI()
|
||||
{
|
||||
if (assignedSongData == null) return;
|
||||
|
||||
if (gameplay_songname != null)
|
||||
{
|
||||
gameplay_songname.text = assignedSongData.songName;
|
||||
}
|
||||
|
||||
if (gameplay_difficultyID != null)
|
||||
{
|
||||
// Find the ChartFileEntry matching the assignedDifficulty to get difficultyLEVEL (star rating)
|
||||
float diffLevel = 0;
|
||||
if (assignedSongData.chartFiles != null)
|
||||
{
|
||||
foreach (var entry in assignedSongData.chartFiles)
|
||||
{
|
||||
if (entry.difficulty == assignedDifficulty)
|
||||
{
|
||||
diffLevel = entry.difficultyLEVEL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
gameplay_difficultyID.text = diffLevel.ToString();
|
||||
}
|
||||
|
||||
if (gameplay_difficultyName != null)
|
||||
{
|
||||
string diffText = "Unknown";
|
||||
switch (assignedDifficulty)
|
||||
{
|
||||
case 0: diffText = "Easy"; break;
|
||||
case 1: diffText = "Hard"; break;
|
||||
case 2: diffText = "Incredible"; break;
|
||||
case 3: diffText = "Impossible"; break;
|
||||
}
|
||||
gameplay_difficultyName.text = diffText;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections;
|
||||
@@ -204,18 +204,18 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
try
|
||||
{
|
||||
// Reset audio time to 0 before playing to ensure playback starts from the beginning
|
||||
musicSource.time = 0f;
|
||||
float startTime = 0f;
|
||||
if (delaySeconds < 0f)
|
||||
startTime = -delaySeconds;
|
||||
|
||||
if (delaySeconds > 0f)
|
||||
{
|
||||
// Use coroutine instead of PlayDelayed so pause state can interrupt it
|
||||
playbackDelayCoroutine = StartCoroutine(DelayAndPlayMusic(delaySeconds));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Immediate playback (no delay)
|
||||
musicSource.time = 0f;
|
||||
float clampedStartTime = ClampAudioStartTime(startTime);
|
||||
musicSource.time = clampedStartTime;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
Debug.Log("GameManager.PlayMusicWithDelay: Music playback started immediately (no delay)");
|
||||
@@ -226,7 +226,8 @@ public class GameManager : MonoBehaviour
|
||||
Debug.LogWarning($"PlayMusicWithDelay failed: {ex}");
|
||||
try
|
||||
{
|
||||
musicSource.time = 0f;
|
||||
float clampedStartTime = ClampAudioStartTime(delaySeconds < 0f ? -delaySeconds : 0f);
|
||||
musicSource.time = clampedStartTime;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
}
|
||||
@@ -234,6 +235,14 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private float ClampAudioStartTime(float startTime)
|
||||
{
|
||||
if (musicSource == null || musicSource.clip == null)
|
||||
return Mathf.Max(0f, startTime);
|
||||
|
||||
return Mathf.Clamp(startTime, 0f, musicSource.clip.length);
|
||||
}
|
||||
|
||||
private IEnumerator DelayAndPlayMusic(float delaySeconds)
|
||||
{
|
||||
Debug.Log($"GameManager.DelayAndPlayMusic: waiting {delaySeconds} seconds before playing (respects pause state)");
|
||||
|
||||
@@ -37,6 +37,13 @@ public class GfxController : MonoBehaviour
|
||||
public GameObject k_o_fx;
|
||||
public GameObject koFatherObject;
|
||||
|
||||
[Header("Explosion Effect Settings")]
|
||||
[Tooltip("当单次伤害超过敌人最大生命值的百分比时触发爆炸 (0.2 = 20%)")]
|
||||
public float explosionDamageThresholdRatio = 0.2f;
|
||||
[Tooltip("爆炸特效是否使用独立的缩放设置 (否则跟随Hit特效缩放)")]
|
||||
public bool useExplosionSeparateScale = false;
|
||||
public float explosionFxScale = 1.5f;
|
||||
|
||||
[Header("Hit Effect Settings - Enemy")]
|
||||
public float enemyOffsetX = 0f;
|
||||
public float enemyOffsetY = 0f;
|
||||
@@ -123,11 +130,26 @@ public class GfxController : MonoBehaviour
|
||||
/// <param name="isEnemy">是否为敌人受击</param>
|
||||
/// <param name="customWorldPos">可选:自定义世界坐标播放(如匹配弹道终点)</param>
|
||||
/// <param name="scaleOverride">可选:强制指定特效缩放倍率</param>
|
||||
public void PlayHitFX(GameObject target, GameObject parent = null, bool isEnemy = false, Vector3? customWorldPos = null, float? scaleOverride = null)
|
||||
/// <param name="damageInfo">可选:传入伤害信息以判断是否触发爆炸特效 (damage, maxHealth)</param>
|
||||
public void PlayHitFX(GameObject target, GameObject parent = null, bool isEnemy = false, Vector3? customWorldPos = null, float? scaleOverride = null, (float damage, float maxHealth)? damageInfo = null)
|
||||
{
|
||||
if (target == null || hitFX == null)
|
||||
// Determine which FX prefab to use (Hit or Explosion)
|
||||
GameObject fxPrefab = hitFX;
|
||||
bool isExplosion = false;
|
||||
|
||||
if (isEnemy && damageInfo.HasValue && explosionFX != null)
|
||||
{
|
||||
if (hitFX == null) Debug.LogWarning("[GfxController] hitFX Prefab 未在 Inspector 中分配!");
|
||||
float ratio = damageInfo.Value.damage / damageInfo.Value.maxHealth;
|
||||
if (ratio >= explosionDamageThresholdRatio)
|
||||
{
|
||||
fxPrefab = explosionFX;
|
||||
isExplosion = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (target == null || fxPrefab == null)
|
||||
{
|
||||
if (fxPrefab == null) Debug.LogWarning($"[GfxController] {(isExplosion ? "ExplosionFX" : "HitFX")} Prefab 未在 Inspector 中分配!");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,7 +198,14 @@ public class GfxController : MonoBehaviour
|
||||
}
|
||||
else if (isEnemy)
|
||||
{
|
||||
fxScale = useEnemyRandomScale ? Random.Range(enemyMinFxScale, enemyMaxFxScale) : enemyFxScale;
|
||||
if (isExplosion && useExplosionSeparateScale)
|
||||
{
|
||||
fxScale = explosionFxScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
fxScale = useEnemyRandomScale ? Random.Range(enemyMinFxScale, enemyMaxFxScale) : enemyFxScale;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -194,7 +223,7 @@ public class GfxController : MonoBehaviour
|
||||
visualParent.transform.localScale = Vector3.one * fxScale;
|
||||
|
||||
// 作为子物体生成
|
||||
fx = Instantiate(hitFX, visualParent.transform);
|
||||
fx = Instantiate(fxPrefab, visualParent.transform);
|
||||
|
||||
// 确保特效在 UI 层级中显示在最前方
|
||||
fx.transform.SetAsLastSibling();
|
||||
@@ -215,7 +244,7 @@ public class GfxController : MonoBehaviour
|
||||
|
||||
rt.localRotation = Quaternion.identity;
|
||||
// 确保 UI 特效的原始大小正确,且 localScale 为 1(继承 parent 的缩放)
|
||||
rt.sizeDelta = hitFX.GetComponent<RectTransform>().sizeDelta;
|
||||
rt.sizeDelta = fxPrefab.GetComponent<RectTransform>().sizeDelta;
|
||||
rt.localScale = Vector3.one;
|
||||
}
|
||||
else
|
||||
@@ -235,7 +264,7 @@ public class GfxController : MonoBehaviour
|
||||
else
|
||||
{
|
||||
Vector3 spawnPos = customWorldPos.HasValue ? customWorldPos.Value : (target.transform.position + offsetVector);
|
||||
fx = Instantiate(hitFX, spawnPos, Quaternion.identity);
|
||||
fx = Instantiate(fxPrefab, spawnPos, Quaternion.identity);
|
||||
fx.transform.localScale = Vector3.one * fxScale;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Pool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class groundParticularController : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct MaterialConfig
|
||||
{
|
||||
public Material material;
|
||||
public bool isEnabled;
|
||||
[Tooltip("是否为该材质启用渐变颜色覆盖")]
|
||||
public bool useGradientOverride;
|
||||
[Tooltip("粒子生命周期内的颜色渐变 (仅在 useGradientOverride 开启时有效)")]
|
||||
public Gradient colorGradient;
|
||||
}
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("粒子的起点")]
|
||||
[SerializeField] private Transform startPoint;
|
||||
[Tooltip("粒子的终点")]
|
||||
[SerializeField] private Transform endPoint;
|
||||
[Tooltip("作为粒子的预制体")]
|
||||
[SerializeField] private GameObject particlePrefab;
|
||||
|
||||
[Header("Visuals")]
|
||||
[Tooltip("粒子可用的材质配置列表")]
|
||||
[SerializeField] private List<MaterialConfig> particleMaterials = new List<MaterialConfig>();
|
||||
[Tooltip("粒子最小缩放比例")]
|
||||
[SerializeField] private float minParticleScale = 0.8f;
|
||||
[Tooltip("粒子最大缩放比例")]
|
||||
[SerializeField] private float maxParticleScale = 1.2f;
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("性能门控:是否启用粒子生成")]
|
||||
public bool enableParticles = true;
|
||||
[Tooltip("粒子的物理层级 (Layer Name)")]
|
||||
[SerializeField] private string physicsLayerName = "particles";
|
||||
[Tooltip("粒子的渲染层级名称 (Sorting Layer)")]
|
||||
[SerializeField] private string sortingLayerName = "Default";
|
||||
[Tooltip("粒子的渲染层级编号 (Order in Layer)")]
|
||||
[SerializeField] private int sortingOrder = 0;
|
||||
[Tooltip("渲染队列偏移 (Render Queue Offset),用于解决 3D 物体与 UI 遮挡问题。数值越小越先渲染(被遮挡)")]
|
||||
[SerializeField] private int renderQueueOffset = 0;
|
||||
|
||||
[Header("Spawn Quantity")]
|
||||
[Tooltip("基础发射频率(每秒发射次数)")]
|
||||
[SerializeField] private float baseEmitRate = 10f;
|
||||
[Tooltip("每次发射生成的粒子数量 (Batch Size) - 增加此数值可显著增加粒子密度")]
|
||||
[SerializeField] private int emitBatchSize = 1;
|
||||
|
||||
[Header("Spawn Randomness")]
|
||||
[Tooltip("混乱度:随机时间偏移范围")]
|
||||
[SerializeField] private float chaosRandomDelay = 0.05f;
|
||||
[Tooltip("出生点 X 轴随机偏移范围")]
|
||||
[SerializeField] private float spawnOffsetX = 0.5f;
|
||||
[Tooltip("出生点 Z 轴 (深度) 随机偏移范围")]
|
||||
[SerializeField] private float spawnOffsetY = 0.5f;
|
||||
[Tooltip("速率系数,用于调节基础移动速度")]
|
||||
[SerializeField] private float speedMultiplier = 0.1f;
|
||||
|
||||
[Tooltip("起点缩放抖动强度 (基于音频电平)")]
|
||||
[SerializeField] private float shakeIntensity = 0.1f;
|
||||
|
||||
[Header("External References")]
|
||||
[Tooltip("粒子生成的父物体")]
|
||||
[SerializeField] private Transform particleParent;
|
||||
[Tooltip("主音频源,用于同步音乐状态")]
|
||||
public AudioSource musicSource;
|
||||
[Tooltip("赋值对应的 BeatmapManager 以获取当前歌曲的 BPM")]
|
||||
public BeatmapManager beatmapManager;
|
||||
|
||||
[Header("Audio Analysis")]
|
||||
public bool useAudioAnalysis = true;
|
||||
[Range(0, 1)] public float audioSensitivity = 0.5f;
|
||||
[Tooltip("音频对发射频率的影响强度")]
|
||||
[SerializeField] private float audioEmitBoost = 50f;
|
||||
|
||||
private float[] _audioSamples = new float[128];
|
||||
private float _currentAverageVolume = 0f;
|
||||
private float _emitTimer = 0f;
|
||||
|
||||
// 材质均衡分配逻辑
|
||||
private List<MaterialConfig> _cachedEnabledConfigs = new List<MaterialConfig>();
|
||||
private int _materialIndex = 0;
|
||||
|
||||
private IObjectPool<GameObject> _pool;
|
||||
private Vector3 _originalStartScale;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (startPoint != null) _originalStartScale = startPoint.localScale;
|
||||
|
||||
_pool = new ObjectPool<GameObject>(
|
||||
createFunc: () => {
|
||||
GameObject go = Instantiate(particlePrefab);
|
||||
go.SetActive(false);
|
||||
return go;
|
||||
},
|
||||
actionOnGet: (obj) => {
|
||||
obj.SetActive(true);
|
||||
// 解决对象池拉线问题:如果球体带拖尾,获取时必须清除旧路径
|
||||
var trail = obj.GetComponent<TrailRenderer>();
|
||||
if (trail != null) trail.Clear();
|
||||
},
|
||||
actionOnRelease: (obj) => obj.SetActive(false),
|
||||
actionOnDestroy: (obj) => Destroy(obj),
|
||||
defaultCapacity: 30
|
||||
);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 执行实时音频 analysis(获取音量强度)
|
||||
AnalyzeAudio();
|
||||
|
||||
// 持续发射逻辑
|
||||
UpdateContinuousEmit();
|
||||
|
||||
// 强制打印当前状态
|
||||
if (Time.frameCount % 60 == 0)
|
||||
{
|
||||
string audioStatus = musicSource != null ? (musicSource.isPlaying ? "Playing" : "Paused/Stopped") : "Null";
|
||||
Debug.Log($"[GroundParticular] Vol: {_currentAverageVolume:F3}, Status: {audioStatus}");
|
||||
}
|
||||
|
||||
// 起点缩放抖动
|
||||
if (startPoint != null)
|
||||
{
|
||||
float shakeEffect = _currentAverageVolume * shakeIntensity;
|
||||
startPoint.localScale = _originalStartScale * (1f + shakeEffect);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateContinuousEmit()
|
||||
{
|
||||
// 门控检查:如果玩家禁用了粒子,则不执行生成逻辑
|
||||
if (!enableParticles || musicSource == null || !musicSource.isPlaying) return;
|
||||
|
||||
// 计算当前瞬间的发射频率:基础频率 + 音频增益
|
||||
float currentRate = baseEmitRate + (_currentAverageVolume * audioEmitBoost);
|
||||
if (currentRate <= 0) return;
|
||||
|
||||
float interval = 1f / currentRate;
|
||||
_emitTimer += Time.deltaTime;
|
||||
|
||||
if (_emitTimer >= interval)
|
||||
{
|
||||
_emitTimer = 0;
|
||||
|
||||
// 批量生成
|
||||
for (int i = 0; i < emitBatchSize; i++)
|
||||
{
|
||||
// 引入混乱度:随机延迟发射
|
||||
if (chaosRandomDelay > 0)
|
||||
{
|
||||
Invoke(nameof(DelayedEmit), Random.Range(0f, chaosRandomDelay));
|
||||
}
|
||||
else
|
||||
{
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DelayedEmit()
|
||||
{
|
||||
Emit();
|
||||
}
|
||||
|
||||
private void AnalyzeAudio()
|
||||
{
|
||||
if (musicSource == null || !musicSource.isPlaying || !useAudioAnalysis)
|
||||
{
|
||||
_currentAverageVolume = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 GetOutputData 获取实时波形采样数据(电平)
|
||||
musicSource.GetOutputData(_audioSamples, 0);
|
||||
|
||||
float sum = 0;
|
||||
for (int i = 0; i < _audioSamples.Length; i++)
|
||||
{
|
||||
sum += Mathf.Abs(_audioSamples[i]);
|
||||
}
|
||||
|
||||
// 计算平均振幅并应用敏感度
|
||||
_currentAverageVolume = (sum / _audioSamples.Length) * audioSensitivity * 2.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射一个粒子
|
||||
/// </summary>
|
||||
public void Emit()
|
||||
{
|
||||
if (startPoint == null || endPoint == null || particlePrefab == null) return;
|
||||
|
||||
// 1. 获取粒子
|
||||
GameObject particle = _pool.Get();
|
||||
|
||||
// 2. 设置层级
|
||||
particle.transform.SetParent(particleParent != null ? particleParent : null);
|
||||
|
||||
// 3. 计算缩放
|
||||
float randomScale = Random.Range(minParticleScale, maxParticleScale);
|
||||
float audioBoost = useAudioAnalysis ? (1f + _currentAverageVolume) : 1f;
|
||||
particle.transform.localScale = particlePrefab.transform.localScale * randomScale * audioBoost;
|
||||
|
||||
// 4. 计算位置 (全表面随机 - XZ 平面)
|
||||
// 修改为 X 和 Z 轴随机,以覆盖地面 (Ground) 的水平和深度方向
|
||||
Vector3 surfaceRandomPos = new Vector3(Random.Range(-0.5f, 0.5f), 0f, Random.Range(-0.5f, 0.5f));
|
||||
Vector3 finalLocalPos = surfaceRandomPos + new Vector3(
|
||||
Random.Range(-spawnOffsetX, spawnOffsetX),
|
||||
0f,
|
||||
Random.Range(-spawnOffsetY, spawnOffsetY)
|
||||
);
|
||||
particle.transform.position = startPoint.TransformPoint(finalLocalPos);
|
||||
particle.transform.rotation = startPoint.rotation;
|
||||
|
||||
// 5. 材质均衡选取逻辑
|
||||
MaterialConfig? selectedConfig = GetBalancedMaterialConfig();
|
||||
|
||||
// 设置物理层级
|
||||
int layer = LayerMask.NameToLayer(physicsLayerName);
|
||||
if (layer != -1)
|
||||
{
|
||||
particle.layer = layer;
|
||||
}
|
||||
|
||||
var renderer = particle.GetComponent<Renderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
// 设置渲染层级
|
||||
renderer.sortingLayerName = sortingLayerName;
|
||||
renderer.sortingOrder = sortingOrder;
|
||||
|
||||
if (selectedConfig != null)
|
||||
{
|
||||
// 使用实例化材质以允许独立修改 RenderQueue
|
||||
renderer.material = selectedConfig.Value.material;
|
||||
if (renderQueueOffset != 0)
|
||||
{
|
||||
renderer.material.renderQueue = selectedConfig.Value.material.renderQueue + renderQueueOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 开始移动
|
||||
StartCoroutine(MoveRoutine(particle, finalLocalPos, selectedConfig));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轮询式均衡选取材质,确保每种材质数量大致相同
|
||||
/// </summary>
|
||||
private MaterialConfig? GetBalancedMaterialConfig()
|
||||
{
|
||||
if (particleMaterials == null || particleMaterials.Count == 0) return null;
|
||||
|
||||
// 过滤出所有启用的材质
|
||||
_cachedEnabledConfigs.Clear();
|
||||
foreach (var config in particleMaterials)
|
||||
{
|
||||
if (config.isEnabled && config.material != null)
|
||||
_cachedEnabledConfigs.Add(config);
|
||||
}
|
||||
|
||||
if (_cachedEnabledConfigs.Count == 0) return null;
|
||||
|
||||
// 轮询选取
|
||||
_materialIndex = (_materialIndex + 1) % _cachedEnabledConfigs.Count;
|
||||
return _cachedEnabledConfigs[_materialIndex];
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator MoveRoutine(GameObject particle, Vector3 initialLocalPos, MaterialConfig? config)
|
||||
{
|
||||
if (startPoint == null || endPoint == null) yield break;
|
||||
|
||||
// 【关键修复】在协程开始时,缓存起点的世界坐标
|
||||
Vector3 spawnWorldPos = startPoint.TransformPoint(initialLocalPos);
|
||||
|
||||
Vector3 currentStartPos = startPoint.position;
|
||||
Vector3 currentEndPos = endPoint.position;
|
||||
Vector3 moveDir = (currentEndPos - currentStartPos).normalized;
|
||||
float totalDist = Vector3.Distance(currentStartPos, currentEndPos);
|
||||
float traveledDist = 0f;
|
||||
|
||||
particle.transform.position = spawnWorldPos;
|
||||
|
||||
// 获取材质属性块用于动态修改颜色 (不产生材质实例 GC)
|
||||
MaterialPropertyBlock propBlock = new MaterialPropertyBlock();
|
||||
var renderer = particle.GetComponent<Renderer>();
|
||||
|
||||
while (particle != null && particle.activeInHierarchy)
|
||||
{
|
||||
// 如果游戏暂停,则等待直到取消暂停
|
||||
if (PauseManager.Instance != null && PauseManager.Instance.IsPaused)
|
||||
{
|
||||
yield return new WaitUntil(() => !PauseManager.Instance.IsPaused);
|
||||
}
|
||||
|
||||
float currentBPM = GetCurrentBPM();
|
||||
float noteSpawnerSpeedMultiplier = 1.0f;
|
||||
if (beatmapManager != null && beatmapManager.noteSpawner != null)
|
||||
{
|
||||
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.speedMultiplier;
|
||||
}
|
||||
|
||||
float effectiveBPM = Mathf.Max(currentBPM, 60f);
|
||||
float noteSpeed = (totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f;
|
||||
|
||||
float step = noteSpeed * speedMultiplier * Time.deltaTime;
|
||||
if (step <= 0) step = 0.01f;
|
||||
|
||||
traveledDist += step;
|
||||
particle.transform.position += moveDir * step;
|
||||
|
||||
// 【动态空间渐变色逻辑】
|
||||
if (config != null && config.Value.useGradientOverride && renderer != null)
|
||||
{
|
||||
// 获取渐变的首尾颜色
|
||||
Color headColor = config.Value.colorGradient.Evaluate(1f);
|
||||
Color tailColor = config.Value.colorGradient.Evaluate(0f);
|
||||
|
||||
renderer.GetPropertyBlock(propBlock);
|
||||
// 对应 Shader 中的属性
|
||||
propBlock.SetColor("_HeadColor", headColor);
|
||||
propBlock.SetColor("_TailColor", tailColor);
|
||||
propBlock.SetVector("_MoveDir", moveDir);
|
||||
renderer.SetPropertyBlock(propBlock);
|
||||
}
|
||||
|
||||
if (traveledDist >= totalDist)
|
||||
{
|
||||
if (particle.activeInHierarchy) _pool.Release(particle);
|
||||
yield break;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetCurrentBPM()
|
||||
{
|
||||
if (beatmapManager != null && beatmapManager.assignedSongData != null)
|
||||
{
|
||||
return beatmapManager.assignedSongData.bpm;
|
||||
}
|
||||
return 120f;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (startPoint == null) return;
|
||||
|
||||
// 绘制起点的发射区域(完全匹配 Cube 的视觉大小)
|
||||
Gizmos.matrix = startPoint.localToWorldMatrix;
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
|
||||
|
||||
if (endPoint != null)
|
||||
{
|
||||
Gizmos.matrix = Matrix4x4.identity; // 切回世界坐标画线
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawLine(startPoint.position, endPoint.position);
|
||||
|
||||
Gizmos.matrix = endPoint.localToWorldMatrix;
|
||||
Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee19a6040a62c2248b337879e955533a
|
||||
@@ -304,36 +304,7 @@ public class settlementController : MonoBehaviour
|
||||
{
|
||||
int pm = sm != null ? sm.allSum_pmScore : 0;
|
||||
int idol = sm != null ? sm.allSum_idolScore : 0;
|
||||
int total = pm + idol;
|
||||
|
||||
// Documentation text normalized.
|
||||
thisSong_so.UpdateDifficultyRecord(diff, pm, idol);
|
||||
|
||||
// Documentation text normalized.
|
||||
thisSong_so.UpdateChartScore(diff, total);
|
||||
|
||||
// Documentation text normalized.
|
||||
if (thisSong_so.chartFiles != null)
|
||||
{
|
||||
var entry = thisSong_so.chartFiles.Find(e => e != null && e.difficulty == diff);
|
||||
if (entry != null)
|
||||
{
|
||||
// Documentation text normalized.
|
||||
float newProgress = Mathf.Clamp01((float)total / (float)maxScore_sum);
|
||||
if (newProgress > entry.levelProgressForThisDifficulty)
|
||||
{
|
||||
entry.levelProgressForThisDifficulty = newProgress;
|
||||
}
|
||||
|
||||
if (thisSong_so.thisLevel_selectedDifficultyID == diff)
|
||||
{
|
||||
thisSong_so.current_levelProgress = Mathf.Max(thisSong_so.current_levelProgress, entry.levelProgressForThisDifficulty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 更新:保存到持久化存储(Build包必需) ---
|
||||
thisSong_so.SavePersistent();
|
||||
thisSong_so.ApplySettlementResult(diff, pm, idol, maxScore_sum);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,4 +797,3 @@ public class settlementController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user