1
\$\begingroup\$

I'm making a rhythm game for a university course and I can't figure out a way to make the long/sustained notes. For reference, this is what I mean taken from Guitar Hero:

enter image description here

I already have a system working for regular notes. The way I do it is that the notes are stored in an external file as numbers which represent the absence or presence of a note as well as what type of note it is, separated into musical bars: 000 010 020 001

There are three lanes for the notes to be, and this means that in this particular bar there are notes wherever it's not 0 (with the numbers being different types of notes). I had thought of using this system to create long/sustained note start and end markers (say 3 and 4) and that create another game object which would be the line between the start and end markers. But the way my note generator works is that the notes are not all generated at once because that'd be pretty heavy. Instead, the notes are generated at a certain time taking into account its speed so that it's instantiated and will reach the player's activator when it's supposed to. The following is a part of my note generator code, put in the Update method:

// if current song time - time offset is greater than
// time taken for all executed bars so far
// spawn the next bar's notes
if (songTimer - _timeOffset >= (barExecutedTime - barTime))
{
   StartCoroutine(PlaceBar(noteData.bars[barCount++]));

   barExecutedTime += barTime;
}

And here is the PlaceBar coroutine:

// go through all notes in a bar
// creates an instance of note prefab
// depending on which note is meant to be spawned
private IEnumerator PlaceBar(List<SongParser.Notes> bar)
{
   for (int i = 0; i < bar.Count; i++)
   {
       if (IsThereNote(bar[i].bottom))
       {
          GameObject _obj = (GameObject) Instantiate(GetNotePrefab(bar[i].bottom, true), new Vector3(bottomLane.transform.position.x + distance, bottomLane.transform.position.y, bottomLane.transform.position.z - 0.3f), Quaternion.identity);
       }
       if (bar[i].middle != 0)
       {
          GameObject _obj = (GameObject) Instantiate(GetNotePrefab(bar[i].middle, false), new Vector3(middleLane.transform.position.x + distance, middleLane.transform.position.y, middleLane.transform.position.z - 0.3f), Quaternion.identity);
       }
       if (bar[i].top != 0)
       {
          GameObject _obj = (GameObject) Instantiate(GetNotePrefab(bar[i].top, true), new Vector3(topLane.transform.position.x + distance, topLane.transform.position.y, topLane.transform.position.z - 0.3f), Quaternion.identity);
       }

       yield return new WaitForSeconds((barTime / bar.Count) - Time.deltaTime);
   }
}

So if I were to make start and end markers, the start marker would not know where the end marker is because it doesn't exist. Could anyone help please?

\$\endgroup\$
1
\$\begingroup\$

It looks like you can solve this by keeping track of the last-placed note in each lane, then stitching the continuation of that note onto it.

Let's assume you have a script on your note representation prefab(s) onto which we can add the concept of a "continuation" like what's below:

public class NoteRepresentation : MonoBehaviour {

    public NoteKind kind;
    NoteRepresentation continuation;

    public void AddContinuation(NoteRepresentation note) {
        if(kind != NoteKind.SustainedBegin && kind != NoteKind.SustainedMiddle) {
            Debug.LogWarningFormat("Tried to add a continuation to a note that doesn't support it ({0}). Check your song data.", kind);
            return;
        }
        continuation = note;
        // Here you might want to do things like adjust the size of the middle segment
        // to connect to this note, or extend a line renderer between the notes...
    }
}

Then your parser can track the most recent note it spawned in each lane:

NoteRepresentation[] latestNote = new NoteRepresentation[LANE_COUNT];

And when placing a note, you can check to see if it's a continuation of a previous starting note, and glue them together if so:

PlaceNote(NoteRepresentation prefab, int laneIndex) {

    Vector3 position = laneTransforms[laneIndex];
    position.x += distance;
    position.z -= 0.3f;

    // You could also serve these from a pool to reduce allocations/garbage.
    var noteRepresentation = Instantiate(prefab, position, Quaternion.identity);

    if ( noteRepresentation.kind == NoteKind.SustainedMiddle
      || noteRespresentation.kind == NoteKind.SustainedEnd ) {

        if(latestNote[laneIndex] == null) {
            Debug.LogError("Continuation note {0} is first note found in lane {1}", noteRepresentation.kind, laneIndex);
        } else {
            latestNote[laneIndex].AddContinuation(noteRepresentation);
        }
    }

    // Remember this note for future.
    latestNote[laneIndex] = noteRepresentation;
}

You can call this PlaceNote routine each time you try to place a note (we could go even further with indexing to make this a loop over n lanes instead of three separate checks):

if (IsThereNote(bar[i].bottom))
   PlaceNote(GetNotePrefab(bar[i].bottom,false), 0);

if (bar[i].middle != 0)
   PlaceNote(GetNotePrefab(bar[i].middle,false), 1);

if (bar[i].top != 0)
   PlaceNote(GetNotePrefab(bar[i].top,false), 2);

As long as your parser looks far enough ahead to find the continuation of a note before that note appears on screen,the player shouldn't notice that the long notes are built-up a piece at a time - by the time they come into view they should be fully-formed. If you can't guarantee that with your default approach, you could have the parser check to see if it has any outstanding nodes that need continuation in its latestNote array, and parse an extra bar further than normal if so.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.