Gå til indhold

Callbacks

Dette afsnit dækker indsamling af data, oprettelse af effektfunktioner og brug af Meter-klassen til at udløse disse funktioner. Der lægges ikke vægt på forklaring af animationer eller detaljer om vores effekthåndtere. Vil du friske op, anbefaler jeg vores afsnit om Tutorials eller API-reference.

Processen til effektivt at udløse en animation fra start til slut er:

  1. Brug metoden Meter.setValue(value) inden for update-funktionen til at indsætte rå målerdata i en instans af din Meter-klasse.
  2. Når Meter’en vurderer sig selv som stabil, aktiverer den callback-funktionen der er sendt som anden parameter.
  3. Denne callback-funktion indeholder betinget logik til evaluering af Meter’ens tilstand. Hvis tilstanden er sand, bør din effekt føjes til effects-arrayet (effects.push(new yourEffect())) eller til state-handleren (steMgr.Push(new yourEffect())).
  4. Hvert gennemløb af update-funktionen evaluerer det komplette effects-array og det nyeste element i state-handleren. I denne fase tegnes effekterne baseret på tilstandsvariabler.
  5. Efter tegning bidrager hver effekt til kontrol af sin levetid. Effekter i effects-arrayet itererer simpelthen en levetidsvariabel der er sat til instansen af deres klasse. Hvis levetiden er 0 eller derunder, fjernes effekten let med et splice under evaluering af effects-arrayet. Effekter i state-handleren kan fjerne sig selv om nødvendigt, da state-handleren fungerer som en stak og den betingede logik for disse effekter typisk er mere kompleks.

Denne måler søger efter en typisk livsstang og returnerer procentdelen af matchende farve som et tal mellem 0 og 1. HSL-området er i dette tilfælde ret bredt for at tage højde for en farvegradient i stangfarven. Dette skaber et problem: Når livsstangen skrumper, afslører den faktisk en gennemsigtig baggrund der kan indeholde grønne elementer fra spilmiljøet, som kan bestå farvetjekket og forstyrre vores måler.

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

Her har jeg oprettet en instans af vores Meter-klasse ved navn healthM for at aktivere en “modtaget skade”-effekt.

<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>

Jeg har valgt et ret højt stabilitetskrav (25) for at tage højde for ovennævnte instabilitetsproblem. Den tilknyttede callback-funktion aktiveres kun, når målerværdien er stabil i 25 opdateringer, hvilket udelukker det meste data der ikke er den faktiske livsstang, selv hvis spilleren står stille. Den eneste ulempe er den voksende forsinkelse af effekten, som i dette scenario altid vil være den begrænsende faktor. For denne bestemte livsstang kan vi modvirke det på mindst et par måder:

  • Når et slag lander, konverteres en del af den grønne stang straks til rød, inden den skrumper til den nye grønne del. Hvis vi tilføjer en måler healthRed der søger efter rødt på samme sted som sundhedsmåleren, kan vi kombinere deres værdier for hurtigere aktivering. En betingelse if ( healthM.decreased && healthRed.increased ) bør give god nøjagtighed i dette tilfælde.
  • Vi kan også kombinere aflæsningerne fra flere lineære målere med de samme farveindstillinger. Jeg har i øjeblikket en lineær måler i midten af livsstangen. Hvis jeg opretter to til, én lidt over og under originalen, men stadig inden for livsstangen, kan nærheden af de tre målerværdier bruges som en yderligere stabilitetskontrol. Alle tre målere skal rapportere Meter.decreased og ligge inden for et lille værdiområde fra hinanden for at udløse effekten.

Denne callback-funktion køres kun, efter at måleren har bekræftet at alle elementer i dens værdisarray er ens. Dette sikrer at målerværdierne er baseret på stabile data. Inden for callbacken bruger jeg en betinget sætning til at afgøre om effekten skal afspilles.

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

Den betingede sætning kræver to sande betingelser: et stabilt fald i måleren og en afvigelse mellem den aktuelle målerværdi og en tidligere registreret værdi. Jeg har tilføjet denne anden betingelse for at undgå en fejl. Tjekker vi kun for et fald, ville effekten afspilles gentagne gange. Da Meter.decreased sættes, så snart måleren er stabil, forbliver dens værdi den samme så længe måleren ikke ændrer sig. På den måde afspilles effekten kun, når der er et fald der adskiller sig fra den seneste stabile værdi.

Jeg opretter to effekter: én til effects-arrayet og én til state-handleren.

Denne første kodeblok definerer en effektfunktion beregnet til effects-arrayet. Den er designet til at være hurtig og letvægtig, så flere mindre effekter kan køre samtidig. For at reducere systembelastningen har jeg også gjort effekten let genanvendelig via parametre.

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;
}
}

Sådan bør det se ud i aktion:

Takket være det redigerbare design af funktionen kan vi nemt oprette en helingseffekt! Tilføj blot en ekstra betingelse til callback-funktionen.

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;
}
}

Enkelheden i denne effekt er dens største fordel for en LightScript-udvikler. I stedet for at oprette brugerdefinerede designereffekter der hver kræver hundredvis af kodelinjer, byg små, genanvendelige effekter der fungerer som byggeklodser for specifikke udløsere. De næste tre effekter er variationer baseret på den lille effekt vi lige har oprettet. Eksperimentér og se hvad du kan opnå!

Jeg reserverer personligt state-handleren til store, komplekse effekter med meget specifik tilstandslogik, især når effekten skal have prioritet over alle andre. Bemærk at state-handleren fungerer som en stak og kun kører den øverste effekt; hver state-effekt er ansvarlig for at fjerne sig selv, når den er færdig. Dette eksempel demonstrerer en matchmaking-lobby-animation der tilpasser sig afhængigt af om du søger eller ej. Det inkluderer også sit eget lille effects-array med en hjælpeeffekt. De målere der bruges her er inLobby og yellowPlayButton, som kun aktiveres i matchmaking-lobbyen. yellowPlayButton skifter farve afhængigt af om du søger.

function lobbyAnimation(){
// Instance variables
this.start = new Date().getTime();
this.elapsed = 0;
this.radius = 0;
this.searching = 1000;
// Instance effects array
this.effects = [];
this.Process = function () {
if (inLobby.decreased && yellowPlayButton.decreased) {
stateMgr.Pop();
lobbyAnim = false;
}
yellowPlayButton.value == 1 ? this.searching = 1000 : this.searching = 100;
this.Draw();
};
this.Draw = function(){
this.elapsed = new Date().getTime() - this.start;
this.radius = 50 + (Math.sin(this.elapsed/this.searching)*10);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 320, 200);
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, 50%)`;
ctx.lineWidth = 20;
ctx.arc(160, 100, this.radius, 0, 2 * Math.PI);
ctx.stroke();
if(this.radius > 59 && this.elapsed % 2 == 0){
this.effects.push(new lobbyHelper(160, 100, 69));
}
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(){
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();
ctx.globalAlpha = 1;
this.radius++;
}
}
var stateMgr = new StateHandler();
var lobbyAnim = false;
function update(){
stateMgr.Process();
}
function lobbyAnimationPlay(){
if(inLobby.value == 1 && yellowPlayButton.value == 1 && !lobbyAnim){
stateMgr.Push(new lobbyAnimation());
lobbyAnim = true;
}
};

Søger ikke:

Søger:

Hermed afsluttes vores Callbacks-tutorial samt den officielle SignalRGB-udvikler-gennemgang. Hold øje med næste og sidste tilføjelse til vores gennemgang, Lightscript-vedligeholdelse!