/* * Unity Taiko 示例代码合集 * 可以直接复制使用的完整代码示例 */ using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; using System; using System.Collections; using System.Collections.Generic; using System.IO; // ==================== 数据结构 ==================== /// /// 音符类型枚举 /// public enum NoteType { Don, // 红色咚 Ka, // 蓝色咔 DaiDon, // 大咚 DaiKa, // 大咔 Drumroll, // 连打 Balloon // 气球 } /// /// 判定结果枚举 /// public enum JudgeResult { Perfect, // 良 Good, // 可 Bad, // 不可 Miss // 未打中 } /// /// 歌曲数据 /// [Serializable] public class SongData { public string songId; public string title; public string subtitle; public float bpm; public string audioFile; public float offset; } /// /// 音符数据 /// [Serializable] public class NoteData { public NoteType type; public float time; // 出现时间(秒) public float endTime; // 结束时间(连打和气球用) public int hitCount; // 需要打击次数(气球用) public bool isHit; // 是否已被打击 } /// /// 谱面数据 /// [Serializable] public class ChartData { public SongData song; public List notes = new List(); } // ==================== 游戏管理器 ==================== /// /// 游戏主管理器(单例模式) /// 使用方法:GameManager.Instance.方法名() /// 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(); } } // ==================== 音符管理器 ==================== /// /// 音符管理器 - 负责生成和管理所有音符 /// 在Unity中:创建空GameObject,命名为"NoteManager",添加此脚本 /// 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 activeNotes = new List(); 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(); 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 GetActiveNotes() { return activeNotes; } } // ==================== 音符基类 ==================== /// /// 音符基类 - 所有音符的基础行为 /// 使用方法:创建DonNote, KaNote等子类继承此类 /// 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(); // 查找判定线 GameObject judgeLine = GameObject.FindGameObjectWithTag("JudgeLine"); if (judgeLine != null) { judgeLineTransform = judgeLine.transform; } else { Debug.LogWarning("找不到带有'JudgeLine'标签的判定线对象"); } } protected virtual void Update() { if (!isHit) { Move(); CheckAutoMiss(); } } /// /// 移动音符 /// protected virtual void Move() { transform.position += Vector3.left * speed * Time.deltaTime; } /// /// 检查是否自动Miss /// protected virtual void CheckAutoMiss() { if (judgeLineTransform == null) return; float distance = transform.position.x - judgeLineTransform.position.x; // 如果音符已经超过判定线很远,自动Miss if (distance < -badWindow * speed) { OnMiss(); } } /// /// 判定打击 /// 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; } /// /// 被打击时调用 /// 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); } /// /// Miss时调用 /// protected virtual void OnMiss() { if (isHit) return; isHit = true; data.isHit = true; // 记录Miss ScoreManager scoreManager = FindObjectOfType(); 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); // 蓝色 } } } // ==================== 分数管理器 ==================== /// /// 分数管理器 - 管理分数、连击、判定统计 /// 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; } } // ==================== 输入管理器 ==================== /// /// 输入管理器 - 处理玩家输入和判定 /// 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(); scoreManager = FindObjectOfType(); } 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 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; } } // ==================== 音频管理器 ==================== /// /// 音频管理器 - 管理音乐和音效播放 /// 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(); musicSource.loop = false; musicSource.playOnAwake = false; } if (sfxSource == null) { sfxSource = gameObject.AddComponent(); sfxSource.playOnAwake = false; } } public void LoadMusic(string audioPath) { // 从Resources加载(audioPath应该是相对于Resources文件夹的路径,不含扩展名) AudioClip clip = Resources.Load(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管理器 ==================== /// /// UI管理器 - 管理所有UI显示 /// 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(); 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") + "%"; } } // ==================== 特效管理器 ==================== /// /// 特效管理器 - 管理打击特效 /// 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解析器 ==================== /// /// TJA谱面解析器 /// 使用方法: /// TJAParser parser = new TJAParser(); /// ChartData chart = parser.Parse("路径/到/main.tja"); /// 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(); 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 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; } } // ==================== 谱面加载器 ==================== /// /// 谱面加载器组件 /// 使用方法:附加到GameController上,调用LoadChart() /// 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?.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 * * 祝你成功! */