/*
* 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
*
* 祝你成功!
*/