Zum Inhalt springen

Callbacks

Dieser Abschnitt behandelt das Erfassen von Daten, das Erstellen von Effektfunktionen und die Verwendung der Meter-Klasse, um diese Funktionen auszulösen. Es wird keine Betonung auf die Erklärung von Animationen oder die Einzelheiten unserer Effekt-Handler gelegt. Falls du dich auffrischen möchtest, empfehle ich unsere Tutorials- oder API-Referenz-Abschnitte.

Der Prozess zum effektiven Auslösen einer Animation von Anfang bis Ende ist:

  1. Verwende die Methode Meter.setValue(value) innerhalb der update-Funktion, um rohe Meter-Daten in eine Instanz deiner Meter-Klasse einzufügen.
  2. Wenn der Meter sich als stabil einschätzt, aktiviert er die callback-Funktion, die als zweiter Parameter übergeben wurde.
  3. Diese callback-Funktion enthält bedingte Logik zur Auswertung des Zustands des Meters. Wenn der Zustand truthy ist, sollte dein Effekt zum effects-Array hinzugefügt werden (effects.push(new yourEffect())) oder zum state-Handler (steMgr.Push(new yourEffect())).
  4. Jeder Durchlauf der update-Funktion wertet das vollständige effects-Array und das neueste Element im state-Handler aus. In dieser Phase werden die Effekte basierend auf Zustandsvariablen gezeichnet.
  5. Nach dem Zeichnen trägt jeder Effekt zur Überprüfung seiner Lebensdauer bei. Effekte im effects-Array iterieren einfach eine Lebensdauervariable, die auf die Instanz ihrer Klasse gesetzt ist. Wenn die Lebensdauer 0 oder weniger beträgt, wird der Effekt leicht mit einem splice während der Auswertung des effects-Arrays entfernt. Effekte im state-Handler können sich selbst entfernen, wenn nötig, da der state-Handler als Stack funktioniert und die bedingte Logik für diese Effekte tendenziell komplizierter ist.

Dieser Meter sucht nach einer typischen Lebensleiste und gibt den Prozentsatz der übereinstimmenden Farbe als Zahl zwischen 0 und 1 zurück. Der HSL-Bereich ist in diesem Fall recht breit, um einen Farbverlauf in der Leistenfärbung zu berücksichtigen. Dies führt zu einem Problem: Wenn die Lebensleiste schrumpft, enthüllt sie tatsächlich einen transparenten Hintergrund, der grüne Elemente aus der Spielumgebung enthalten kann, die die Farbprüfung bestehen und unseren Meter stören.

<head>
<meta meter="health" tags="example" x= ".05" y=".9" width=".189" h="70-140" s="40-100" l="40-100" type="linear"/>
</head>

Hier habe ich eine Instanz unserer Meter-Klasse namens healthM erstellt, um einen “Schaden erlitten”-Effekt zu aktivieren.

<script>
// Meter instance
var healthM = new Meter(25, healthHelper);
function update () => {
healthM.setValue(health);
window.requestAnimationFrame(update);
}
function healthHelper () => {
// "Took Damage" effect up next
}
// . . .
</script>

Ich habe eine ziemlich hohe Stabilitätsanforderung (25) gewählt, um das oben genannte Instabilitätsproblem zu berücksichtigen. Die angehängte callback-Funktion wird nur aktiviert, wenn der Meter-Wert für 25 Updates stabil ist, was die meisten Daten ausschließt, die nicht die eigentliche Lebensleiste sind, selbst wenn der Spieler stillsteht. Der einzige Nachteil ist die wachsende Latenz des Effekts, die in diesem Szenario immer der limitierende Faktor sein wird. Für diese bestimmte Lebensleiste können wir dem auf mindestens ein paar Arten entgegenwirken:

  • Wenn ein Treffer landet, wird ein Teil der grünen Leiste sofort in Rot umgewandelt, bevor sie auf den neuen Grünanteil schrumpft. Wenn wir einen Meter healthRed hinzufügen, der an derselben Stelle wie der Gesundheits-Meter nach Rot sucht, können wir ihre Werte für schnelleres Auslösen kombinieren. Ein bedingtes if ( healthM.decreased && healthRed.increased ) sollte in diesem Fall gute Genauigkeit liefern.
  • Wir können auch die Messwerte mehrerer linearer Meter mit denselben Farbeinstellungen kombinieren. Ich habe derzeit einen linearen Meter in der Mitte der Lebensleiste eingerichtet. Wenn ich zwei weitere einrichte, einen leicht ober- und unterhalb des Originals, aber noch innerhalb der Lebensleiste, kann die Nähe der drei Meter-Werte als zusätzliche Stabilitätsprüfung verwendet werden. Alle drei Meter müssten Meter.decreased melden und innerhalb eines kleinen Wertebereichs voneinander liegen, um den Effekt auszulösen.

Diese callback-Funktion wird nur ausgeführt, nachdem der Meter bestätigt hat, dass alle Elemente in seinem Werte-Array gleich sind. Dies stellt sicher, dass die Meter-Werte auf stabilen Daten basieren. Innerhalb des Callbacks verwende ich eine bedingte Anweisung, um zu bestimmen, ob der Effekt abgespielt werden soll.

let healthPrev = 0;
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
effects.push(new healthEffect());
healthPrev = healthM.value;
}
}

Die bedingte Anweisung erfordert zwei truthy-Bedingungen: eine stabile Abnahme des Meters und eine Abweichung zwischen dem aktuellen Meter-Wert und einem zuvor aufgezeichneten Wert. Ich habe diese zweite Bedingung hinzugefügt, um einen Fehler zu vermeiden. Wenn wir nur auf eine Abnahme prüfen würden, würde der Effekt wiederholt abgespielt. Da Meter.decreased gesetzt wird, sobald der Meter stabil ist, bleibt sein Wert gleich, solange sich der Meter nicht ändert. Auf diese Weise wird der Effekt nur abgespielt, wenn es eine Abnahme gibt, die sich vom letzten stabilen Wert unterscheidet.

Ich erstelle zwei Effekte: einen für das effects-Array und einen für den state-Handler.

Dieser erste Code-Block definiert eine Effektfunktion, die für das effects-Array gedacht ist. Sie ist darauf ausgelegt, schnell und leichtgewichtig zu sein, damit mehrere kleinere Effekte gleichzeitig laufen können. Um die Systemlast zu reduzieren, habe ich den Effekt auch leicht über Parameter zur Wiederverwendung anpassbar gemacht.

function healthEffect(color, direction, speed){
// The lifetime of the effect should be iterated in the draw function.
this.lifetime = 200;
this.speed = speed;
this.direction = direction;
this.color = color;
this.draw = () => {
// Set fill from parameter
ctx.fillStyle = `hsl(${this.color}, 100%, 50%)`;
// Check direction
if (this.direction == "up"){
ctx.fillRect(0, this.lifetime, width, 30);
} else {
ctx.fillRect(0, height - this.lifetime, width, 30);
}
// As the lifetime decreases by 5 each update, the rectangle moves accordingly.
this.lifetime -= speed;
}
}
function update() {
// Background color
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 320, 200);
// Draw all effects in array and remove them if they are done.
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime == 0){
effects.splice(i, 1);
}
})
window.requestAnimationFrame(update);
}
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Make sure to correctly set the color and direction paremeters.
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
}

So sollte es in Aktion aussehen:

Dank des editierbaren Designs der Funktion können wir auch leicht einen Heilungseffekt erstellen! Füge einfach eine zusätzliche Bedingung zur callback-Funktion hinzu.

function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Damage effect
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
if (healthM.increased && healthM.value != healthPrev){
// Healing effect
effects.push(new healthEffect(120, "up", 5));
healthPrev = healthM.value;
}
}

Die Einfachheit dieses Effekts ist sein größter Vorteil für einen LightScript-Entwickler. Anstatt benutzerdefinierte Designer-Effekte zu erstellen, die jeweils Hunderte von Codezeilen erfordern, baue kleine, wiederverwendbare Effekte, die als Bausteine für bestimmte Auslöser dienen. Die nächsten drei Effekte sind Variationen basierend auf dem kleinen Effekt, den wir gerade erstellt haben. Experimentiere und sieh, was du erreichen kannst!

Ich reserviere den state-Handler persönlich für große, komplexe Effekte mit sehr spezifischer Zustandslogik, besonders wenn der Effekt Priorität über alle anderen haben soll. Beachte, dass der state-Handler wie ein Stack funktioniert und nur den obersten Effekt ausführt; jeder state-Effekt ist dafür verantwortlich, sich selbst zu entfernen, wenn er fertig ist. Dieses Beispiel demonstriert eine Matchmaking-Lobby-Animation, die sich anpasst, je nachdem ob du suchst oder nicht. Es enthält auch sein eigenes kleines effects-Array mit einem Hilfseffekt. Die hier verwendeten Meter sind inLobby und yellowPlayButton, die nur im Matchmaking-Lobby aktiviert werden. Der yellowPlayButton ändert die Farbe je nachdem, ob du suchst.

function lobbyAnimation(){
// Instance variables
this.start = new Date().getTime();
this.elapsed = 0;
this.radius = 0;
this.searching = 1000;
// Instance effects array
this.effects = [];
// Process is the method that allows a state effect to remove itself
this.Process = function () {
// If the lobby menu and play button disappear, remove the effect
if (inLobby.decreased && yellowPlayButton.decreased) {
stateMgr.Pop();
// Global boolean to prevent duplicate lobby effects in the state handler
lobbyAnim = false;
}
// If the play button is present, edit this.searching based on its color
yellowPlayButton.value == 1 ? this.searching = 1000 : this.searching = 100;
// Call the Draw function attached to the effect
this.Draw();
};
this.Draw = function(){
// Iterate important variables
this.elapsed = new Date().getTime() - this.start;
this.radius = 50 + (Math.sin(this.elapsed/this.searching)*10);
// Fill the background to overwrite other effects
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 320, 200);
// Draw the main arc
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, 50%)`;
ctx.lineWidth = 20;
ctx.arc(160, 100, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// If the main arc is the right size, push helper effects into the helper array
if(this.radius > 59 && this.elapsed % 2 == 0){
this.effects.push(new lobbyHelper(160, 100, 69));
}
// Play and remove helper effects if necessary
this.effects.forEach((e, i) => {
ctx.lineWidth = 1;
e.draw();
if (e.radius > 360) {
this.effects.splice(i, 1);
}
});
}
}
function lobbyHelper(x, y, radius){
this.x = x;
this.y = y;
this.radius = radius;
this.draw = function(){
// Set the global transparency value. 1 is opaque, 0 is invisible.
ctx.globalAlpha = 1 - this.radius/360;
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, ${50 + (this.radius/7)}%)`;
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// Set the global transparency back to 1 so your other effects are opaque
ctx.globalAlpha = 1;
this.radius++;
}
}
// Declare state handler (found in Snippets)
var stateMgr = new StateHandler();
// Global boolean to prevent duplicate lobby animations
var lobbyAnim = false;
function update(){
// Run the Process function to draw the animation
stateMgr.Process();
}
function lobbyAnimationPlay(){
if(inLobby.value == 1 && yellowPlayButton.value == 1 && !lobbyAnim){
stateMgr.Push(new lobbyAnimation());
lobbyAnim = true;
}
};

Nicht suchend:

Suchend:

Damit schließt unser Callbacks-Tutorial sowie der offizielle SignalRGB-Entwickler-Walkthrough ab. Halte Ausschau nach dem nächsten und letzten Zusatz zu unserem Walkthrough, Lightscript-Wartung!