Custom Notes
Creating custom notes is very easy. Simply inherit from the Note class and override the functions you are interested in.
The code below shows an example of a Tap Note. The comments on the functions explain in detail when they are called and why.
/// <summary>
/// The Tap Note detects a single press input.
/// </summary>
public class TapNote : Note
{
/// <summary>
/// The note is initialized when it is added to the top of a track.
/// </summary>
/// <param name="rhythmClipData">The rhythm clip data.</param>
public override void Initialize(RhythmClipData rhythmClipData)
{
base.Initialize(rhythmClipData);
}
/// <summary>
/// Reset when the note is returned to the pool.
/// </summary>
public override void Reset()
{
base.Reset();
}
/// <summary>
/// The note needs to be activated as it is within range of being triggered.
/// This usually happens when the clip starts.
/// </summary>
protected override void ActivateNote()
{
base.ActivateNote();
}
/// <summary>
/// The note needs to be deactivated when it is out of range from being triggered.
/// This usually happens when the clip ends.
/// </summary>
protected override void DeactivateNote()
{
base.DeactivateNote();
//Only send the trigger miss event during play mode.
if(Application.isPlaying == false){return;}
if (m_IsTriggered == false) {
InvokeNoteTriggerEventMiss();
}
}
/// <summary>
/// An input was triggered on this note.
/// The input event data has the information about what type of input was triggered.
/// </summary>
/// <param name="inputEventData">The input event data.</param>
public override void OnTriggerInput(InputEventData inputEventData)
{
//Since this is a tap note, only deal with tap inputs.
if (!inputEventData.Tap) { return; }
//The game object can be set to active false. It is returned to the pool automatically when reset.
gameObject.SetActive(false);
m_IsTriggered = true;
//You may compute the perfect time anyway you want.
//In this case the perfect time is half of the clip.
var perfectTime = m_RhythmClipData.RealDuration / 2f;
var timeDifference = TimeFromActivate - perfectTime;
var timeDifferencePercentage = Mathf.Abs((float)(100f*timeDifference)) / perfectTime;
//Send a trigger event such that the score system can listen to it.
InvokeNoteTriggerEvent(inputEventData, timeDifference, (float) timeDifferencePercentage);
RhythmClipData.TrackObject.RemoveActiveNote(this);
}
/// <summary>
/// Hybrid Update is updated both in play mode, by update or timeline, and edit mode by the timeline.
/// </summary>
/// <param name="timeFromStart">The time from reaching the start of the clip.</param>
/// <param name="timeFromEnd">The time from reaching the end of the clip.</param>
protected override void HybridUpdate(double timeFromStart, double timeFromEnd)
{
//Compute the perfect timing.
var perfectTime = m_RhythmClipData.RealDuration / 2f;
var deltaT = (float)(timeFromStart - perfectTime);
//Compute the position of the note using the delta T from the perfect timing.
//Here we use the direction of the track given at delta T.
//You can easily curve all your notes to any trajectory, not just straight lines, by customizing the TrackObjects.
//Here the target position is found using the track object end position.
var direction = RhythmClipData.TrackObject.GetNoteDirection(deltaT);
var distance = deltaT * m_RhythmClipData.RhythmDirector.NoteSpeed;
var targetPosition = m_RhythmClipData.TrackObject.EndPoint.position;
//Using those parameters we can easily compute the new position of the note at any time.
var newPosition = targetPosition + (direction * distance);
transform.position = newPosition;
}
}
The “RhythmClipData” property has a lot of very useful information about the clip that is bound to the note. It is extremely useful if you plan to create your own notes.
You may easily customize Note to function exactly as you wish by overriding those functions.
Here are some ideas of custom notes which could be created by you
- Notes which rotate while going down the track
- Notes which gives you extra points when pressed
- Note that combines hold with swipe
These are just examples, with a bit of creativity any type of notes are possible.
Apart from customizing notes directly you may also customize the Track Object code to tell the notes trajectory at any given time.
Custom data per clip
In some cases you may want to create a single note prefab and note definition, but have each note clip in the timeline have it’s own data.
The “CounterNote” is a simple example of that. using the RythmClipParameter to set how many times it needs to be pressed.
In some cases though you may need additional data types not available by default in the RhythmClipParameters. That’s when you’ll want to use a RhythmClipExtraNoteData. You can find a working example of that in the OSU demo, where we made a OsuSplineNoteRhythmClipExtraData that contains the spline Data for that clip.
To make your own you’ll need a few things
- A custom note
- A custom RhythmClipExtraNoteData
- In the custom Note script override RhythmClipExtraDataType property to return your custom RhythmClipExtraNoteData
/// <summary>
/// This function returns the RhythmClipExtraNoteData Type to assign to the RhythmClip when added to the timeline.
/// </summary>
public override Type RhythmClipExtraNoteDataType => typeof(YourCustomNoteRhythmClipExtraData);
- In The Intialize or HyrbidUpdate function use your custom RhythmClipExtraNoteData to make your note unique for that clip
if (m_RhythmClipData.ClipParameters.RhythmClipExtraNoteData is YourCustomNoteRhythmClipExtraData extraNoteData) {
//use your extraNoteData!
}
- Optionally you may want to inherit INoteRhythmClipOnSceneGUIChange or INoteOnSceneGUIChange and define a RhythmClipOnSceneGUIChange or OnSceneGUIChange function (inside UNITY_EDITOR compilation condition) to sync your data with the note in the editor preview.
We use RhythmClipOnSceneGUIChange and OnSceneGUIChange in the OSU notes for example, to move the notes with a handle in the scene view and sync the sprite shapes data from the scene inside the clip. The following code is slightly simplified but shows the basics:
// Make sure to inherit the INoteRhythmClipOnSceneGUIChange and INoteOnSceneGUIChange to receive the callbacks
public class YourCustomNote : Note, INoteRhythmClipOnSceneGUIChange, INoteOnSceneGUIChange
{
... Your note code
#if UNITY_EDITOR
public virtual bool RhythmClipOnSceneGUIChange(UnityEditor.SceneView sceneView, RhythmClip mainSelectedClip,
List<RhythmClip> selectedClips)
{
var rhythmClipData = mainSelectedClip.RhythmClipData;
if (rhythmClipData.IsValid == false) { return false;}
var trackObject = rhythmClipData.TrackObject;
if (trackObject == null) { return false; }
UnityEditor.EditorGUI.BeginChangeCheck();
//Example of showing editor handle to edit the Vector2 parameter
var clipOffset = mainSelectedClip.ClipParameters.Vector2Parameter;
var noteOriginalPosition = trackObject.EndPoint.position + new Vector3(clipOffset.x,clipOffset.y,0);
Vector3 newTargetPosition = UnityEditor.Handles.PositionHandle(noteOriginalPosition, Quaternion.identity);
if (UnityEditor.EditorGUI.EndChangeCheck())
{
//Record the change on the clip and in undo if there was one.
var deltaPos = newTargetPosition - noteOriginalPosition;
if (deltaPos == Vector3.zero)
{
return false;
}
foreach (var otherRhythmClip in selectedClips)
{
UnityEditor.Undo.RecordObject(otherRhythmClip, "Change Target Position");
otherRhythmClip.ClipParameters.Vector2Parameter += new Vector2(deltaPos.x, deltaPos.y);
}
return false;
}
return false;
}
public virtual bool OnSceneGUIChange(SceneView sceneView)
{
if (m_RhythmClipData.IsValid == false) { return false;}
// Get the custom extra data and synchronize the data inside the clip when the scene changes.
if (m_RhythmClipData.ClipParameters
.RhythmClipExtraNoteData is OsuSplineNoteRhythmClipExtraData extraNoteData) {
extraNoteData.PingPong = PingPong;
SpriteShapeControllerSynchronizer.SyncSplineData(m_MainSpline.ShapeController.spline, extraNoteData.spline);
EditorUtility.SetDirty(extraNoteData);
}
return false;
}
#endif
}
For pratical examples of these implementation have a look at the source code of the OSUNoteBase and OSUSplineNote where you will find a lot of interesting things, like how to set your own editor scene view handles to more intuitively edit values in the editor. And how to keep everything synced in editor preview.