1159 lines
30 KiB
C#
1159 lines
30 KiB
C#
/*
|
||
* Unity Taiko 示例代码合集
|
||
* 可以直接复制使用的完整代码示例
|
||
*/
|
||
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEngine.SceneManagement;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
|
||
// ==================== 数据结构 ====================
|
||
|
||
/// <summary>
|
||
/// 音符类型枚举
|
||
/// </summary>
|
||
public enum NoteType
|
||
{
|
||
Don, // 红色咚
|
||
Ka, // 蓝色咔
|
||
DaiDon, // 大咚
|
||
DaiKa, // 大咔
|
||
Drumroll, // 连打
|
||
Balloon // 气球
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判定结果枚举
|
||
/// </summary>
|
||
public enum JudgeResult
|
||
{
|
||
Perfect, // 良
|
||
Good, // 可
|
||
Bad, // 不可
|
||
Miss // 未打中
|
||
}
|
||
|
||
/// <summary>
|
||
/// 歌曲数据
|
||
/// </summary>
|
||
[Serializable]
|
||
public class SongData
|
||
{
|
||
public string songId;
|
||
public string title;
|
||
public string subtitle;
|
||
public float bpm;
|
||
public string audioFile;
|
||
public float offset;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 音符数据
|
||
/// </summary>
|
||
[Serializable]
|
||
public class NoteData
|
||
{
|
||
public NoteType type;
|
||
public float time; // 出现时间(秒)
|
||
public float endTime; // 结束时间(连打和气球用)
|
||
public int hitCount; // 需要打击次数(气球用)
|
||
public bool isHit; // 是否已被打击
|
||
}
|
||
|
||
/// <summary>
|
||
/// 谱面数据
|
||
/// </summary>
|
||
[Serializable]
|
||
public class ChartData
|
||
{
|
||
public SongData song;
|
||
public List<NoteData> notes = new List<NoteData>();
|
||
}
|
||
|
||
// ==================== 游戏管理器 ====================
|
||
|
||
/// <summary>
|
||
/// 游戏主管理器(单例模式)
|
||
/// 使用方法:GameManager.Instance.方法名()
|
||
/// </summary>
|
||
public class GameManager : MonoBehaviour
|
||
{
|
||
// 单例实例
|
||
public static GameManager Instance { get; private set; }
|
||
|
||
[Header("游戏状态")]
|
||
public bool isPlaying = false;
|
||
public float gameTime = 0f;
|
||
|
||
[Header("当前歌曲")]
|
||
public SongData currentSong;
|
||
public ChartData currentChart;
|
||
|
||
void Awake()
|
||
{
|
||
// 单例模式实现
|
||
if (Instance == null)
|
||
{
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject); // 场景切换时不销毁
|
||
}
|
||
else
|
||
{
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (isPlaying)
|
||
{
|
||
// 更新游戏时间(由AudioManager提供)
|
||
if (AudioManager.Instance != null)
|
||
{
|
||
gameTime = AudioManager.Instance.GetMusicTime();
|
||
}
|
||
}
|
||
}
|
||
|
||
public void StartGame(SongData song)
|
||
{
|
||
currentSong = song;
|
||
SceneManager.LoadScene("Game");
|
||
}
|
||
|
||
public void PauseGame()
|
||
{
|
||
isPlaying = false;
|
||
Time.timeScale = 0f;
|
||
AudioManager.Instance?.PauseMusic();
|
||
}
|
||
|
||
public void ResumeGame()
|
||
{
|
||
isPlaying = true;
|
||
Time.timeScale = 1f;
|
||
AudioManager.Instance?.ResumeMusic();
|
||
}
|
||
|
||
public void EndGame()
|
||
{
|
||
isPlaying = false;
|
||
// 显示结果界面
|
||
UIManager.Instance?.ShowResultPanel();
|
||
}
|
||
}
|
||
|
||
// ==================== 音符管理器 ====================
|
||
|
||
/// <summary>
|
||
/// 音符管理器 - 负责生成和管理所有音符
|
||
/// 在Unity中:创建空GameObject,命名为"NoteManager",添加此脚本
|
||
/// </summary>
|
||
public class NoteManager : MonoBehaviour
|
||
{
|
||
[Header("音符预制体")]
|
||
public GameObject donNotePrefab;
|
||
public GameObject kaNotePrefab;
|
||
public GameObject daiDonNotePrefab;
|
||
public GameObject daiKaNotePrefab;
|
||
public GameObject drumrollPrefab;
|
||
public GameObject balloonPrefab;
|
||
|
||
[Header("音符配置")]
|
||
public float noteSpeed = 5f; // 音符移动速度
|
||
public float spawnDistance = 10f; // 音符生成位置(X坐标)
|
||
public float anticipationTime = 2.5f; // 提前生成时间(秒)
|
||
|
||
private ChartData currentChart;
|
||
private List<Note> activeNotes = new List<Note>();
|
||
private int nextNoteIndex = 0;
|
||
|
||
public void LoadChart(ChartData chart)
|
||
{
|
||
currentChart = chart;
|
||
nextNoteIndex = 0;
|
||
|
||
// 清除旧音符
|
||
foreach (Note note in activeNotes)
|
||
{
|
||
if (note != null)
|
||
Destroy(note.gameObject);
|
||
}
|
||
activeNotes.Clear();
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (currentChart == null || !GameManager.Instance.isPlaying)
|
||
return;
|
||
|
||
// 根据当前游戏时间生成音符
|
||
SpawnNotesAtTime(GameManager.Instance.gameTime);
|
||
|
||
// 清理已销毁的音符
|
||
activeNotes.RemoveAll(note => note == null);
|
||
}
|
||
|
||
void SpawnNotesAtTime(float currentTime)
|
||
{
|
||
// 检查是否有应该生成的音符
|
||
while (nextNoteIndex < currentChart.notes.Count)
|
||
{
|
||
NoteData noteData = currentChart.notes[nextNoteIndex];
|
||
|
||
// 如果音符时间 - 提前时间 <= 当前时间,则生成
|
||
if (noteData.time - anticipationTime <= currentTime)
|
||
{
|
||
SpawnNote(noteData);
|
||
nextNoteIndex++;
|
||
}
|
||
else
|
||
{
|
||
break; // 后面的音符还不到生成时间
|
||
}
|
||
}
|
||
}
|
||
|
||
void SpawnNote(NoteData noteData)
|
||
{
|
||
GameObject prefab = GetNotePrefab(noteData.type);
|
||
|
||
if (prefab == null)
|
||
{
|
||
Debug.LogWarning($"找不到音符类型 {noteData.type} 的预制体");
|
||
return;
|
||
}
|
||
|
||
// 实例化音符
|
||
GameObject noteObj = Instantiate(prefab);
|
||
noteObj.transform.position = new Vector3(spawnDistance, 0, 0);
|
||
|
||
// 初始化音符
|
||
Note note = noteObj.GetComponent<Note>();
|
||
if (note != null)
|
||
{
|
||
note.Initialize(noteData, noteSpeed);
|
||
activeNotes.Add(note);
|
||
}
|
||
}
|
||
|
||
GameObject GetNotePrefab(NoteType type)
|
||
{
|
||
switch (type)
|
||
{
|
||
case NoteType.Don:
|
||
return donNotePrefab;
|
||
case NoteType.Ka:
|
||
return kaNotePrefab;
|
||
case NoteType.DaiDon:
|
||
return daiDonNotePrefab;
|
||
case NoteType.DaiKa:
|
||
return daiKaNotePrefab;
|
||
case NoteType.Drumroll:
|
||
return drumrollPrefab;
|
||
case NoteType.Balloon:
|
||
return balloonPrefab;
|
||
default:
|
||
return donNotePrefab;
|
||
}
|
||
}
|
||
|
||
public List<Note> GetActiveNotes()
|
||
{
|
||
return activeNotes;
|
||
}
|
||
}
|
||
|
||
// ==================== 音符基类 ====================
|
||
|
||
/// <summary>
|
||
/// 音符基类 - 所有音符的基础行为
|
||
/// 使用方法:创建DonNote, KaNote等子类继承此类
|
||
/// </summary>
|
||
public class Note : MonoBehaviour
|
||
{
|
||
[Header("音符数据")]
|
||
public NoteData data;
|
||
public float speed = 5f;
|
||
public bool isHit = false;
|
||
|
||
[Header("判定窗口(单位:秒)")]
|
||
public float perfectWindow = 0.075f;
|
||
public float goodWindow = 0.125f;
|
||
public float badWindow = 0.2f;
|
||
|
||
protected SpriteRenderer spriteRenderer;
|
||
protected Transform judgeLineTransform;
|
||
|
||
public virtual void Initialize(NoteData noteData, float moveSpeed)
|
||
{
|
||
data = noteData;
|
||
speed = moveSpeed;
|
||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||
|
||
// 查找判定线
|
||
GameObject judgeLine = GameObject.FindGameObjectWithTag("JudgeLine");
|
||
if (judgeLine != null)
|
||
{
|
||
judgeLineTransform = judgeLine.transform;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("找不到带有'JudgeLine'标签的判定线对象");
|
||
}
|
||
}
|
||
|
||
protected virtual void Update()
|
||
{
|
||
if (!isHit)
|
||
{
|
||
Move();
|
||
CheckAutoMiss();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移动音符
|
||
/// </summary>
|
||
protected virtual void Move()
|
||
{
|
||
transform.position += Vector3.left * speed * Time.deltaTime;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否自动Miss
|
||
/// </summary>
|
||
protected virtual void CheckAutoMiss()
|
||
{
|
||
if (judgeLineTransform == null)
|
||
return;
|
||
|
||
float distance = transform.position.x - judgeLineTransform.position.x;
|
||
|
||
// 如果音符已经超过判定线很远,自动Miss
|
||
if (distance < -badWindow * speed)
|
||
{
|
||
OnMiss();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判定打击
|
||
/// </summary>
|
||
public virtual JudgeResult Judge()
|
||
{
|
||
if (judgeLineTransform == null)
|
||
return JudgeResult.Miss;
|
||
|
||
float distance = Mathf.Abs(transform.position.x - judgeLineTransform.position.x);
|
||
float timeDistance = distance / speed;
|
||
|
||
if (timeDistance < perfectWindow)
|
||
return JudgeResult.Perfect;
|
||
else if (timeDistance < goodWindow)
|
||
return JudgeResult.Good;
|
||
else if (timeDistance < badWindow)
|
||
return JudgeResult.Bad;
|
||
else
|
||
return JudgeResult.Miss;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 被打击时调用
|
||
/// </summary>
|
||
public virtual void OnHit(JudgeResult result)
|
||
{
|
||
if (isHit)
|
||
return;
|
||
|
||
isHit = true;
|
||
data.isHit = true;
|
||
|
||
// 播放音效
|
||
AudioManager.Instance?.PlayHitSound(data.type);
|
||
|
||
// 显示特效
|
||
EffectManager.Instance?.ShowJudgeEffect(transform.position, result);
|
||
|
||
// 销毁音符
|
||
Destroy(gameObject, 0.1f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Miss时调用
|
||
/// </summary>
|
||
protected virtual void OnMiss()
|
||
{
|
||
if (isHit)
|
||
return;
|
||
|
||
isHit = true;
|
||
data.isHit = true;
|
||
|
||
// 记录Miss
|
||
ScoreManager scoreManager = FindObjectOfType<ScoreManager>();
|
||
scoreManager?.AddJudgement(JudgeResult.Miss);
|
||
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
|
||
// 具体音符类示例
|
||
public class DonNote : Note
|
||
{
|
||
public override void Initialize(NoteData noteData, float moveSpeed)
|
||
{
|
||
base.Initialize(noteData, moveSpeed);
|
||
if (spriteRenderer != null)
|
||
{
|
||
spriteRenderer.color = new Color(1f, 0.3f, 0.3f); // 红色
|
||
}
|
||
}
|
||
}
|
||
|
||
public class KaNote : Note
|
||
{
|
||
public override void Initialize(NoteData noteData, float moveSpeed)
|
||
{
|
||
base.Initialize(noteData, moveSpeed);
|
||
if (spriteRenderer != null)
|
||
{
|
||
spriteRenderer.color = new Color(0.3f, 0.5f, 1f); // 蓝色
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 分数管理器 ====================
|
||
|
||
/// <summary>
|
||
/// 分数管理器 - 管理分数、连击、判定统计
|
||
/// </summary>
|
||
public class ScoreManager : MonoBehaviour
|
||
{
|
||
[Header("分数数据")]
|
||
public int totalScore = 0;
|
||
public int combo = 0;
|
||
public int maxCombo = 0;
|
||
|
||
[Header("判定统计")]
|
||
public int perfectCount = 0;
|
||
public int goodCount = 0;
|
||
public int badCount = 0;
|
||
public int missCount = 0;
|
||
|
||
[Header("分数配置")]
|
||
public int perfectPoints = 100;
|
||
public int goodPoints = 50;
|
||
public int badPoints = 10;
|
||
|
||
public void AddJudgement(JudgeResult result)
|
||
{
|
||
switch (result)
|
||
{
|
||
case JudgeResult.Perfect:
|
||
perfectCount++;
|
||
combo++;
|
||
totalScore += perfectPoints;
|
||
break;
|
||
|
||
case JudgeResult.Good:
|
||
goodCount++;
|
||
combo++;
|
||
totalScore += goodPoints;
|
||
break;
|
||
|
||
case JudgeResult.Bad:
|
||
badCount++;
|
||
combo = 0;
|
||
totalScore += badPoints;
|
||
break;
|
||
|
||
case JudgeResult.Miss:
|
||
missCount++;
|
||
combo = 0;
|
||
break;
|
||
}
|
||
|
||
// 更新最大连击
|
||
if (combo > maxCombo)
|
||
maxCombo = combo;
|
||
|
||
// 更新UI
|
||
UIManager.Instance?.UpdateScore(totalScore, combo);
|
||
|
||
Debug.Log($"判定: {result}, 分数: {totalScore}, 连击: {combo}");
|
||
}
|
||
|
||
public void ResetScore()
|
||
{
|
||
totalScore = 0;
|
||
combo = 0;
|
||
maxCombo = 0;
|
||
perfectCount = goodCount = badCount = missCount = 0;
|
||
}
|
||
|
||
public float GetAccuracy()
|
||
{
|
||
int total = perfectCount + goodCount + badCount + missCount;
|
||
if (total == 0)
|
||
return 100f;
|
||
|
||
return ((float)(perfectCount + goodCount) / total) * 100f;
|
||
}
|
||
}
|
||
|
||
// ==================== 输入管理器 ====================
|
||
|
||
/// <summary>
|
||
/// 输入管理器 - 处理玩家输入和判定
|
||
/// </summary>
|
||
public class InputManager : MonoBehaviour
|
||
{
|
||
[Header("按键设置")]
|
||
public KeyCode donLeft = KeyCode.F;
|
||
public KeyCode donRight = KeyCode.J;
|
||
public KeyCode kaLeft = KeyCode.D;
|
||
public KeyCode kaRight = KeyCode.K;
|
||
|
||
private NoteManager noteManager;
|
||
private ScoreManager scoreManager;
|
||
|
||
void Start()
|
||
{
|
||
noteManager = FindObjectOfType<NoteManager>();
|
||
scoreManager = FindObjectOfType<ScoreManager>();
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (!GameManager.Instance.isPlaying)
|
||
return;
|
||
|
||
// 检测咚(红色)输入
|
||
if (Input.GetKeyDown(donLeft) || Input.GetKeyDown(donRight))
|
||
{
|
||
ProcessHit(NoteType.Don);
|
||
}
|
||
|
||
// 检测咔(蓝色)输入
|
||
if (Input.GetKeyDown(kaLeft) || Input.GetKeyDown(kaRight))
|
||
{
|
||
ProcessHit(NoteType.Ka);
|
||
}
|
||
}
|
||
|
||
void ProcessHit(NoteType hitType)
|
||
{
|
||
// 查找最接近判定线的匹配音符
|
||
Note closestNote = FindClosestNote(hitType);
|
||
|
||
if (closestNote != null)
|
||
{
|
||
JudgeResult result = closestNote.Judge();
|
||
|
||
if (result != JudgeResult.Miss)
|
||
{
|
||
closestNote.OnHit(result);
|
||
scoreManager?.AddJudgement(result);
|
||
}
|
||
}
|
||
}
|
||
|
||
Note FindClosestNote(NoteType hitType)
|
||
{
|
||
if (noteManager == null)
|
||
return null;
|
||
|
||
List<Note> activeNotes = noteManager.GetActiveNotes();
|
||
Note closestNote = null;
|
||
float minDistance = float.MaxValue;
|
||
|
||
GameObject judgeLine = GameObject.FindGameObjectWithTag("JudgeLine");
|
||
if (judgeLine == null)
|
||
return null;
|
||
|
||
foreach (Note note in activeNotes)
|
||
{
|
||
if (note == null || note.isHit)
|
||
continue;
|
||
|
||
// 检查音符类型是否匹配
|
||
if (!IsNoteTypeMatch(note.data.type, hitType))
|
||
continue;
|
||
|
||
float distance = Mathf.Abs(note.transform.position.x - judgeLine.transform.position.x);
|
||
|
||
if (distance < minDistance && distance < note.badWindow * note.speed)
|
||
{
|
||
minDistance = distance;
|
||
closestNote = note;
|
||
}
|
||
}
|
||
|
||
return closestNote;
|
||
}
|
||
|
||
bool IsNoteTypeMatch(NoteType noteType, NoteType hitType)
|
||
{
|
||
if (hitType == NoteType.Don)
|
||
{
|
||
return noteType == NoteType.Don || noteType == NoteType.DaiDon;
|
||
}
|
||
else if (hitType == NoteType.Ka)
|
||
{
|
||
return noteType == NoteType.Ka || noteType == NoteType.DaiKa;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ==================== 音频管理器 ====================
|
||
|
||
/// <summary>
|
||
/// 音频管理器 - 管理音乐和音效播放
|
||
/// </summary>
|
||
public class AudioManager : MonoBehaviour
|
||
{
|
||
public static AudioManager Instance { get; private set; }
|
||
|
||
[Header("音频源")]
|
||
public AudioSource musicSource;
|
||
public AudioSource sfxSource;
|
||
|
||
[Header("音效")]
|
||
public AudioClip donHitSound;
|
||
public AudioClip kaHitSound;
|
||
public AudioClip drumrollSound;
|
||
|
||
void Awake()
|
||
{
|
||
if (Instance == null)
|
||
{
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
}
|
||
else
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
// 创建音频源
|
||
if (musicSource == null)
|
||
{
|
||
musicSource = gameObject.AddComponent<AudioSource>();
|
||
musicSource.loop = false;
|
||
musicSource.playOnAwake = false;
|
||
}
|
||
|
||
if (sfxSource == null)
|
||
{
|
||
sfxSource = gameObject.AddComponent<AudioSource>();
|
||
sfxSource.playOnAwake = false;
|
||
}
|
||
}
|
||
|
||
public void LoadMusic(string audioPath)
|
||
{
|
||
// 从Resources加载(audioPath应该是相对于Resources文件夹的路径,不含扩展名)
|
||
AudioClip clip = Resources.Load<AudioClip>(audioPath);
|
||
|
||
if (clip != null)
|
||
{
|
||
musicSource.clip = clip;
|
||
Debug.Log($"音乐加载成功: {audioPath}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"找不到音乐文件: {audioPath}");
|
||
}
|
||
}
|
||
|
||
public void PlayMusic()
|
||
{
|
||
if (musicSource.clip != null)
|
||
{
|
||
musicSource.Play();
|
||
}
|
||
}
|
||
|
||
public void PauseMusic()
|
||
{
|
||
musicSource.Pause();
|
||
}
|
||
|
||
public void ResumeMusic()
|
||
{
|
||
musicSource.UnPause();
|
||
}
|
||
|
||
public void StopMusic()
|
||
{
|
||
musicSource.Stop();
|
||
}
|
||
|
||
public float GetMusicTime()
|
||
{
|
||
return musicSource.time;
|
||
}
|
||
|
||
public void PlayHitSound(NoteType type)
|
||
{
|
||
AudioClip clip = null;
|
||
|
||
switch (type)
|
||
{
|
||
case NoteType.Don:
|
||
case NoteType.DaiDon:
|
||
clip = donHitSound;
|
||
break;
|
||
case NoteType.Ka:
|
||
case NoteType.DaiKa:
|
||
clip = kaHitSound;
|
||
break;
|
||
case NoteType.Drumroll:
|
||
clip = drumrollSound;
|
||
break;
|
||
}
|
||
|
||
if (clip != null)
|
||
{
|
||
sfxSource.PlayOneShot(clip);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== UI管理器 ====================
|
||
|
||
/// <summary>
|
||
/// UI管理器 - 管理所有UI显示
|
||
/// </summary>
|
||
public class UIManager : MonoBehaviour
|
||
{
|
||
public static UIManager Instance { get; private set; }
|
||
|
||
[Header("游戏UI")]
|
||
public Text scoreText;
|
||
public Text comboText;
|
||
public GameObject comboPanel;
|
||
|
||
[Header("判定显示")]
|
||
public Text judgeText;
|
||
public float judgeFadeTime = 0.5f;
|
||
|
||
[Header("结果面板")]
|
||
public GameObject resultPanel;
|
||
public Text resultScoreText;
|
||
public Text resultPerfectText;
|
||
public Text resultGoodText;
|
||
public Text resultBadText;
|
||
public Text resultMissText;
|
||
public Text resultAccuracyText;
|
||
|
||
void Awake()
|
||
{
|
||
if (Instance == null)
|
||
{
|
||
Instance = this;
|
||
}
|
||
else
|
||
{
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
|
||
void Start()
|
||
{
|
||
if (resultPanel != null)
|
||
resultPanel.SetActive(false);
|
||
}
|
||
|
||
public void UpdateScore(int score, int combo)
|
||
{
|
||
if (scoreText != null)
|
||
{
|
||
scoreText.text = score.ToString("D8");
|
||
}
|
||
|
||
if (combo > 0)
|
||
{
|
||
if (comboText != null)
|
||
{
|
||
comboText.text = combo.ToString();
|
||
}
|
||
|
||
if (comboPanel != null)
|
||
{
|
||
comboPanel.SetActive(true);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (comboPanel != null)
|
||
{
|
||
comboPanel.SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
public void ShowJudgeResult(JudgeResult result)
|
||
{
|
||
if (judgeText == null)
|
||
return;
|
||
|
||
switch (result)
|
||
{
|
||
case JudgeResult.Perfect:
|
||
judgeText.text = "良";
|
||
judgeText.color = Color.yellow;
|
||
break;
|
||
case JudgeResult.Good:
|
||
judgeText.text = "可";
|
||
judgeText.color = Color.white;
|
||
break;
|
||
case JudgeResult.Bad:
|
||
judgeText.text = "不可";
|
||
judgeText.color = Color.gray;
|
||
break;
|
||
}
|
||
|
||
StopAllCoroutines();
|
||
StartCoroutine(FadeOutJudgeText());
|
||
}
|
||
|
||
IEnumerator FadeOutJudgeText()
|
||
{
|
||
judgeText.gameObject.SetActive(true);
|
||
yield return new WaitForSeconds(judgeFadeTime);
|
||
|
||
float fadeTime = 0.3f;
|
||
float elapsed = 0f;
|
||
Color originalColor = judgeText.color;
|
||
|
||
while (elapsed < fadeTime)
|
||
{
|
||
elapsed += Time.deltaTime;
|
||
float alpha = 1f - (elapsed / fadeTime);
|
||
judgeText.color = new Color(originalColor.r, originalColor.g, originalColor.b, alpha);
|
||
yield return null;
|
||
}
|
||
|
||
judgeText.gameObject.SetActive(false);
|
||
judgeText.color = originalColor;
|
||
}
|
||
|
||
public void ShowResultPanel()
|
||
{
|
||
if (resultPanel == null)
|
||
return;
|
||
|
||
ScoreManager scoreManager = FindObjectOfType<ScoreManager>();
|
||
if (scoreManager == null)
|
||
return;
|
||
|
||
resultPanel.SetActive(true);
|
||
|
||
if (resultScoreText != null)
|
||
resultScoreText.text = scoreManager.totalScore.ToString("D8");
|
||
|
||
if (resultPerfectText != null)
|
||
resultPerfectText.text = "良: " + scoreManager.perfectCount;
|
||
|
||
if (resultGoodText != null)
|
||
resultGoodText.text = "可: " + scoreManager.goodCount;
|
||
|
||
if (resultBadText != null)
|
||
resultBadText.text = "不可: " + scoreManager.badCount;
|
||
|
||
if (resultMissText != null)
|
||
resultMissText.text = "Miss: " + scoreManager.missCount;
|
||
|
||
if (resultAccuracyText != null)
|
||
resultAccuracyText.text = "准确率: " + scoreManager.GetAccuracy().ToString("F1") + "%";
|
||
}
|
||
}
|
||
|
||
// ==================== 特效管理器 ====================
|
||
|
||
/// <summary>
|
||
/// 特效管理器 - 管理打击特效
|
||
/// </summary>
|
||
public class EffectManager : MonoBehaviour
|
||
{
|
||
public static EffectManager Instance { get; private set; }
|
||
|
||
[Header("特效预制体")]
|
||
public GameObject perfectEffectPrefab;
|
||
public GameObject goodEffectPrefab;
|
||
public GameObject badEffectPrefab;
|
||
|
||
[Header("特效配置")]
|
||
public float effectLifetime = 1f;
|
||
|
||
void Awake()
|
||
{
|
||
if (Instance == null)
|
||
{
|
||
Instance = this;
|
||
}
|
||
else
|
||
{
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
|
||
public void ShowJudgeEffect(Vector3 position, JudgeResult result)
|
||
{
|
||
GameObject prefab = null;
|
||
|
||
switch (result)
|
||
{
|
||
case JudgeResult.Perfect:
|
||
prefab = perfectEffectPrefab;
|
||
break;
|
||
case JudgeResult.Good:
|
||
prefab = goodEffectPrefab;
|
||
break;
|
||
case JudgeResult.Bad:
|
||
prefab = badEffectPrefab;
|
||
break;
|
||
}
|
||
|
||
if (prefab != null)
|
||
{
|
||
GameObject effect = Instantiate(prefab, position, Quaternion.identity);
|
||
Destroy(effect, effectLifetime);
|
||
}
|
||
|
||
// 显示判定文字
|
||
UIManager.Instance?.ShowJudgeResult(result);
|
||
}
|
||
}
|
||
|
||
// ==================== TJA解析器 ====================
|
||
|
||
/// <summary>
|
||
/// TJA谱面解析器
|
||
/// 使用方法:
|
||
/// TJAParser parser = new TJAParser();
|
||
/// ChartData chart = parser.Parse("路径/到/main.tja");
|
||
/// </summary>
|
||
public class TJAParser
|
||
{
|
||
public ChartData Parse(string filePath)
|
||
{
|
||
if (!File.Exists(filePath))
|
||
{
|
||
Debug.LogError($"找不到谱面文件: {filePath}");
|
||
return null;
|
||
}
|
||
|
||
ChartData chart = new ChartData();
|
||
chart.song = new SongData();
|
||
chart.notes = new List<NoteData>();
|
||
|
||
string[] lines = File.ReadAllLines(filePath);
|
||
|
||
bool inNoteData = false;
|
||
float currentTime = 0f;
|
||
float bpm = 120f;
|
||
|
||
foreach (string line in lines)
|
||
{
|
||
string trimmed = line.Trim();
|
||
|
||
// 跳过空行和注释
|
||
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("//"))
|
||
continue;
|
||
|
||
// 解析元数据
|
||
if (trimmed.Contains(":") && !inNoteData)
|
||
{
|
||
ParseMetadata(trimmed, chart.song, ref bpm);
|
||
}
|
||
// 开始音符数据
|
||
else if (trimmed == "#START")
|
||
{
|
||
inNoteData = true;
|
||
currentTime = -chart.song.offset; // 应用offset
|
||
}
|
||
// 结束音符数据
|
||
else if (trimmed == "#END")
|
||
{
|
||
break;
|
||
}
|
||
// 解析音符数据
|
||
else if (inNoteData)
|
||
{
|
||
if (trimmed.StartsWith("#"))
|
||
{
|
||
ProcessCommand(trimmed, ref bpm);
|
||
}
|
||
else if (trimmed.EndsWith(","))
|
||
{
|
||
ParseMeasure(trimmed, chart.notes, ref currentTime, bpm);
|
||
}
|
||
}
|
||
}
|
||
|
||
Debug.Log($"解析完成: {chart.song.title}, BPM: {chart.song.bpm}, 音符数: {chart.notes.Count}");
|
||
return chart;
|
||
}
|
||
|
||
void ParseMetadata(string line, SongData song, ref float bpm)
|
||
{
|
||
string[] parts = line.Split(new char[] { ':' }, 2);
|
||
if (parts.Length != 2)
|
||
return;
|
||
|
||
string key = parts[0].Trim();
|
||
string value = parts[1].Trim();
|
||
|
||
switch (key)
|
||
{
|
||
case "TITLE":
|
||
song.title = value;
|
||
break;
|
||
case "SUBTITLE":
|
||
song.subtitle = value;
|
||
break;
|
||
case "BPM":
|
||
if (float.TryParse(value, out float parsedBpm))
|
||
{
|
||
bpm = parsedBpm;
|
||
song.bpm = bpm;
|
||
}
|
||
break;
|
||
case "WAVE":
|
||
song.audioFile = value;
|
||
break;
|
||
case "OFFSET":
|
||
if (float.TryParse(value, out float offset))
|
||
{
|
||
song.offset = offset;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
void ProcessCommand(string command, ref float bpm)
|
||
{
|
||
if (command.StartsWith("#BPMCHANGE"))
|
||
{
|
||
string value = command.Replace("#BPMCHANGE", "").Trim();
|
||
if (float.TryParse(value, out float newBpm))
|
||
{
|
||
bpm = newBpm;
|
||
}
|
||
}
|
||
}
|
||
|
||
void ParseMeasure(string measure, List<NoteData> notes, ref float currentTime, float bpm)
|
||
{
|
||
measure = measure.TrimEnd(',');
|
||
|
||
float beatDuration = 60f / bpm;
|
||
float noteDuration = (beatDuration * 4f) / measure.Length;
|
||
|
||
for (int i = 0; i < measure.Length; i++)
|
||
{
|
||
char noteChar = measure[i];
|
||
|
||
if (noteChar != '0')
|
||
{
|
||
NoteData note = new NoteData();
|
||
note.time = currentTime + (i * noteDuration);
|
||
|
||
switch (noteChar)
|
||
{
|
||
case '1':
|
||
note.type = NoteType.Don;
|
||
break;
|
||
case '2':
|
||
note.type = NoteType.Ka;
|
||
break;
|
||
case '3':
|
||
note.type = NoteType.DaiDon;
|
||
break;
|
||
case '4':
|
||
note.type = NoteType.DaiKa;
|
||
break;
|
||
case '5':
|
||
case '6':
|
||
note.type = NoteType.Drumroll;
|
||
break;
|
||
case '7':
|
||
note.type = NoteType.Balloon;
|
||
break;
|
||
}
|
||
|
||
notes.Add(note);
|
||
}
|
||
}
|
||
|
||
currentTime += beatDuration * 4f;
|
||
}
|
||
}
|
||
|
||
// ==================== 谱面加载器 ====================
|
||
|
||
/// <summary>
|
||
/// 谱面加载器组件
|
||
/// 使用方法:附加到GameController上,调用LoadChart()
|
||
/// </summary>
|
||
public class ChartLoader : MonoBehaviour
|
||
{
|
||
public void LoadChart(string songId)
|
||
{
|
||
// 从StreamingAssets加载
|
||
string path = Path.Combine(Application.streamingAssetsPath, "Charts", songId, "main.tja");
|
||
|
||
if (!File.Exists(path))
|
||
{
|
||
Debug.LogError($"找不到谱面文件: {path}");
|
||
return;
|
||
}
|
||
|
||
TJAParser parser = new TJAParser();
|
||
ChartData chart = parser.Parse(path);
|
||
|
||
if (chart == null)
|
||
{
|
||
Debug.LogError("谱面解析失败");
|
||
return;
|
||
}
|
||
|
||
// 保存到GameManager
|
||
GameManager.Instance.currentChart = chart;
|
||
GameManager.Instance.currentSong = chart.song;
|
||
|
||
// 加载音乐(假设音乐文件在Resources/Music/下)
|
||
string audioPath = "Music/" + Path.GetFileNameWithoutExtension(chart.song.audioFile);
|
||
AudioManager.Instance?.LoadMusic(audioPath);
|
||
|
||
// 传递给NoteManager
|
||
NoteManager noteManager = FindObjectOfType<NoteManager>();
|
||
noteManager?.LoadChart(chart);
|
||
|
||
Debug.Log($"谱面加载完成: {chart.song.title}");
|
||
}
|
||
}
|
||
|
||
/*
|
||
* ==================== 使用说明 ====================
|
||
*
|
||
* 1. 创建空GameObject,添加对应的Manager脚本
|
||
* 2. 设置必要的公共变量(如prefab、UI引用等)
|
||
* 3. 确保GameObject有正确的Tag(如判定线需要"JudgeLine"标签)
|
||
* 4. 音频文件放在Assets/Resources/Music/下
|
||
* 5. 谱面文件放在Assets/StreamingAssets/Charts/下
|
||
* 6. 按照指南创建场景和UI
|
||
*
|
||
* 祝你成功!
|
||
*/
|