If you're seeing this message, it means we're having trouble loading external resources on our website.

Jeżeli jesteś za filtrem sieci web, prosimy, upewnij się, że domeny *.kastatic.org i *.kasandbox.org są odblokowane.

Główna zawartość

System cząstek

Do tej pory stworzyliśmy jedną cząsteczkę, która ponownie pojawia się po tym, jak umrze. Teraz chcemy stworzyć ciągły strumień cząsteczek, dodając nowe w każdym cyklu za pomocą draw(). Możemy po prostu stworzyć tablicę i dodawać do niej nowe cząsteczki za każdym razem:
var particles = [];
draw = function() {
  background(133, 173, 242);
  particles.push(new Particle(new PVector(width/2, 50)));

  for (var i = 0; i < particles.length; i++) {
    var p = particles[i];
    p.run();
  }
};
Jeśli spróbujesz tej metody i uruchomisz ten kod na parę minut, zdasz sobie sprawę, że liczba klatek na sekundę będzie spadać i spadać, aż w końcu program się zawiesi. Jest to spowodowane tym, że dodajemy coraz więcej cząstek które musimy przetworzyć i wyświetlić, a żadnych nie usuwamy. Gdy cząsteczki są martwe, nie mamy z nich więcej pożytku, więc możemy zaoszczędzić naszemu programowi niepotrzebnej pracy i je po prostu usunąć.
By usunąć rzeczy z tablicy w JavaScript, możemy użyć metody splice(), podając identyfikator rzeczy, którą chcemy usunąć oraz liczbę elementów (w tym wypadku tylko jeden). Zrobimy to po sprawdzeniu, czy cząsteczka rzeczywiście jest martwa:
var particles = [];
draw = function() {
  background(133, 173, 242);
  particles.push(new Particle(new PVector(width/2, 50)));

  for (var i = 0; i < particles.length; i++) {
    var p = particles[i];
    p.run();
    if (p.isDead()) {
      particles.splice(i, 1);
    }
  }
};
Chociaż powyższy kod będzie działał dobrze (a program nigdy się nie zawiesi), otworzyliśmy małą puszkę pandory. Gdy manipulujemy zawartością tablicy podczas iterowania po niej, możemy mieć do czynienia z pewnymi problemami. Weźmy dla przykładu następujący kod:
for (var i = 0; i < particles.length; i++) {
  var p = particles[i];
  p.run();
  particles.push(new Particle(new PVector(width/2, 50)));
}
To dosyć ekstremalny przykład (plus nie ma on większego sensu), ale przedstawia dobrze o co chodzi. W powyższym przykładzie dla każdej cząstki w tablicy dodajemy nową cząstkę do tablicy(przez to zmieniając długość(length) tablicy). Sprawi to, ze znajdziemy się w pętli nieskończonej, gdyż i nigdy nie dogoni wartości particles.length.
Pomimo, że usuwanie obiektów z tablicy cząsteczek podczas pętli nie powoduje awarii programu (jak ma to miejsce w przypadku dodawania), problem jest jeszcze gorszy, gdyż nie zostawia żadnych śladów. By odkryć przyczynę, wpierw musimy zdać sobie sprawę z jednej rzeczy. Gdy coś jest usuwane z tablicy, wszystkie pozostałe obiekty przesuwają się o jedno miejsce w lewo. Zauważ na poniższym wykresie moment, gdy cząstka C (indeks 2) jest usuwana. Cząstki A i B zachowują ten sam indeks, a cząstki D i E zmieniają pozycję kolejno z 3 i 4 na 2 i 3.
Udajmy, że jesteśmy wartością i która iteruje w pętli po tablicy.
  • gdy i = 0 → Sprawdź cząstkę A → Nie usuwaj
  • gdy i = 1 → Sprawdź cząstkę B → Nie usuwaj
  • gdy i = 2 → Sprawdź cząstkę C → Usuń!
  • (Przesuń cząsteczki D i E z pozycji 3 i 4 na 2 i 3)
  • gdy i = 3 → Sprawdź cząstkę E → Nie usuwaj
Widzisz problem? Nigdy nie sprawdziliśmy cząsteczki D! Gdy C zostało usunięte z miejsca #2, D przeniosło się na miejsce #2, ale i już przeniosło się na pozycję #3. To nie jest żadna katastrofa, gdyż cząstka D zostanie sprawdzona następnym razem. Jednak mimo wszystko oczekujemy, że nasz kod będzie przeglądał wszystkie przedmioty w tablicy. Pomijanie elementu jest niedopuszczalne.
Istnieje proste rozwiązanie: po prostu iterujmy tablicę od tyłu. Jeśli przesuwamy przedmioty z prawej do lewej usuwając przedmioty z tablicy, niemożliwe jest wtedy pominąć przypadkiem element. Musimy tylko zmienić trzy znaki w naszej pętli:
  for (var i = particles.length-1; i >= 0; i--) {
    var p = particles[i];
    p.run();
    if (p.isDead()) {
      particles.splice(i, 1);
    }
  }
Składając to wszystko w jedną całość, otrzymujemy:
OK. Zrobiliśmy w sumie dwie rzeczy. Napisaliśmy obiekt opisujący pojedynczą Cząsteczkę. Odkryliśmy, jak używać tablic by zarządzać wieloma Cząsteczkami (oraz jak je dodawać i usuwać według potrzeby).
Moglibyśmy na tym skończyć. Jednak możemy, a nawet powinniśmy dołożyć jeszcze jeden dodatkowy krok i stworzyć obiekt opisujący kolekcję Cząsteczek — obiekt ParticleSystem. Pozwoli nam to usunąć niepotrzebna logikę przeglądania wszystkich cząsteczek z głównej tablicy, a także pozwoli na posiadanie więcej niż jednego systemu cząsteczek.
Przypomnij sobie, że na początku rozdziału zażyczyliśmy sobie, aby nasz program wyglądał tak:
var ps = new ParticleSystem(new PVector(width/2, 50));

draw = function() {
  background(0, 0, 0);
  ps.run();
};
Weźmy program napisany powyżej i sprawdźmy, jak będzie pasował do obiektu ParticleSystem.
Oto co mieliśmy wcześniej - zwróć uwagę na pogrubione linie:
var particles = [];

draw = function() {
  background(133, 173, 242);
  particles.push(new Particle(new PVector(width/2, 50)));

  for (var i = particles.length-1; i >= 0; i--) {
    var p = particles[i];
    p.run();
    if (p.isDead()) {
      particles.splice(i, 1);
    }
  }
};
Oto jak przepisać to w obiekt - stworzymy tablicę particles jako pole obiektu, dodając metodę addParticle do dodawania nowych cząsteczek, a logikę działania cząsteczek w run:
var ParticleSystem = function() {
  this.particles = [];
};

ParticleSystem.prototype.addParticle = function() {
  this.particles.push(new Particle());
};

ParticleSystem.prototype.run = function() {
  for (var i = this.particles.length-1; i >= 0; i--) {
      var p = this.particles[i];
      p.run();
      if (p.isDead()) {
        this.particles.splice(i, 1);
      }
    }
};
Możemy również dodać parę nowych funkcji do samego systemu cząsteczek. Dla przykładu przydatne może być pozwolenie obiektowi ParticleSystem przechowywać informacje o punkcie, gdzie tworzone są cząsteczki. To pasuje do pomysłu, by system cząstek był “emiterem”, miejscem gdzie cząstki są tworzone i wysyłane w świat. Punkt początkowy powinien być zainicjowany w konstruktorze.
var ParticleSystem = function(position) {
  this.origin = position.get();
  this.particles = [];
};

ParticleSystem.prototype.addParticle = function() {
  this.particles.push(new Particle(this.origin));
};
Oto, jak wygląda nasz kod w całości: