Przejdź do głównej zawartości

Callbacks

Ta sekcja omawia zbieranie danych, budowanie funkcji efektów i używanie klasy Meter do ich aktywowania. Nie skupiamy się tutaj na wyjaśnianiu animacji ani szczegółów naszych procedur obsługi efektów; jeśli chcesz odświeżyć wiedzę w tym zakresie, polecamy nasze sekcje Samouczki lub Dokumentacja API.

Proces efektywnego aktywowania animacji od początku do końca to:

  1. Użyj metody Meter.setValue(value) w funkcji aktualizacji, aby wstawić surowe dane miernika do instancji klasy Meter.
  2. Gdy Meter uzna się za stabilny, aktywuje funkcję callback przekazaną jako drugi parametr.
  3. Ta funkcja callback zawiera logikę warunkową do oceny stanu Meter. Jeśli stan jest prawdziwy, twój efekt powinien zostać dodany do tablicy efektów (effects.push(new twójEfekt())) lub procedury obsługi stanu (stateMgr.Push(new twójEfekt())).
  4. Każde wykonanie funkcji aktualizacji ocenia pełną tablicę efektów i ostatni element w procedurze obsługi stanu. Na tym etapie efekty są rysowane na podstawie zmiennych stanu.
  5. Po narysowaniu każdy efekt przyczynia się do kontrolowania swojego czasu życia. Efekty w tablicy efektów po prostu iterują zmienną czasu życia ustawioną w instancji ich klasy. Gdy czas życia wynosi 0 lub mniej, efekt można po prostu usunąć za pomocą splice podczas oceny tablicy efektów. Efekty w procedurze obsługi stanu mogą się same usuwać w razie potrzeby, ponieważ procedura obsługi stanu działa jak stos, a logika warunkowa dla tych efektów jest często bardziej złożona.

Ten miernik szuka typowego paska zdrowia i zwraca procent pasującego koloru jako liczbę między 0 a 1. Zakres HSL jest w tym przypadku dość szeroki, aby objąć gradient kolorów na pasku. Powoduje to jednak problem: gdy pasek zdrowia kurczy się, odsłania przezroczyste tło, które może zawierać zielone elementy ze środowiska gry przechodzące kontrolę koloru i zakłócające nasz miernik.

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

Tutaj stworzyłem instancję naszej klasy Meter, o nazwie healthM, aby aktywować efekt “otrzymano obrażenia”.

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

Ustawiłem dość wysoki wymóg stabilności (25), aby uwzględnić wspomnianą wcześniej niestabilność. Dołączona funkcja callback jest aktywowana tylko wtedy, gdy wartość miernika jest stabilna przez 25 aktualizacji, co wyklucza większość danych, które nie są właściwym paskiem zdrowia, nawet gdy gracz stoi nieruchomo. Jedyną wadą jest rosnące opóźnienie efektu, co zawsze będzie czynnikiem ograniczającym w tym scenariuszu. W przypadku tego konkretnego paska zdrowia możemy temu zaradzić na co najmniej kilka sposobów:

  • Gdy gracz otrzymuje obrażenia, część zielonego paska natychmiast zmienia kolor na czerwony, zanim skurczy się do nowego poziomu zielonego zdrowia. Jeśli dodamy miernik healthRed szukający czerwonego w tym samym miejscu co miernik zdrowia, możemy połączyć ich wartości w celu szybszej aktywacji. Warunek if ( healthM.decreased && healthRed.increased ) zapewniłby w tym przypadku dobrą dokładność.
  • Możemy również łączyć pomiary z wielu mierników liniowych z tymi samymi ustawieniami koloru. Obecnie mam jeden miernik liniowy ustawiony na środku paska zdrowia. Gdybym dodał jeszcze dwa — jeden nieco powyżej i jeden nieco poniżej oryginału, ale nadal w obrębie paska zdrowia — bliskość trzech wartości mierników mogłaby być użyta jako dodatkowa kontrola stabilności. Wszystkie trzy mierniki musiałyby odczytywać Meter.decreased i znajdować się w małym zakresie od wartości siebie nawzajem, aby aktywować efekt.

Ta funkcja callback jest wykonywana tylko po tym, jak miernik zweryfikuje, że wszystkie elementy w jego tablicy wartości są równe. Zapewnia to, że wartości miernika są oparte na stabilnych danych. Wewnątrz callbacku używam instrukcji warunkowej do określenia, czy efekt powinien zostać odtworzony.

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

Instrukcja warunkowa wymaga dwóch prawdziwych warunków: stabilnego spadku w mierniku i różnicy między bieżącą wartością miernika a wcześniej zapisaną wartością. Dodałem ten drugi warunek, aby zapobiec błędowi. Gdybyśmy sprawdzali tylko spadek, efekt byłby odtwarzany wielokrotnie. Ponieważ Meter.decreased jest ustawiane gdy miernik się stabilizuje, wartość pozostaje taka sama dopóki miernik się nie zmieni. W ten sposób efekt jest odtwarzany tylko wtedy, gdy nastąpił spadek różniący się od ostatniej stabilnej wartości.

Stworzę dla ciebie dwa efekty: jeden dla tablicy efektów i jeden dla procedury obsługi stanu.

Ten pierwszy blok kodu definiuje funkcję efektu przeznaczoną dla tablicy efektów. Jest zaprojektowana tak, aby była szybka i lekka, pozwalając na jednoczesne wykonywanie wielu małych efektów. Aby zmniejszyć obciążenie systemu, uczyniłem efekt również łatwo konfigurowalnym za pomocą parametrów do ponownego użycia.

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

Oto jak to wygląda w akcji:

Dzięki edytowalnej konstrukcji funkcji możemy również łatwo stworzyć efekt leczenia! Wystarczy dodać dodatkowy warunek do funkcji callback.

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

Prostota tego efektu to największa zaleta dla dewelopera LightScript. Zamiast tworzyć niestandardowe efekty projektowe, z których każdy wymaga setek linii kodu, budujesz małe, wielokrotnego użytku efekty, które służą jako elementy budulcowe dla konkretnych wyzwalaczy. Poniższe trzy efekty to warianty oparte na małym efekcie, który właśnie stworzyliśmy. Eksperymentuj swobodnie i odkryj, co możesz osiągnąć!

Osobiście rezerwuję procedurę obsługi stanu dla dużych, złożonych efektów z bardzo specyficzną logiką stanu, szczególnie gdy chcesz, aby efekt miał priorytet nad wszystkimi innymi. Pamiętaj, że procedura obsługi stanu działa jak stos i wykonuje tylko górny efekt; każdy efekt stanu jest odpowiedzialny za usuwanie się, gdy jest gotowy. Ten przykład demonstruje animację lobby matchmakingu, która dostosowuje się w zależności od tego, czy szukasz meczu, czy nie. Zawiera również własną małą tablicę efektów z efektem pomocniczym. Używane tutaj mierniki to inLobby i yellowPlayButton, które aktywują się tylko w lobby matchmakingu. Kolor yellowPlayButton zmienia się w zależności od tego, czy szukasz meczu.

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

Nie szukam meczu:

Szukam meczu:

Na tym kończymy nasz samouczek dotyczący callbacków, a także oficjalny przewodnik dewelopera SignalRGB. Śledź kolejny i ostatni dodatek do naszego przewodnika: Konserwacja LightScript!