Pelin elementteja

Olemme nyt tutustuneet hieman olio-ohjelmointiin. Lisätään nyt ohjelmaamme pari peleille tyypillistä elementtiä olioiden avulla.

Kaksi oliota

Haluamme nyt luoda uuden olion, jonka tarkoituksena on tulla pelaajahahmomme syömäksi. Tarvitsemme tähän kaksi asiaa. Ensinnäkin meidän on määriteltävä uusi luokka uudelle Nappula-oliotyypille. Toisekseen meidän on kehitettävä algoritmi, jonka avulla voimme päätellä, ovatko pelaaja ja syötävä nappula riittävän lähellä toisiaan, jotta nappula tulisi syödyksi.

Luodaan ensin uusi luokka Nappula avaamalla uusi välilehti ja määrittelemällä luokka sen sisään. 

// Nappula-luokan määrittely on aaltosulkeiden sisällä
class Nappula {   

// jokaisella Nappula-luokan oliolla on tiedossa omat x- ja y-koordinaattinsa
  float x;   
  float y;   

// tämä on konstruktori: sen sisältö suoritetaan aina kun 
// luodaan uusi Nappula-luokan olio
  Nappula(){    
    this.x = random(0, width);    // arvotaan uudelle oliolle sijainti
    this.y = random(0, height);   // this.x ja this.y viittaavat 
                                  // olion sisäisiin muuttujiin
  }

// määritellään piirra-metodin sisälle, miltä haluamme nappulan näyttävän
  void piirra(){  
    fill(255, 150, 0);
    ellipse(this.x, this.y, 20, 20);
  }
}

Huomaamme, että Nappula on hyvin samankaltainen luokka kuin Pelaaja. Myös uusi nappulaolio luodaan samaan tapaan:

Pelaaja pelaaja;
Nappula nappula;   // asetetaan olio globaaliksi muuttujaksi

void setup(){
  size(500, 500);
  pelaaja = new Pelaaja();
  nappula = new Nappula():    // luodaan uusi Nappula-luokan olio 
                              // ja asetetaan se nappula-nimiseen muuttujaan
}

void draw(){
  background(255);
  nappula.piirra();    // kutsutaan nappula-olion piirra-metodia
  pelaaja.piirra(); 
}

void keyPressed(){   
  if (keyCode == RIGHT){ 
    pelaaja.liiku(10, 0);
  } else if (keyCode == LEFT){
    pelaaja.liiku(-10, 0);
  } else if (keyCode == UP){
    pelaaja.liiku(0, -10);
  } else if (keyCode == DOWN){
    pelaaja.liiku(0, 10);
  }
}

Refaktorointi

Määritellään Pelaaja-luokassa uusi muuttuja, annetaan sille arvo konstruktorissa ja käytetään sitten tätä muuttujaa piirra-metodin sisällä.

class Pelaaja {  
  float x;  
  float y;   
  float koko;   // asetetaan Pelaaja-luokalle uusi muuttuja, koko

  Pelaaja(){   
    this.x = 50;    
    this.y = 50;
    this.koko = 50;   // asetetaan konstruktorissa hahmon kooksi 50
  }

  void piirra(){ 
    fill(255, 255, 0);
    // ruumiin koko
    arc(this.x, this.y, this.koko, this.koko, QUARTER_PI, TWO_PI-QUARTER_PI, PIE);  
    fill(0);
    // määritellään silmän sijainti ja koko suhteessa koko hahmon kokoon
    // silmän koko on 1/10 hahmon ruumiin koosta
    ellipse(this.x, this.y-(this.koko/5), this.koko/10, this.koko/10);    

  }

  void liiku(int suunta_x, int suunta_y){
    this.x += suunta_x;  
    this.y += suunta_y;   
  }
}

Nappula-luokan refaktorointi on hieman suoraviivaisempaa:

class Nappula {  
  float x; 
  float y;   
  float koko;    // asetetaan Nappula-luokan muuttujaksi koko

  Nappula(){ 
    this.x = random(0, width);   
    this.y = random(0, height); 
    this.koko = 20;    // kun luodaan uusi olio, annetaan sille koko konstruktorissa
  }

  void piirra(){ 
    fill(255, 150, 0);
    // käytetään uutta muuttujaa entisten 20 ja 20 tilalla
    ellipse(this.x, this.y, this.koko, this.koko);    
  }
}

Tekemämme muutos saattaa aluksi tuntua vähäpätöiseltä tai jopa turhalta, mutta ohjelman kasvaessa tällaisista rakenteellisista parannuksista on hurjasti hyötyä. Tällaisella rakenteella (koko-muuttujaa käyttämällä) voimme yhtä arvoa muuttamalla muuttaa olion kokoa, vaikka itse olio rakentuisi kuinka monesta erilaisesta ellipsistä, neliöstä tai viivasta.


Törmäysalgoritmi

Ongelman ratkaisussa kannattaa usein lähteä liikkeelle kynän ja paperin kanssa. Kun ongelman on ymmärtänyt ja hahmotellut edes jonkin ratkaisun paperille, on sen ohjelmointi huomattavasti helpompaa. Mitä vaikeampi ongelma, sitä suurempi aika ohjelmoinnista kuluu ajatellessa, paperille piirtäessä ja seinää tuijottaessa. Isompia kokonaisuuksia kannattaa pilkkoa mahdollisimman pieniksi yksittäisiksi ongelmiksi.

Lähdetään piirtelemään ja hahmottelemaan ongelmaamme. Meillä on siis kaksi ympyrää, ja haluamme tietää osuvatko ne toisiinsa.

Piirretään kuvaan pisteet kuvaamaan ympyröiden keskipisteitä ja viivat niiden välille kuvaamaan näiden pisteiden välistä etäisyyttä. Mitä muuta tiedämme ympyröistä?

Ympyröiden keskipisteiden sijainnin lisäksi meillä on tiedossa niiden säde. Tämän avulla voimme päätellä, törmäävätkö ympyrät. Algoritmimme oivallus piilee siinä, että kaksi ympyrää osuvat toisiinsa, jos niiden keskipisteiden välinen etäisyys on pienempi kuin kummankin ympyrän säteiden pituus yhteensä.

Voimme siis hahmotella algoritmimme seuraavasti:

  1. Lasketaan pisteiden välinen etäisyys d
  2. Lasketaan säteiden summa sateiden_summa
  3. Verrataan lukuja d ja sateiden_summa toisiinsa
    • Jos d on suurempi kuin sateiden_summa: ei törmäystä
    • Jos d on pienempi kuin sateiden_summa: törmäys

Algoritmin toteutus

Lähdetään nyt toteuttamaan algoritmiamme pala kerrallaan. Aloitetaan pelaajan ja nappulan välisen etäisyyden laskemisesta. Koska kyse on kahden pisteen etäisyydestä ja tiedämme näiden pisteiden koordinaatit, voimme hyvin käyttää pisteiden välisen etäisyyden laskukaavaa:

Luodaan uusi metodi tarkistaTormays, jonka sisällä tarkistus tapahtuu. Kutsutaan tätä metodia joka draw-kierroksella.

Pelaaja pelaaja;
Nappula nappula;  

void setup(){
  size(500, 500);
  pelaaja = new Pelaaja();
  nappula = new Nappula(): 
}

void draw(){
  background(255);
  nappula.piirra();  
  pelaaja.piirra(); 
  tarkistaTormays();   // tarkistetaan joka draw-kierroksella
                       // törmäävätkö pelaaja ja nappula
}

void keyPressed(){   
  if (keyCode == RIGHT){ 
    pelaaja.liiku(10, 0);
  } else if (keyCode == LEFT){
    pelaaja.liiku(-10, 0);
  } else if (keyCode == UP){
    pelaaja.liiku(0, -10);
  } else if (keyCode == DOWN){
    pelaaja.liiku(0, 10);
  }
}

void tarkistaTormays(){
  // lasketaan ensin erotukset x2-x1 ja y2-y1
  float etaisyys_x = pelaaja.x - nappula.x;
  float etaisyys_y = pelaaja.y - nappula.y;
  // ja lasketaan niillä etäisyys kuten kaavassa. 
  // sqrt = square root = neliöjuuri
  float etaisyys = sqrt((etaisyys_x*etaisyys_x)+(etaisyys_y*etaisyys_y));
}

Lasketaan seuraavaksi säteiden summa. 

void tarkistaTormays(){
  // lasketaan ensin erotukset x2-x1 ja y2-y1
  float etaisyys_x = pelaaja.x - nappula.x;
  float etaisyys_y = pelaaja.y - nappula.y;
  // ja lasketaan niillä etäisyys kuten kaavassa. 
  // (sqrt = square root = neliöjuuri)
  float etaisyys = sqrt((etaisyys_x*etaisyys_x)+(etaisyys_y*etaisyys_y));

  // koko-muuttujassa on ympyröiden halkaisija, 
  // joten jaetaan koko kahdella.
  float sateiden_summa = (pelaaja.koko/2) + (nappula.koko/2);
}

Ja lopuksi tehdään vielä vertailu if-lauseella.

void tarkistaTormays(){
  // lasketaan ensin erotukset x2-x1 ja y2-y1
  float etaisyys_x = pelaaja.x - nappula.x;
  float etaisyys_y = pelaaja.y - nappula.y;
  // ja lasketaan niillä etäisyys kuten kaavassa 
  // sqrt = square root = neliöjuuri
  float etaisyys = sqrt((etaisyys_x*etaisyys_x)+(etaisyys_y*etaisyys_y));

  // koko-muuttujassa on ympyröiden halkaisija, 
  // joten jaetaan koko kahdella.
  float sateiden_summa = (pelaaja.koko/2) + (nappula.koko/2);

  // jos kappaleet osuvat, tulostetaan teksti
  if (etaisyys < sateiden_summa){
    text("törmäys!", 50, 50);
  }
}

Nyt meillä on melko toimiva törmäyksenhavaitsja kahdelle ympyrälle. Se ei ole täydellinen, mutta aivan riittävä pelimme ensimmäiseen versioon. Mikäli haluat tutustua lisää erilaisiin törmäyksenhavaitsemisalgoritmeihin (esimerkiksi toteuttaaksesi neliöiden väliset törmäykset), Jeffrey Thompsonin nettikirja Collision Detection on koodiesimerkkeineen loistavaa luettavaa.