taiko_web/Unity示例代码合集.cs

1159 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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