在之前的项目中有遇到过负责开发节奏游戏的需求,查阅网上相关资料后了解了关于音乐游戏的制作核心(节点谱),但是网上目前基于Unity3D的音乐游戏还较匮乏,仅有的DEMO甚至还在使用PartieEmitter所以处于无法运行的状态,于是参考DEMO(Guitar Hero)的编辑器部分配合配合反射获取的API实现了一个编辑器制谱工具。
注:该篇由于涉及到多个层面(反射/编辑器GUI交互/ScriptableObject)导致代码量过多 所以将项目DEMO传到GITHUB上,地址在最下文。
个人需求
节奏游戏的制谱工具,能够在编辑器环境下设定BPM,Offset,节点并对当前音乐进行播放停止等操作.
实现效果
设定获取BPM与Offset后,对节拍点击即可给选定节拍绑定制定类型的节点.
思路原理
节点数存储:使用ScriptableObject作为节点数据存储容器.
编辑器播放音乐:反射Internall类方法供编辑器GUI调用.
节点数据调整:使用编辑器GUI交互对ScriptableObject内数据进行调整.
代码实现
节点存储类
思路:使用ScriptableObject储存节点,与目标音乐信息
using System.Collections.Generic;
using UnityEngine;
public enum enum_BeatType
{
Invalid = -1,
Single = 1,
Double = 2,
Triple = 3,
}
[System.Serializable]
public class Node //节点
{
public Node(int beatPos, bool isLeft,enum_BeatType type)
{
i_BeatPos = beatPos;
b_IsLeft = isLeft;
e_Type = type;
}
public int i_BeatPos;
public bool b_IsLeft;
public enum_BeatType e_Type;
}
[CreateAssetMenu(fileName = "Nodes_", menuName = "BeatsNodes")]
public class BeatNodes : ScriptableObject
{
[SerializeField]
List<Node> l_Nodes = new List<Node>();
public int I_BeatPerMinute;
public float F_BeatStartOffset;
public AudioClip AC_ClipToPlay;
Node tempNode;
#region Interact APIs
public void Clear()
{
l_Nodes.Clear();
}
public int GetPerfectScore()
{
int perfect=0;
for (int i = 0; i < l_Nodes.Count; i++)
{
switch (l_Nodes[i].e_Type)
{
case enum_BeatType.Single:
perfect+=1;
break;
case enum_BeatType.Double:
perfect+=2;
break;
case enum_BeatType.Triple:
perfect += 3;
break;
}
}
return perfect;
}
public List<Node> GetNodes()
{
return l_Nodes;
}
public void ForceSort()
{
l_Nodes.Sort((left,right)=> {
return left.i_BeatPos >= right.i_BeatPos ? 1 : -1; });
}
public bool ContainsNode(int beatPos,bool isLeft)
{
tempNode = l_Nodes.Find(p => p.i_BeatPos == beatPos);
return tempNode != null&& tempNode.b_IsLeft==isLeft;
}
public void SetNode(int beatPos,bool isLeft,enum_BeatType type)
{
tempNode = GetNodeByPos(beatPos);
if (tempNode != null)
{
int index = l_Nodes.IndexOf(tempNode);
Node n = new Node(beatPos, isLeft,type);
l_Nodes[index] = n;
}
else
{
for (int i = 0; i < l_Nodes.Count; i++)
{
if (l_Nodes[i].i_BeatPos>beatPos)
{
l_Nodes.Insert(i,new Node(beatPos,isLeft,type));
return;
}
}
l_Nodes.Add(new Node(beatPos, isLeft,type));
}
}
public void AdjustNode(int beatPos, enum_BeatType type)
{
tempNode = GetNodeByPos(beatPos);
if (tempNode != null)
{
int index = l_Nodes.IndexOf(tempNode);
Node n = new Node(beatPos, tempNode.b_IsLeft, type);
l_Nodes[index] = n;
}
else
{
Debug.LogError("Can't Adjust A Unexisted Node!Pos:"+beatPos) ;
}
}
public void RemoveNode(int beatPos)
{
tempNode = GetNodeByPos(beatPos);
if (tempNode == null)
{
Debug.LogWarning(beatPos.ToString() + " Node Not Found Howdf U Remove A UnAddNode In Editor?");
return;
}
l_Nodes.Remove(tempNode);
}
public Node GetNodeByPos(int beatPos)
{
return l_Nodes.Find(p => p.i_BeatPos == beatPos);
}
public Node GetNodeByIndex(int index)
{
return l_Nodes[index];
}
public int GetNodeIndex(int beatPos)
{
return l_Nodes.FindIndex(p=>p.i_BeatPos==beatPos);
}
public Dictionary<float,int> GetTotalBeatsCenterWithOffset(float f_beatEach)
{
Dictionary<float, int> dic = new Dictionary<float, int>();
for (int i = 0; i < l_Nodes.Count; i++)
{
switch (l_Nodes[i].e_Type)
{
case enum_BeatType.Single:
{
dic.Add(F_BeatStartOffset + l_Nodes[i].i_BeatPos * f_beatEach,i);
}
break;
case enum_BeatType.Double:
{
dic.Add(F_BeatStartOffset + l_Nodes[i].i_BeatPos * f_beatEach, i);
dic.Add(F_BeatStartOffset + l_Nodes[i].i_BeatPos * f_beatEach + f_beatEach * .5f, i);
}
break;
case enum_BeatType.Triple:
{
dic.Add(F_BeatStartOffset + l_Nodes[i].i_BeatPos * f_beatEach, i);
dic.Add(F_BeatStartOffset + l_Nodes[i].i_BeatPos * f_beatEach + f_beatEach / 3,i);
dic.Add(F_BeatStartOffset + l_Nodes[i].i_BeatPos * f_beatEach + f_beatEach * 2 / 3,i);
}
break;
}
}
return dic;
}
public List<float> BeatsCenterWithOffset(int beatPos, enum_BeatType type,float f_beatEach)
{
List<float> beatMids = new List<float>();
switch (type)
{
case enum_Beat