Asteroids-pelin ohjelmointi

[PELI EI OLE VIELÄ IHAN VALMIS, MUTTA JOS HALUAT KOKEILLA KEHITYSVERSIOTA, NIIN TÄSTÄ PÄÄSEE PELAAMAAN & TESTAAMAAN!]

Tee-se-itse-miehenä koodasin viime viikolla nettiselaimessa toimivan Minesweeper-pelin. Tällä viikolla harrasteprojektina oli mennä ajassa taaksepäin vuoteen 1979 ja Atarin legendaarisen Asteroids-pelin kimppuun.  Halusin tehdä pelin mahdollisimman pitkälle vektorigrafiikalla, aivan kuten esikuvansa.

Asteroids-pelin idea on yksinkertainen: ohjaat avaruusalusta, jolla ammutaan tohjoksi sua lähestyviä asteroideja ja vihaisia ufoja. Mitä enemmän saat tuhoa aikaan, sitä suuremmalla pistemäärällä sut palkitaan. Pelin syvimmät arvot eivät sovellu sellaisenaan toimivan yhteiskunnan peruskiveksi, mutta tämä on rentouttavaa viihdettä, jossa ketään ei oikeasti vahingoiteta.

Koodausalustana käytän Minesweeperin tapaan p5*js-kirjastoa, jolla grafiikat saa loihdittua helposti nettisivun HTML5 canvas-alueelle. Alustus on  yksinkertaista; tarvitaan vain kaksi Javascript-funktiota eli setup() ja draw(). Loput ovat kuorruttamista.

function setup() {
  createCanvas( 800, 400 );
  frameRate( 30 );
}
function draw() {
   // Tätä funktiota kutsutaan nyt 30 kertaa sekunnissa.
}

 

Setup()-funktiolla luodaan piirtoalue, jossa peli toimii. p5*js-kirjasto kutsuu draw()-funktiota automaattisesti halutuin aikavälein (yllä asetettu 30 kertaa sekunnissa) ja sen sisällä piirretään pelialue aina puhtaalta pöydältä uusiksi.

Ääretön avaruus

Asteroids-pelissä pelaaja, asteroidit ja ammukset pullahtavat reunan yli jouduttuaan toiselle puolelle ruutua.

if (this.x > space_w) this.x = 0;
if (this.y > space_h) this.y = 0;
if (this.x < 0) this.x = space_w;
if (this.y < 0) this.y = space_h;

Vektorigrafiikan tekeminen

Piirsin pelin grafiikat muuten kokonaan katsomalla Youtubesta alkuperäistä Asteroids-peliä ja kuvittelemalla pelihahmot 12×12 kokoiseen ruudukkoon. Kuvittelin mielessäni asteroidin ja syötin viivapiirroksena sen koordinaatit Javascript-taulukkoon tällä tavalla:

// Asteroidien vektorigrafiikka
asteroid_shapes[0] = [
 4,0,
7,1,
 8,0,
 10,0,
 10,2,
 12,3,
 12,5,
 10,6,
 12,9,
 9,12,
 2,12,
 0,9,
 1,7,
 1,5,
 0,4,
 0,2,
 4,0
];
Piirtäminen ruudulle tuon taulukon perusteella tapahtuu näin:
    pixel( shape_coordinate ) {
       return (shape_coordinate-6 ) * this.size;
    }
    for (var i=0; i < this.shape.length-2; i+=2) {
            line (
                this.pixel( this.shape[i] ),
                this.pixel( this.shape[i+1] ),
                this.pixel( this.shape[i+2] ),
                this.pixel( this.shape[i+3] )
            );
        }
pixel()-funktio skaalaa taulukon arvot 0-12 sopivan kokoisiksi x,y-koordinaateiksi vastaamaan ruudulla olevia pikseleitä. Ennen viivojen piirtämistä piirtokoordinaatiston origo on siirretty asteroidin paikalle ruudulla.
Pelaajan avaruusaluksen ja asteroidit pystyin piirtämään yhdellä, yhtenäisellä viivalla, mutta ufo vaati monta erillistä viivataulukkoa. Tein koordinaatit ottamalla alkuperäisestä Asteroids-pelistä kuvakaappauksen tilanteesta, jossa ufo näkyy ruudulla. Otin ufon lähempään tarkasteluun PhotoShoppiin, jotta pystyin tarkastelemaan sen yksittäisiä pikseleitä:
Päätin käyttää ufon koordinaatistona 30 x 24 kokoista taulukkoa. Hahmottelin vektorigrafiikkaufon tutkimalla yllä olevassa kuvassa olevia x- ja y-koordinaatteja. Syötin viivojen päätepisteet taulukkoon seuraavasti:
     this.shape[0] = [ 0,10, 13,6, 17,1, 24,0, 31,1, 35,6, 47,10 ];
        this.shape[1] = [ 5,9, 24,9, 42,9 ];
        this.shape[2] = [ 0,10, 12,17, 24,17, 36,17, 47,10];
Lopputulos kuvaruudulta kaapattuna:
Ufon piirtorutiini:
    draw() {
        push();
        stroke(”#ddd”);
        strokeWeight(1);
        translate( this.x, this.y );
        // Piirrä ufo
        for (var sh=0; sh < this.shape.length; sh++) {
            var s = this.shape[sh];
            for (var i=0; i <= s.length -2; i+=2) {
                line ( this.pixel(s[i]),  this.pixel(s[i+1]), this.pixel(s[i+2]),  this.pixel(s[i+3] ));
            }
        }
        pop();
    }
Törmäysten tarkastusta varten tein ufon ympäri menevän viivan:
        this.collision_shape = [0,20, 29,0, 58, 20, 41,30, 16,30,0,20];// Yksinkertainen ufon muoto törmäyksien tarkastamiseen

Törmäyksen tutkiminen

Ensimmäisessä versiossa tutkin kappaleiden välistä törmäystä käyttämällä yksinkertaista etäisyyden mittaamista kappaleen keskeltä. Pythagoran lausetta ei olekaan tullut käytettyä sitten kouluaikojen… 🙂 Tämä ei kuitenkaan tyydyttänyt täysin minua, joten halusin tutkia törmäykset tarkemmin kappaleiden ääriviivojen tarkkuudella.

Löysin netistä Javascript-funktion, joka palauttaa true tai false sen mukaan, risteävätkö kaksi viivaa keskenään. Pelaajan törmäyksen asteroidiin tarkastan niin, että tutkin, leikkaako jompi kumpi avaruusaluksen kahdesta pisimmästä sivusta  minkä tahansa asteroidissa olevan viivan kanssa.

 for (var i=0; i < this.shape.length-4; i += 2 ) {
            if ( lineIntersect(
                this.x + this.pixel( this.shape[i] ),
                this.y + this.pixel( this.shape[i+1]),
                this.x + this.pixel( this.shape[i+2]),
                this.y + this.pixel( this.shape[i+3]),
                player.x + player.pixel( player.shape[0] ),
                player.y + player.pixel( player.shape[1] ),
                player.x + player.pixel( player.shape[2] ),
                player.y + player.pixel( player.shape[3] )
            ) ||
            lineIntersect(
                this.x + this.pixel( this.shape[i] ),
                this.y + this.pixel( this.shape[i+1]),
                this.x + this.pixel( this.shape[i+2]),
                this.y + this.pixel( this.shape[i+3]),
                player.x + player.pixel( player.shape[2] ),
                player.y + player.pixel( player.shape[3] ),
                player.x + player.pixel( player.shape[4] ),
                player.y + player.pixel( player.shape[5] )
            )
            ) {
                return true;
            }
        }
Asteroidin ja ammuksen välisen törmäyksen tarkastan yksinkertaisemmin etäisyysmittauksella asteroidin keskipisteeseen ja ottamalla huomioon asteroidin keskimääräisen halkaisijan.

Äänet

Samplasin alkuperäisestä Atari Asteroids -pelistä kaikki pelin äänet. Editoin ja masteroin ne Adobe Auditionilla siisteiksi ja muunsin äänitiedostot nettiystävällisiksi mp3-tiedostoiksi.

 

Asteroidin räjähdysanimaatio

Asteroids-pelissä räjähdys on yksinkertainen partikkelien lentäminen satunnaiselta vaikuttaviin suuntiin räjähtävän kappaleen origosta.  Itse tekemässäni versiossa räjähdykset tallennetaan explosions[]-taulukkoon luomalla siihen Explosion-luokkia antamalla räjähdyksen x- ja y-koordinaatit sekä partikkelien lukumäärä. Pienet asteroidit tuottavat vähemmän partikkeleita kuin jättimöhkäleet.

Esim. luodaan 50 partikkelin räjähdys kohtaan x,y: explosions.push( new Explosion( x, y, 50 );

Tallensin partikkeleiden attribuutit yhteen luokan sisällä olevaan taulukkoon:
[0] x
[1] y
[2] dx
[3] dy
[4]  x (seuraava räjähdys)
[5] y (seuraava räjähdys)
jne.
x = räjähdyksen x-koorinaatti
y = räjähdyksen y-koordinaatti
dx = partikkelin nopeus x-akselilla (negatiivinen arvo = vasemmalle, positiivnen = oikealle)
dy = partikkelin nopeus y-akselilla (negatiivinen arvo = ylös, positiivnen = alas)
class Explosion {
    particles;
    particle_count;
    age;
    alive;
    constructor(x, y, particle_count) {
        this.age = 50;
        this.alive = true;
        this.particle_count = particle_count;
        this.particles = [];
        for (var i=0; i < this.particle_count-4; i+=4) {
            var dx = ( Math.random() * 7 )  -3.5;
            var dy = ( Math.random() * 7 ) -3.5;
            this.particles[i] = x;
            this.particles[i+1] = y;
            this.particles[i+2] = dx;
            this.particles[i+3] = dy;
        }
    }
    move() {
        if (!this.alive) return;
        for (var i=0; i < this.particle_count-4; i+=4) {
            this.particles[i] += this.particles[i+2];
            this.particles[i+1] += this.particles[i+3];
        }
        this.age–;
        if (this.age <= 0) {
            this.alive = false;
            console.log(”Räjähdys valmis.”);
        }
    }
    draw() {
        if (!this.alive) return;
        for (var i=0; i < this.particle_count-4; i+=4) {
            push ();
            ellipseMode(CENTER);
            fill ( 100+ this.age*2);
            ellipse ( this.particles[i], this.particles[i+1], 2+this.age/16);
            pop ();
        }
    }
}
Jokaisella päivityskerralla kunkin partikkelin x- ja y-koordinaatteja muuttaan muuttujien dx ja dy verran. Nuo arvot taas on arvottu satunnaislukugeneraattorilla räjähdyksen luomishetkellä.

Ufon ampuman ammuksen suunnan laskeminen

Taas paluu koulumatematiikkaan… Kun tiedän aluksen ja ufon x- ja y-koordinaatit ja haluan, että ufo tähtää keskelle pelaajan alusta, niin miten tämä lasketaan? Tarvitsen ammuksen nopeudelle deltaX ja deltaY -arvot, joilla sitä liikutetaan 30 kertaa sekunnissa ruudulla.
Kulman laskeminen tapahtuu Math.atan2()funktiolla, joka palauttaa kulman tasolla radiaaneina (0,0) ja (y, x) -pisteiden välillä.
       // Ammuksen suunta: ufosta pelaajaan päin
        var deltaX = Math.floor( ufo.x – player.x );
        var deltaY = Math.floor( ufo.y- player.y +40 ); // +40 tarvitaan, jotta keskipiste olisi aluksen keskellä eikä vasemmassa reunassa
        this.angle =  Math.atan2( deltaY, deltaX );  // tällä funktiolla lasketaan aluksen ja ufon välinen kulma ruuudulla
        this.angle += Math.random()*.1 -.05; // lisätään hieman satunnaisuutta lähtökulmaan
        this.dx = -this.speed * Math.cos( this.angle ); //  deltaX = luodin nopeus kertaa kulman kosini
        this.dy =- this.speed * Math.sin( this.angle ); // ja sama deltaY:lle siniä käyttäen