mgr inż. Wacław Libront * Bobowa 2017-2019

ZSO Bobowa, ul. Długoszowskich 1, 38-350 Bobowa, tel: 0183514009, fax: 0183530221, email: sekretariat@zsobobowa.eu, www: zsobobowa.eu

Lekcja 11

Gra LASER w JS

  1. gra
  2. szkielet
  3. kwadrat
  4. losowanie
  5. inicjowanie
  6. gwiazda
  7. laser
  8. strzelanie
  9. śmierć
  10. dzieci
  11. LASER

Gra komputerowa LASER
pomysł zaczerpnięto z http://www.crunchzilla.com/game-maven

Na środku ekranu kręci się laserowa baza (trzy obrócone kwadraty), z której (klikając myszką) możemy wypuszczać laserowe strzały. Z brzegów ekranu „płyną” w kierunku bazy wrogie (kwadratowe) okręty przeciwnika. Jeśli laserowa strzała trafi w kwadratowy okręt, ten zmniejsza się, rozbija na mniejsze kawałki i znika, a my otrzymujemy punkty. Jeśli wrogi obiekt trafi w bazę punkty się zmniejszają.

Gra zostanie napisana metodą obiektową.


Podstawa
Wykorzystujemy typowy program do animacji: rekurencja i dwie systemowe funkcje clearTimeout oraz setTimeout.

<canvas id="c1" width="500" height="500">
</canvas>
<script>
  var c = c1.getContext('2d')
  var w = c.canvas.width;
  var h = c.canvas.height;
  var SKOK=50;
  var CZAS;

  function ANIMACJA(){
    c.clearRect(0,0,w,h);
    c.strokeStyle="black";
    c.strokeRect(0,0,w,h);


    clearTimeout(CZAS);
    CZAS=setTimeout(ANIMACJA,SKOK);
  }
  ANIMACJA();
</script>

Obiekt KWA – kwadratowy wróg
Każdy obiekt-kwadrat KWA posiada typowe parametry: środek (x,y), bok (bok), kąt (kat), szybkość przesuwania w pionie i poziomie (dx,dy) oraz szybkość obrotu (dk). Metoda przerysuj oblicza nowe położenie i nowy kąt obrotu oraz sprawdza odbicia od brzegów. Na końcu metody wykonujemy od razu metodę rysuj – nie trzeba jej wtedy wykonywać podczas przerysowywania w animacji. Metoda rysuj, to kwadrat narysowany od środka.

// obiektowy kwadrat KWA wraz z metodami
// tworzymy kwadratowy laser i obiekty wroga
function KWA(x,y,bok,kat,kol,dx,dy,dk){
  //własności metody KWA
  this.x=x;
  this.y=y;
  this.bok=bok;
  this.kat=kat;
  this.kol=kol;
  this.dx=dx;
  this.dy=dy;
  this.dk=dk;
  // obliczanie położenie, obrotu, odbić
  //i przerysowywanie kwadratu wroga i lasera
  this.przerysuj=function(){
    var b2=this.bok/2;
    this.x=this.x+this.dx;
    this.y=this.y+this.dy;
    this.kat=this.kat+this.dk;
    if (this.x-b2 < 0 || this.x+b2 > h){
      this.dx=-this.dx;
      this.dk=-this.dk;
    }
    if (this.y-b2 < 0 || this.y+b2 > w){
      this.dy=-this.dy;
      this.dk=-this.dk;
    }
    if (this.kat>360){this.kat=0}
    //automatyczne przerysowanie
    this.rysuj();
  }
  // rysowanie
  this.rysuj=function(){
    var b2=this.bok/2;
    c.save();
    c.translate(this.x,this.y);
    c.rotate(this.kat);
    c.strokeStyle=this.kol;
    c.strokeRect(-b2,-b2,this.bok,this.bok);
    c.restore();
  }
}

Tworzenie – losowanie
Definiujemy pustą tablicę TKWA i do jej kolejnych elementów wstawiamy obiekty KWA za pomocą instrukcji new i push. Losujemy niezbędne parametry do zainicjowania kwadratowych obiektów. W pętli głównej animacji wystarczy wykonać przerysowanie wszystkich obiektów w tablicy, aby zostały narysowane animowane kwadraty - 5 okrętów wroga.

for (var i=0;i<ileKWA;i++){
  var x=Math.random()*(w-maxB)+maxB/2;
  var y=Math.random()*(h-maxB)+maxB/2;
  var kat=Math.random()*3;
  var kol="black";
  var dx=Math.random()*maxV-1;
  var dy=Math.random()*maxV-1;
  var dk=(Math.random()*maxK-1)*0.1;
  // tworzenie nowego i przypisanie do tablicy
  TKWA.push( new KWA(x,y,maxB,kat,kol,dx,dy,dk));
}  

//w głównej pętli animacji - rysowanie wrogów
for (var i=0;i<TKWA.length;i++){
  TKWA[i].przerysuj();
}  

Inicjowanie podczas kreowania
Prawdziwy obiekt powinien w swoim wnętrzu posiadać metodę, która potrafi utworzyć nowy losowy kwadrat. Przyjęło się, że takie inicjujące metody noszą nazwę init. Co należy zmienić w obiekcie KWA?

<canvas id="c1" width="500" height="500"></canvas>
<script>

var c = c1.getContext('2d')
var w = c.canvas.width;
var h = c.canvas.height;
var SKOK=50;
var CZAS;


// zmienne główne na początku
var TKWA=[];
var maxB=30;
var maxV=4;
var maxK=4;
var ileKWA=5;

// NOWY obiektowy kwadrat bez parametrów
function KWA(){
  //NOWA metoda init - losowanie i tworzenie
  this.init=function(){
    this.x=Math.random()*(w-maxB)+maxB/2;
    this.y=Math.random()*(h-maxB)+maxB/2;
    this.bok=maxB;
    this.kat=Math.random()*3;
    this.kol="black";
    this.dx=Math.random()*maxV-1;
    this.dy=Math.random()*maxV-1;
    this.dk=(Math.random()*maxK-1)*0.1;
  }
  this.init();

  // położenie
  this.przerysuj=function(){
    var b2=this.bok/2;
    this.x=this.x+this.dx;
    this.y=this.y+this.dy;
    this.kat=this.kat+this.dk;
    if (this.x-b2 < 0 || this.x+b2 > h){
      this.dx=-this.dx;
      this.dk=-this.dk;
    }
    if (this.y-b2 < 0 || this.y+b2 > w){
      this.dy=-this.dy;
      this.dk=-this.dk;
    }
    if (this.kat>360){this.kat=0}
    //automatyczne przerysowanie
    this.rysuj();
  }
  // rysowanie
  this.rysuj=function(){
    var b2=this.bok/2;
    c.save();
    c.translate(this.x,this.y);
    c.rotate(this.kat);
    c.strokeStyle=this.kol;
    c.strokeRect(-b2,-b2,this.bok,this.bok);
    c.restore();
  }
}

  //NOWE tworzenie nowych obiektowych wrogów
  for (var i=0; i<ileKWA; i++) {
    TKWA.push(new KWA());
  }
  
  function ANIMACJA(){
    c.clearRect(0,0,w,h);
    c.strokeStyle="black";
    c.strokeRect(0,0,w,h);
    
    //przerysowania wrogów
    for (var i=0;i<TKWA.length;i++){
      TKWA[i].przerysuj();
    }
       
    clearTimeout(CZAS);
    CZAS=setTimeout(ANIMACJA,SKOK);
  }
  ANIMACJA();
</script>

Obracająca się gwiazda
Na środku obszaru canvas kręci się gwiazda - trzy kwadraty, obrócone co 30 stopni. Metoda init w funkcji-obiekcie GWIAZDA inicjuje własności gwiazdy. Po zainicjowaniu od razu jest wywoływana this.init(). Metoda przerysuj zmienia kąt i rysuje gwiazdę this.rysuj(). Metoda rysuj rysuje gwiazdę w punkcie (x,y), trzy obrócone kwadraty. Po zdefiniowaniu obiektu GWIAZDA tworzymy zmienną obiektową GWI za pomocą instrukcji new. W części głównej ANIMACJA wystarczy wywołać metodę GWI.przerysuj().

//obiektowa laserowa gwiazda
function GWIAZDA(){
  //metoda init
  this.init=function(){
    this.x=w/2;
    this.y=h/2;
    this.bok=30;
    this.kat=0;
    this.kol="blue";
    this.dk=3;
  }
  // inicjowanie gwiazdy
  this.init(); 
  // obliczanie i przerysowanie
  this.przerysuj=function(){
    this.kat=this.kat+this.dk;
    this.rysuj();
  }
  // rysowanie gwiazdy
  this.rysuj=function(){
    var b2=this.bok / 2;
    var b=this.bok;
    c.save();
    c.strokeStyle=this.kol;
    c.translate(this.x,this.y);
    c.rotate(Math.PI * this.kat / 180);
    c.strokeRect(-b2, -b2, b, b);
    c.rotate(Math.PI * 30 / 180);
    c.strokeRect(-b2, -b2, b, b);
    c.rotate(Math.PI * 30 / 180);
    c.strokeRect(-b2, -b2, b, b);
    c.restore();
  }

  //tworzenie gwiazdy
  //zmienna obiektowa
  var GWI=new GWIAZDA();


  //pętla ANIMACJA
  // przerysowanie gwiazdy
  GWI.przerysuj();

Laserowy strzał
Laserowy strzał, to czerwona linia biegnąca od środka obracającej się gwiazdy do kursora myszki. Rysowana linia jest nową metodą strzel w obiekcie GWIAZDA. Rysujemy wtedy, gdy przycisk myszki jest wciśnięty MyszPrzycisk=true, gdy puścimy przycisk myszy - linia nie jest rysowana.

Myszka – Zdarzenia
Położenie myszki zapisywane jest w zmiennej var MYSZKA={x:0, y:0}. Zdarzenia związane z ruchem myszki, naciskaniem i zwalnianiem przycisku opisują zdarzenia przypisane do canvas: onmousemove, onmousedown, onmouseup.

// zdarzenia myszki na CANVAS
// położenie myszki
var MYSZKA = {x: 0, y: 0};
// początkowo myszka nie wciśnięta
var MyszPrzycisk = false;	
// ruch myszki
c.canvas.onmousemove =  function(e) {	
  MYSZKA.x = e.clientX - 8;
  MYSZKA.y = e.clientY - 8;
};  
// gdy przycisk myszki wciśnięty
c.canvas.onmousedown =  function(e) { 
  MyszPrzycisk = true; 
};
// gdy przycisk myszki zwolniony
c.canvas.onmouseup = function(e) { 
  MyszPrzycisk = false;
};


// NOWA metoda w obiekcie GWIAZDA
// laserowy strzał
this.strzel=function(){
  if (MyszPrzycisk){
    c.beginPath();
    c.moveTo(this.x, this.y);	
    c.strokeStyle="red";
    c.lineTo(MYSZKA.x, MYSZKA.y);	
    c.stroke();
  }
}

//POPRAWIONA metoda PRZERYSUJ w obiekcie GWIAZDA
this.przerysuj=function(){
  this.kat=this.kat+this.dk;
  this.rysuj();
  this.strzel();
}

Trafienie bazy – kwadrat uderzył w gwiazdę
Jak sprawdzić, czy gwiazda została trafiona przez jeden z kwadratów? Za każdym razem, gdy przerysowywany jest kwadrat trzeba będzie to sprawdzać w pętli przerysowującej kwadraty w animacji głównej. Ale jak to sprawdzić?

Obliczymy odległość pomiędzy kwadratem a bazą. Jeśli jest mniejsza niż suma połowy kwadratu i połowy bazy, to znaczy, że baza została trafiona. Nową metodę CzyTrafiona umieścimy w obiekcie GWIAZDA. Parametrem metody będzie kwadrat TKWA[i], który będzie "wrzucany" do funkcji celem zbadania w pętli przerysowującej. Jeśli gwiazda trafiona to metoda przyjmuje wartość TRUE, w przeciwnym wypadku FALSE. W pętli głównej animacji umieszczamy rysowanie punktacji. Na początku programu należy również umieścić deklarację zmiennej PUNKTY i przypisać do niej początkową wartość zero.

//zliczanie punktów - na początku programu
var PUNKTY=0;

//NOWA metoda w obiekcie GWIAZDA
//sprawdzanie trafienia kwadratu w gwiazdę
this.CzyTrafiona=function(obiekt){
  var sx=this.x-obiekt.x;
  var sy=this.y-obiekt.y;
  var dl=Math.sqrt(sx*sx+sy*sy);
  if (dl < this.bok/2 + obiekt.bok/2){
    return true
  }
  return false;
}

//POPRAWIONE rysowywanie wrogów w pętli ANIMACJA
for (var i=0;i<TKWA.length;i++){
  TKWA[i].przerysuj();
  //spradzanie trafiania
  if (GWI.CzyTrafiona(TKWA[i])) {
    PUNKTY=PUNKTY-10;;
  }
}    

//punkty - pętla ANIMACJA
c.fillText("punkty: " + PUNKTY, w * 0.8, 20);

Zestrzelenie wrogiego kwadratu
Jak sprawdzić, czy koniec laserowego strzału (położenie myszki) trafiło do wnętrza któregoś kwadratowego wroga? Za każdym razem, gdy strzelamy obliczamy odległość pomiędzy myszką, a środkiem kwadratu. Jeśli ta odległość jest mniejsza niż połowa boku, to znaczy, że strzał się udał. Zwiększamy PUNKTY, zmniejszamy bok trafionego kwadratowego wroga, a gdy zniknie, to rodzi się nowy. Całość w metodzie KWA.przerysuj.

Usuwanie wrogów z listy
Zmniejszenie wroga do zera, aby nie był widoczny, to jedno, ale dalej pozostają jego parametry w pamięci – trzeba je usunąć. Dodawanie na koniec listy realizowaliśmy za pomocą metody push, a usuwanie wybranego elementu metodą splice – główna pętla animacyjna. Wróg jest gotowy do usunięcia jeżeli jego bok zmniejszy się do zera.

Nowy wróg zawsze na brzegu
Nowy wróg powinien rodzić się losowo na brzegu. Jak to zrobić? Najpierw wylosujemy czy pojawi się na górnym-dolnym czy na dolnym-prawym brzegu: Math.random()<0.5 (połowa przypadków). Jeśli góra-dół, to współrzędną Y losujemy według starych zasad, a X losujemy do momentu, aż będzie na brzegu – pętla while. I podobnie w drugim przypadku. Polecenia umieszczamy w KWA.init.

//sprawdzanie zestrzelenia
//WSTAWIĆ do metody przerysuj obiektu KWA
if (MyszPrzycisk) {
  var dx = MYSZKA.x - this.x;
  var dy = MYSZKA.y - this.y;
  var d = Math.sqrt(dx*dx+dy*dy);
  if (d < b2 || d < 10) {
    PUNKTY=PUNKTY+1;
    this.bok=this.bok-2;
  }
}
if (this.bok <= 0) {
  this.init();  // nowy
}

//usuwanie wrogów z tablicy i z pamięci
//WSTAWIĆ do pętli ANIMACJA
for (i=TKWA.length-1; i>=0; i=i-1) {
  if (TKWA[i].bok <= 0) {
      TKWA.splice(i, 1);
  }
}

//WSTAWIĆ DO obiektu KWA metoda init
//nowy wróg zawsze losowany na brzegu
//albo góra dół
if (Math.random()<0.5) {
  //Y dowolny a X losowo na brzegu
  this.y=Math.random()*(h-maxB)+maxB/2;
  do {
    var xx=Math.random()*(w-maxB)+maxB/2;
  } while (xx> maxB && xx<w-maxB);
  this.x=xx;
} 
// albo prawy lewy
else {
  // X dowolny a Y losowo na brzegu
  this.x=Math.random()*(w-maxB)+maxB/2;
  do {
    var yy=Math.random()*(h-maxB)+maxB/2;
  } while (yy> maxB && yy<h-maxB)
  this.y=yy;
}

Dzieci
A gdyby tak po trafieniu wroga odrywały się od niego mniejsze wrogie kawałki? Tworzenie nowych potomków-dzieci powinno odbywać się w programie w tym miejscu, w którym sprawdzamy co się dzieje po wciskaniu przycisku myszki, czyli KWA.przerysuj.  Tworzymy nowy obiekt KWA dodajemy na listę za pomocą push.

Ale okazuje się, że każdy z tych kwadracików dziedziczy po obiekcie-rodzicu tworzenie nowego dużego wroga na brzegu po zestrzeleniu potomka! Jak odróżnić zestrzelenie rodzica od zestrzelenia potomka? Każdemu dziecku nadamy dodatkowy parametr DZIECKO, a gdy się rodzi nowy, sprawdzamy, czy nie jest to dziecko.

//POPRAWIONY warunek strzelania
//OBIEKT KWA metoda przerysuj
if (MyszPrzycisk) {
  var dx = MYSZKA.x - this.x;
  var dy = MYSZKA.y - this.y;
  var d = Math.sqrt(dx*dx+dy*dy);
  if (d < b2 || d < 10) {
    PUNKTY=PUNKTY+1;
    this.bok=this.bok-2;
    //tutaj rodzą się DZIECI
    if (this.bok > 10) {
      var kw = new KWA();
      kw.x = this.x + kw.dx * 5;
      kw.y = this.y + kw.dy * 5;
      kw.bok = 4;
      kw.DZIECKO = true;        
      TKWA.push(kw);
    }
  }
}  
  
// POPRAWIONE tworzenie nowego rodzica
//dziecko nie może tworzyć nowego wroga
//OBIEKT KWA metoda przerysuj
if (this.bok <= 0 && !this.DZIECKO) {
  this.init();

LASER
I na koniec cały program ze wszystkimi zmianami

<canvas id="c1" width="500" height="500">
</canvas>
<script>
  var c = c1.getContext('2d')
  var w = c.canvas.width;
  var h = c.canvas.height;
  var SKOK=50;
  var CZAS;
  var maxB=30;
  var maxV=4;
  var maxK=4;
// ***************************************************** KWADRAT
function KWA(){
  this.init=function(){
	// losowo na brzegach
    if (Math.random()<0.5) {
      this.y=Math.random()*(h-maxB)+maxB/2;
      do {
        var xx=Math.random()*(w-maxB)+maxB/2;
      } while (xx>maxB && xx<w-maxB);
      this.x=xx;
    } else {
      this.x=Math.random()*(w-maxB)+maxB/2;
      do {
        var yy=Math.random()*(h-maxB)+maxB/2;
      } while (yy>maxB && yy<h-maxB)
      this.y=yy;
    }

    this.bok=maxB;
    this.kat=Math.random()*3;
    this.kol="black";
    this.dx=Math.random()*maxV-1;
    this.dy=Math.random()*maxV-1;
    this.dk=(Math.random()*maxK-1)*0.1;
  }

  this.init(); 
  
  this.przerysuj=function(){
    var b2=this.bok/2;
    this.x=this.x+this.dx;
    this.y=this.y+this.dy;
    this.kat=this.kat+this.dk;
    // odbijanie od brzegów
	if (this.x-b2 < 0 || this.x+b2 > h){
      this.dx=-this.dx;
      this.dk=-this.dk;
    }
    if (this.y-b2 < 0 || this.y+b2 > w){
      this.dy=-this.dy;
      this.dk=-this.dk;
    }
    if (this.kat>360){this.kat=0}
	// trafienie w kwadrat
	if (MyszPrzycisk) {
      var dx = MYSZKA.x - this.x;
      var dy = MYSZKA.y - this.y;
      var d = Math.sqrt(dx*dx+dy*dy);
      if (d < b2 || d < 10) {
        PUNKTY=PUNKTY+1;
        this.bok=this.bok-2;
	// kwadrat rozpada się na mniejsze
	if (this.bok > 10) {
          var kw = new KWA();
		  kw.x = this.x + kw.dx * 5;
		  kw.y = this.y + kw.dy * 5;
		  kw.bok = 4;
		  kw.DZIECKO = true;        
		  TKWA.push(kw);
		}
      }
    }
    // nowy rodzi się gdy zmaleje do zera
    if (this.bok <= 0 && !this.DZIECKO) {
      this.init(); 
    }
	
    this.rysuj();
  }
  
  this.rysuj=function(){
    var b2=this.bok/2;
    c.save();
    c.translate(this.x,this.y);
    c.rotate(this.kat);
	c.strokeStyle=this.kol;
    c.strokeRect(-b2,-b2,this.bok,this.bok);
    c.restore();
  }
}

var ileKWA=10;
var TKWA=[];
for (var i=0; i<ileKWA; i++) {
  TKWA.push(new KWA());
}
// ***************************************************** KWADRAT

// ***************************************************** GWIAZDA
function GWIAZDA(){
  this.init=function(){
    this.x=w/2;
    this.y=h/2;
    this.bok=30;
    this.kat=0;
    this.kol="blue";
    this.dk=3;
	this.dx=0;
	this.dy=0;
  }
  //automatyczne inicjowanie własności gwiazdy
  this.init();

  this.przerysuj=function(){
	this.kat=this.kat+this.dk;
    this.rysuj();
	this.strzel();
  }
 
  this.rysuj=function(){
    var b2=this.bok / 2;
    var b=this.bok;
    c.save();
    c.strokeStyle=this.kol;
    c.translate(this.x,this.y);
    c.rotate(Math.PI * this.kat / 180);
    c.strokeRect(-b2, -b2, b, b);
    c.rotate(Math.PI * 30 / 180);
    c.strokeRect(-b2, -b2, b, b);
    c.rotate(Math.PI * 30 / 180);
    c.strokeRect(-b2, -b2, b, b);
	c.restore();
  }
  this.strzel=function(){
    if (MyszPrzycisk){
      c.beginPath();
      c.moveTo(this.x, this.y);	
      c.strokeStyle="red";
      c.lineTo(MYSZKA.x, MYSZKA.y);	
      c.stroke();
	}
  }
  // czy gwiazda trafiona przez kwadrat
  this.CzyTrafiona=function(obiekt){
    var sx=this.x-obiekt.x;
    var sy=this.y-obiekt.y;
    var dl=Math.sqrt(sx*sx+sy*sy);
    if (dl < this.bok/2 + obiekt.bok/2){
      return true
    } else { return false;}
  }
}

var GWI=new GWIAZDA();
// ***************************************************** GWIAZDA

// ***************************************************** MYSZKA
var MYSZKA = {x: 0, y: 0};
var MyszPrzycisk = false;	

c.canvas.onmousemove =  function(e) {	
  MYSZKA.x = e.clientX - 8;
  MYSZKA.y = e.clientY - 8;
};  
c.canvas.onmousedown =  function(e) { 
  MyszPrzycisk = true; 
};
c.canvas.onmouseup = function(e) { 
  MyszPrzycisk = false;
};
// ***************************************************** MYSZKA

var PUNKTY=0;
// ***************************************************** ANIMACJA
function ANIMACJA(){
    c.clearRect(0,0,w,h);
	c.strokeStyle="black";
    c.strokeRect(0,0,w,h);
    //rysowanie kwadratów
    for (var i=0; i<TKWA.length; i++){
      TKWA[i].przerysuj();
    //sprawdzamy vzy kwadrat trafił w gwiazdę
      if (GWI.CzyTrafiona(TKWA[i])) {
        PUNKTY=PUNKTY-10;;
      }
    }
    c.fillText("punkty: " + PUNKTY, w * 0.8, 20);
    //rysowanie gwiazdy
    GWI.przerysuj();
    //usuwanie z tabeli skasowane kwadraty
    for (i=TKWA.length-1; i>=0; i=i-1) {
      if (TKWA[i].bok <= 0) {
        TKWA.splice(i, 1);
      }
    }
    clearTimeout(CZAS);
    CZAS=setTimeout(ANIMACJA,SKOK);
}
ANIMACJA();
// ***************************************************** ANIMACJA
</script>

Szablon

<canvas id="c1" width="500" height="500">
</canvas>
<script>
  var c = c1.getContext('2d')
  var w = c.canvas.width;
  var h = c.canvas.height;
  var SKOK=50;
  var CZAS;

  function ANIMACJA(){
    c.clearRect(0,0,w,h);
    c.strokeStyle="black";
    c.strokeRect(0,0,w,h);


    clearTimeout(CZAS);
    CZAS=setTimeout(ANIMACJA,SKOK);
  }
  ANIMACJA();
</script>