Einführung
Sie möchten oder haben versucht, ein Rhythmus-Spiel zu erstellen, aber Spielelemente und Musik sind schnell nicht mehr synchron, und jetzt wissen Sie nicht, was Sie tun sollen. Dieser Artikel hilft Ihnen dabei. Ich habe Rhythmus-Spiele von der High School gespielt und oft in der örtlichen Arcade-Halle bei DDR rumgehangen. Heute bin ich immer auf der Suche nach neuen Spielen dieses Genres und Projekte wie
Crypt of the Necrodancer oder
Bit.Trip.Runner zeigen, dass in diesem Genre noch viel mehr getan werden kann. Ich habe ein wenig an Prototypen von Rhythmus-Spielen in Unity gearbeitet und als Ergebnis einen Monat damit verbracht, ein kurzes Rhythmus-Spiel / Puzzle
Atomic Beats zu erstellen. In diesem Artikel werde ich über die nützlichsten Code-Erstellungstechniken sprechen, die ich beim Erstellen dieser Spiele gelernt habe. Ich konnte nirgendwo anders Informationen über sie finden, oder sie wurden weniger detailliert dargestellt.
Zunächst muss ich Yu Chao meine
tiefe Anerkennung für den Beitrag von
Music Syncing in Rhythm Games [
Übersetzung in Habré ]
aussprechen . Yu überprüfte die Grundlagen der Synchronisierung von Audio-Timings mit der Game-Engine in Unity und lud den Quellcode für sein Boots-Cut-Spiel hoch, was mir bei der Erstellung meines Projekts sehr geholfen hat. Sie können seinen Beitrag studieren, wenn Sie eine kurze Einführung in die Musiksynchronisation von Unity erhalten möchten. Ich werde dieses Thema jedoch ausführlicher und ausführlicher behandeln. Mein Code verwendet aktiv Informationen aus dem Artikel und dem Boots-Cut-Code.
Das Herzstück eines jeden Rhythmus-Spiels sind Timings. Die Menschen reagieren äußerst empfindlich auf Verzerrungen in den Rhythmus-Timings. Daher ist es sehr wichtig, dass alle Aktionen, Bewegungen und Eingaben im Rhythmus-Spiel direkt mit der Musik synchronisiert werden. Leider verlieren herkömmliche Unity-
Zeiterfassungsmethoden wie
Time.timeSinceLevelLoad und
Time.time schnell die Synchronisation mit dem abgespielten Sound. Daher greifen wir direkt mit
AudioSettings.dspTime auf das Audiosystem zu, das die wahre Anzahl der vom Audiosystem verarbeiteten Audio-Samples verwendet. Dank dessen bleibt die Synchronisation mit der wiedergegebenen Musik immer erhalten (möglicherweise ist dies bei sehr langen Audiodateien nicht der Fall, wenn Sampling-Effekte ins Spiel kommen, aber bei Songs normaler Länge sollte das System einwandfrei funktionieren). Diese Funktion wird der Kern unserer Kompositionszeiterfassung sein, und basierend darauf werden wir die Hauptklasse erstellen.
Klassenleiter
Die Dirigentenklasse ist die Hauptkompositionsverwaltungsklasse, auf deren Grundlage der Rest des Rhythmus-Spiels aufgebaut wird. Damit verfolgen wir die Position der Komposition und verwalten alle anderen synchronisierten Aktionen. Um die Komposition zu verfolgen, benötigen wir einige Variablen
Wenn wir die Szene starten, müssen wir Berechnungen durchführen, um die Variablen zu bestimmen, und als Referenz die Zeit aufzeichnen, zu der der Song gestartet wurde.
void Start() {
Wenn Sie ein leeres GameObject mit einem solchen Skript erstellen und dann die Audioquelle mit der Komposition hinzufügen und das Programm ausführen, sehen Sie, dass das Skript die Startzeit der Komposition aufzeichnet, aber nichts anderes passiert. Wir müssen auch den BPM der Musik, die wir der Audioquelle hinzufügen, manuell eingeben.
Dank all dieser Werte können wir die Position in der Komposition bei der Aktualisierung des Spiels in Echtzeit verfolgen. Wir werden den Zeitpunkt der Komposition zuerst in Sekunden, dann in Brüchen bestimmen. Brüche sind eine viel bequemere Möglichkeit, eine Komposition zu verfolgen, da sie es uns ermöglichen, Aktionen und Timings parallel zur Komposition hinzuzufügen, beispielsweise in den Brüchen 1, 3 und 5.5, ohne dass Sekunden zwischen Brüchen berechnet werden müssen. Fügen Sie der Update () - Funktion der Conductor-Klasse die folgenden Berechnungen hinzu:
void Update() {
So erhalten wir die Differenz zwischen der aktuellen Zeit gemäß dem Audiosystem und der Startzeit der Komposition, die die Gesamtzahl der Sekunden angibt, in denen die Komposition abgespielt wird. Wir werden es in der songPosition-Variablen speichern.
Beachten Sie, dass die Partitur in der Musik normalerweise mit einer Einheit mit den Brüchen 1-2-3-4 usw. beginnt und songPositionInBeats bei 0 beginnt und von diesem Wert an zunimmt, sodass der dritte Teil der Komposition songPositionInBeats entspricht, also 2,0 und nicht 3,0.
Wenn Sie zu diesem Zeitpunkt ein traditionelles Spiel im Dance Dance Revolution-Stil erstellen möchten, müssen Sie Notizen entsprechend dem Bruch erstellen, in den Sie sie drücken müssen, ihre Position relativ zur Klicklinie interpolieren und dann songPositionInBeats aufzeichnen, wenn die Taste gedrückt wird, und Vergleichen Sie den Wert mit dem gewünschten Notenanteil. Yu Chao diskutiert in seinem
Artikel ein Beispiel für ein solches Schema. Um mich nicht zu wiederholen, werde ich andere potenziell nützliche Techniken in Betracht ziehen, die auf der Conductor-Klasse aufbauen können. Ich habe sie beim Erstellen von
Atomic Beats verwendet .
Wir passen uns dem ursprünglichen Anteil an
Wenn Sie Ihre eigene Musik für ein Rhythmus-Spiel erstellen, ist es einfach, den ersten Schlag genau auf den Beginn der Musik abzustimmen. Wenn BPM dies korrekt angibt, wird die Conductor-Klasse songPositionInBeats zuverlässig an die Komposition gebunden.
Wenn Sie jedoch vorgefertigte Musik verwenden, besteht eine hohe Wahrscheinlichkeit, dass vor Beginn der Komposition eine kurze Pause eingelegt wird. Wenn dies nicht berücksichtigt wird, glaubt die Conductor-Klasse songPositionInBeats, dass der erste Beat zu Beginn des Songs gestartet wurde und nicht der Beat jetzt. Alles, was weiter an die Werte der Aktien gebunden ist, wird nicht mit der Musik synchronisiert.
Um dies zu beheben, können Sie eine Variable hinzufügen, die diesen Versatz berücksichtigt. Fügen Sie der Conductor-Klasse Folgendes hinzu:
In Update () ist die Variable songPosition:
songPosition = (float)(AudioSettings.dspTime - dspSongTime);
ersetzt durch:
songPosition = (float)(AudioSettings.dspTime - dspSongTime - firstBeatOffset);
Jetzt berechnet songPosition die Position im Song korrekt, wobei der wahre erste Schlag berücksichtigt wird. Sie müssen jedoch den Versatz zum ersten Schlag manuell eingeben, damit er für jede Datei eindeutig ist. Zusätzlich wird es während dieser Verschiebung ein kurzes Fenster geben, in dem sich songPosition als negativ herausstellt. Dies hat möglicherweise keine Auswirkungen auf das Spiel, aber ein Code, der von den Werten von songPosition oder songPositionInBeats abhängt, kann derzeit möglicherweise keine negativen Zahlen verarbeiten.

Wiederholungen
Wenn Sie mit einer Komposition arbeiten, die von Anfang bis Ende abgespielt wird, reicht die oben gezeigte Dirigentenklasse aus, um die Position zu verfolgen. Wenn Sie jedoch eine kurze Spur haben, die geloopt ist, und mit dieser Schleife arbeiten möchten, müssen Sie die Repeater-Unterstützung in Conductor einbauen.
Wenn Sie ein perfekt gelooptes Fragment (z. B. wenn das Songtempo 120 bpm beträgt und das geloopte Fragment eine Länge von 4 Beats hat, sollte es genau 8,0 Sekunden bei 2,0 Sekunden pro Freigabe sein) in die Audio Source-Klasse der Conductor-Klasse geladen haben, aktivieren Sie das Loop-Kontrollkästchen. Der Dirigent funktioniert auf die gleiche Weise wie zuvor und überträgt die Gesamtzeit nach dem
ersten Start des Clips an songPosition. Um die Position der Schleife zu bestimmen, müssen wir Conductor irgendwie mitteilen, wie viele Freigaben sich in einer Schleife befinden und wie viele Schleifen bereits gespielt wurden. Fügen Sie der Conductor-Klasse die folgenden Variablen hinzu:
Jetzt können wir mit jedem Update von SongPositionInBeats auch die Update () - Position der Schleife aktualisieren.
Dies gibt uns einen Marker, der loopPositionInBeats mitteilt, wie viele Freigaben wir durch die Schleife gegangen sind, was für viele andere synchronisierte Elemente nützlich ist. Denken Sie daran, die Anzahl der Freigaben der Schleife in GameObject Conductor einzugeben.
Wir sollten auch die Berechnung der Aktien sorgfältig prüfen. Musik beginnt immer bei 1, daher nimmt die 4-teilige Messung die Form 1-2-3-4- an, und in unserer Klasse beginnt loopPositionInBeats bei 0,0 und führt Schleifen über 4,0 durch. Daher hat die exakte Mitte der Schleife, die bei der Berechnung der musikalischen Proportionen 3 beträgt, in loopPositionInBeats einen Wert von 2,0. Sie können loopPositionInBeats ändern, um dies zu berücksichtigen. Dies wirkt sich jedoch auf alle anderen Berechnungen aus. Seien Sie daher beim Einfügen von Notizen vorsichtig.
Auch für die verbleibenden Tools ist es hilfreich, der Conductor-Klasse zwei weitere Aspekte hinzuzufügen. Erstens eine analoge Version von LoopPositionInBeats namens LoopPositionInAnalog, die die Position in der Schleife im Bereich von 0 bis 1,0 misst. Die zweite ist eine Instanz der Conductor-Klasse zum bequemen Aufrufen von anderen Klassen. Fügen Sie der Conductor-Klasse die folgenden Variablen hinzu:
Fügen Sie in der Funktion Awake () Folgendes hinzu:
void Awake() { instance = this; }
und zur Funktion Update () hinzufügen:
loopPositionInAnalog = loopPositionInBeats / beatsPerLoop;
Synchronisieren
Es wäre sehr nützlich, Bewegung oder Rotation mit den Lappen zu synchronisieren, damit sich die Elemente an den richtigen Stellen befinden. In meinem Atomic Beats-Spiel habe ich damit Noten dynamisch um eine Mittelachse gedreht. Zunächst wurden sie entsprechend ihrem Anteil innerhalb der Schleife um den Umfang gelegt, und dann wurde der gesamte Spielbereich gedreht, so dass die Noten mit der Depressionslinie in ihrem Anteil übereinstimmten.
Erstellen Sie dazu ein neues Skript namens SyncedRotation und hängen Sie es an das GameObject an, das Sie drehen möchten. Fügen Sie der Update () - Funktion des SyncedRotation-Skripts Folgendes hinzu:
void Update() { this.gameObject.transform.rotation = Quaternion.Euler(0, 0, Mathf.Lerp(0, 360, Conductor.instance.loopPositionInAnalog)); }
Dieser Code interpoliert die Drehung des GameObjects, an das dieses Spiel gebunden ist, im Intervall von 0 bis 360 Grad und dreht es so, dass es am Ende jeder Schleife eine volle Umdrehung vollendet. Dies ist als Beispiel nützlich, aber für Loop- oder Frame-für-Frame-Animationen wäre es sinnvoller, Loop-Animationen so zu synchronisieren, dass sie perfekt zum Tempo passen.
Animationssynchronisierung
Unity
Animator ist extrem leistungsfähig, aber nicht immer genau. Für eine zuverlässige Ausrichtung von Animationen und Musik musste ich mich mit der Animator-Klasse und ihrer Tendenz messen, sich allmählich mit dem Tempo zu synchronisieren. Außerdem war es schwierig, dieselben Animationen an unterschiedliche Tempi anzupassen, sodass Sie beim Umschalten zwischen Kompositionen die Keyframes der Animation nicht auf das aktuelle Tempo neu definieren mussten. Stattdessen können wir direkt zur Animationsschleife gehen und die Position in dieser Schleife entsprechend der Position in der Schleife der Conductor-Klasse festlegen.
Erstellen Sie zunächst eine neue Klasse mit dem Namen SyncedAnimation und fügen Sie die folgenden Variablen hinzu:
Hängen Sie es an ein neues oder vorhandenes GameObject an, das Sie animieren möchten. In diesem Beispiel bewegen wir das Objekt einfach über den Bildschirm hin und her. Das gleiche Prinzip kann jedoch auf jede Animation angewendet werden, sei es vor dem Festlegen der Eigenschaft oder auf eine Einzelbildanimation. Fügen Sie GameObject ein Animator-Element hinzu und erstellen Sie einen neuen
Animator-Controller namens SyncedAnimController sowie einen
Animationsclip namens BackAndForth. Wir laden den Controller in die Animator-Klasse, die an das GameObject angehängt ist, und fügen dem Animationsbaum als Standardanimation Animation hinzu.
Zum Beispiel habe ich die Animation so eingerichtet, dass das Objekt zuerst um 6 Einheiten nach rechts, dann um -6 nach links und dann zurück auf 0 verschoben wird.
Fügen Sie nun zum Synchronisieren der Animation den folgenden Code zur Start () - Funktion der SyncedAnimation-Klasse hinzu, mit der Informationen zum Animator initialisiert werden:
void Start() {
Fügen Sie dann den folgenden Code zu Update () hinzu, um die Animation festzulegen:
void Update() {
Wir positionieren die Animation also im exakten Rahmen der Animation relativ zu einer vollständigen Schleife. Wenn Sie beispielsweise die obige Animation verwenden und sich in der Mitte der Schleife befinden, wird die GameObject-Position nur 0 überschreiten. Dies kann auf jede Animation angewendet werden, die Sie erstellen und die Sie mit dem Conductor-Tempo synchronisieren möchten.
Es ist auch erwähnenswert, dass Sie zum Erstellen einer nahtlosen Schleife von Animationen die Tangenten einzelner
Keyframes der Animation auf der Animationskurve konfigurieren müssen. Mit der Einstellung "Linear" wird eine gerade Linie von einem Keyframe zum nächsten erstellt, und Constant behält die Animation bis zum nächsten Keyframe in einem Wert bei, wodurch eine ruckartige und scharfe Bewegung erzielt wird.
Obwohl diese Methode nützlich ist, wirkt sie sich auf alle Übergänge der Animation aus, da sie dazu führt, dass
animationState in dem Zustand bleibt, in dem es sich befand, als das Skript ursprünglich ausgeführt wurde. Diese Methode ist nützlich für Objekte, die nur eine synchronisierte Animation endlos verwenden müssen. Um jedoch komplexere Objekte mit unterschiedlichen synchronisierten Animationen zu erstellen, müssen Sie Code hinzufügen, der diese Übergänge verarbeitet und die Variable currentState entsprechend dem gewünschten Animationsstatus festlegt.
Fazit
Dies sind nur einige der Aspekte, die mir bei der Erstellung von Atomic Beats geholfen haben. Einige von ihnen wurden aus anderen Quellen gesammelt oder aus der Not heraus erstellt, aber die meisten konnte ich nicht in der fertigen Form finden, also hoffe ich, dass dies nützlich ist! Vielleicht ist ein Teil meines Systems aufgrund von CPU- oder Audiosystembeschränkungen in großen Projekten nicht mehr nützlich, aber es ist eine gute Grundlage für das Spielen eines Game Jam oder eines Hobbyprojekts.

Das Erstellen eines Rhythmus-Spiels oder von mit Musik synchronisierten Spielelementen kann schwierig sein. Um alles in einem konstanten Tempo zu halten, benötigen Sie möglicherweise einen kniffligen Code. Ein Ergebnis, mit dem Sie in einem konstanten Tempo spielen können, kann für den Spieler sehr attraktiv sein. In diesem Genre kann viel mehr getan werden als Spiele im traditionellen Dance Dance Revolution-Stil, und ich hoffe, dieser Artikel hilft Ihnen bei der Realisierung solcher Projekte. Ich empfehle außerdem, wenn möglich, mein
Atomic Beats- Spiel zu evaluieren. Ich habe es in einem Monat im Frühjahr dieses Jahres geschafft, es hat 8 kurze Tracks und es ist kostenlos!