Tekoälyn toteuttaminen

Koko koodi-osiossa oletetaan, että aikaisemmat lisäosat on toteuttuna, mutta lisäosat voi tehdä missä vaan järjestyksessä.

Tässä osiossa toteutaan Pongiin yksinkertainen tekoäly. Tekoälystä tehdään sellainen, että se seuraa pallon liikkeitä mahdollisimman tarkasti, mutta maila liikkuu kuitenkin samalla nopeudella kuin pelaajan maila.

Tekoäly tehdään vasemmanpuoleiselle pelaajalle, mutta tässä tutoriaalissa ei toteuteta menua, josta voisi valita, käytetäänkö tekoälyä vai ihmispelaajaa. Tätä tullaan hallinnoimaan koodin puolelta.

Jotta tekoälyn laittaminen päälle ja pois päältä olisi mahdollisimman helppoa, lisää kooditiedoston yläreunaan uusi globaali muuttuja var tekoAlyPaalla = true.

Mailan liikuttamisen tarkkailun pitää tapahtua koko ajan, joten paivitys-aliohjelmaa täytyy muuttaa niin, että se osaa kutsua tekoälyn laskentaa.

Lisää tarkistaLiike(maila2, wButton, sButton);-rivin eteen ehtolause, joka tarkastelee, onko tekoäly päällä vai ei seuraavalla tavalla:

if (!tekoalyPaalla){
    tarkistaLiike(maila2, wButton, sButton);
}

Ehtolauseessa siis tarkastetaan, että mikäli tekoäly ei ole päällä, tehdään tarkistaLiike-aliohjelma. Ehtolauseen voisi kirjoittaa myös tekoalyPaalla == false ja se toimisi samoin.

Lisää vielä tähän ehtolauseeseen else-lohko. Tänne lisätään vielä kutsu tekoälyä käsittelevään aliohjelmaan. Muutosten pitäisi siis näyttää seuraavalta:

if(!tekoalyPaalla){
    tarkistaLiike(maila2, wButton, sButton);
}
else{
    kasitteleTekoaly(maila2, pallo, nopeus);
}

Seuraavaksi täytyy tehdä itse kasitteleTekoaly-aliohjelma, joka ottaa kolme parametria:

  • mailan, jota liikutetaan
  • kohteen, minkä mukaan tekoäly pyrkii liikkumaan
  • nopeus, jolla mailaa liikutetaan.

Muodosta runko aliohjelmalle seuraavan näköiseksi:

function kasitteleTekoaly(maila, kohde, liikutusNopeus){

}

Jos teit kosketuksella liikuttamislisäosan, tämä vaihe on hyvin samankaltainen kuin siellä.

Lisää aluksi muuttuja var suunta = 0.0; aliohjelmaan kasitteleTekoaly.

Seuraavaksi päätämme suunnan, johon mailaa on tarkoitus siirtää. Haluamme asettaa mailan nopeuden suunnan vastaamaan sitä, miten maila on suhteessa kohteeseen. Mikäli maila on kohteen yläpuolella, liikutetaan mailaa alaspäin ja muissa tilanteissa toiseen suuntaan. Lisää seuraavanlaiset rivit koodiin:

if (maila.y > kohde.y){
    suunta = -1.0;
}

Eli mikäli mailan y-koordinaatti on suurempi kuin kohteen y-koordinaatti, asetetaan suunta negatiiviseksi. Tämän jälkeen voidaan lisätä vielä else-lohko, jossa asetetaan suunta positiiviseksi:

else{
    suunta = 1.0;
}

Kun suunta on selvillä, on helppoa asettaa maila liikkumaan haluttuun suuntaan annetulla nopeudella. Se onnistuu seuraavanlaisella rivillä:

maila.body.velocity.y = suunta * nopeus;

Lisää tämä rivi ehtojen jälkeen

Aliohjelman kasitteleTekoaly pitäisi näyttää seuraavalta:

function kasitteleTekoaly(maila, kohde, liikutusNopeus){
    var suunta = 0.0;

    if (maila.y > kohde.y){
        suunta = -1.0;
    }
    else{
        suunta = 1.0;
    }
    maila.body.velocity.y = suunta * nopeus;
}

Voit testata, miten tekoäly toimii nyt.

Tekoäly jää hyvinkin "nykiväksi" tämän koodin jälkeen, joten lienee mielekästä tasoittaa sen liikettä. Voisimme tehdä niin, että tekoälyssä ei tehdä mitään, mikäli pallo on kohteen korkeudella. Pelistä saa kuitenkin mielenkiintoisemman tekemällä niin, että tekoäly "kiihdyttää", kun korkeusero kasvaa.

Mene vielä kasitteleTekoaly-aliohjelmaan. Muutamme vähän aliohjelman rakennetta, jotta tekoäly vaikuttaisi "fiksummalta". Kommentoi kaikki ehtorivit:

//if (maila.y > kohde.y){
//    suunta = -1.0;
//}
//else{
//    suunta = 1.0;
//}

Voime myös laskea suoraan suunnan käyttäen apuna mailan ja kohteen sijaintia. Tarkastellaan seuraavanlaista kuvaa. Sijainnin laskentaa Mikäli pallo on alempana ruudulla kuin maila, on sen y-koordinaatti suurempi. Kuvassa on esimerkkinä tilanne, kun pallon y on 400 ja mailan 200.

Laita kommentoitujen ehtolauseiden jälkeen seuraavanlainen rivi:

suunta = kohde.y - maila.y;

Tällä laskutoimituksella saamme suunnan suoraan laskettua koordinaattien erotuksen avulla.

Jos testaat peliä nyt, huomaat nopeasti, että maila ei vielä käyttäydy mielekkäästi. Maila pomppii valtavaa vauhtia ylös ja alas. Tämä johtuu siitä, että aiemmin suunta oli joko 1 tai -1 ja nyt suunta voi olla monta sataa. Voimme skaalata suunnan esimerkiksi jakamalla sen jollain luvulla. Kuitenkin, jos suunta on suurempaa kuin 1, pystyy tekoälyn liikkumaan nopeammin kuin itse pelaaja.

Testataan aluksi lisätä suunnan laskentaan kentän korkeudella jakaminen:

suunta = (kohde.y - maila.y) / korkeus;

Nyt maila liikkuu hallitusti, mutta on vähän liiankin hidas tarjotakseen mielekkään vastuksen. Tämä voidaan ratkaista pienentämällä jakajan suuruutta, jolloinka skaalaus toimii tehokkaammin. Muotoile laskutoimitus vielä seuraavalla tavalla:

suunta = (kohde.y - maila.y) / (korkeus * 0.5);

Kerroin 0.5 määrittää nyt, kuinka vaikea tekoälystä tulee. Mikäli se on hyvin suuri, reagoi tekoäly hyvinkin hitaasti pallon liikkeisiin. Mitä pienempi se on, sitä parempi tekoälystä tulee. Kuitenkin tämän seurauksena suunta saattaisi olla suurempi kuin 1.0. Lisää vielä seuraanvaliset rivit suunnan laskemisen perään:

suunta = Math.min(suunta, 1.0);
suunta = Math.max(suunta, -1.0);

Ensimmäisellä rivillä tarkistetaan, että suunta ei saa olla suurempi kuin 1.0 ja toisella rivillä tarkastetaan, että suunta ei saa olla pienempi kuin -1.0. Tämän jälkeen voit säätää kerrointa 0.5 löytääksesi sopivan haastavan tekoälyn peliin.

Esimerkiksi 0.75 kertoimena tekee tekoälystä surkean, 0.05 taas saa tekoälyn olemaan hyvinkin mahdoton voittaa.

Mikäli teit aiemman lisäosan, voit liikuttaa hiirellä vielä tekoälyä jossain määrin.

Koko koodi:

var tekoalyPaalla = true;
var leveys = 800;
var korkeus = 600;
var nopeus = 200;
var p1Pisteet = 0;
var p2Pisteet = 0;
var game = new Phaser.Game(leveys, korkeus, Phaser.CANVAS, 'pong', { preload: lataus, create: luonti, update: paivitys });
var tekstinTyyli = { font: "bold 32px Arial", fill: "#fff", boundsAlignH: "center", boundsAlignV: "middle" };

function lataus() {
}

function luonti() {
    game.physics.startSystem(Phaser.Physics.ARCADE);
    game.stage.backgroundColor = "#000000";
    salliKuuntelu();
    
    var kuva = luoPallonKuva(32,"#FFFFFF");
    pallo = luoOlio(leveys/2, korkeus/2, kuva);
    pallo.body.bounce.set(1);
    pallo.body.velocity = new Phaser.Point(150, 0);
    pallo.body.onWorldBounds = new Phaser.Signal();
    pallo.body.onWorldBounds.add(tormasiSeinaan);
    pallo.body.setCircle(32);
    
    var mailanKuva = luoKuva(32, 128, "#FFFFFF");
    maila1 = luoOlio(leveys - 32, korkeus/2, mailanKuva);
    maila1.body.immovable = true;
    
    maila2 = luoOlio(0 + 32, korkeus/2, mailanKuva);
    maila2.body.immovable = true;
    
    p1Pistenaytto = luoTeksti(leveys * 0.8, korkeus * 0.15, "0");
    p2Pistenaytto = luoTeksti(leveys * 0.2, korkeus * 0.15, "0");
}


function luoTeksti(x, y, teksti){
    var text = game.add.text(x, y, teksti, tekstinTyyli);
    
    return text;
}


function tormasiSeinaan(tormaaja, ylos, alas, vasen, oikea){
    if(vasen){
        tormaaja.body.position = new Phaser.Point(leveys/2, korkeus/2);
        p1Pisteet++;
        p1Pistenaytto.text = ""+p1Pisteet;
    }
    if(oikea){
        tormaaja.body.position = new Phaser.Point(leveys/2, korkeus/2);
        p2Pisteet++;
        p2Pistenaytto.text = ""+p2Pisteet;
    }
}


function salliKuuntelu(){
    cursors = game.input.keyboard.createCursorKeys();
    wButton = game.input.keyboard.addKey(Phaser.Keyboard.W);
    sButton = game.input.keyboard.addKey(Phaser.Keyboard.S);
}


function luoOlio(x, y, kuva){
    var olio = game.add.sprite(x,y, kuva);
    olio.anchor.x = 0.5;
    olio.anchor.y = 0.5;
    game.physics.enable(olio, Phaser.Physics.ARCADE);
    olio.body.collideWorldBounds = true;
    return olio;
}


function tarkistaLiike(maila, ylos, alas){
    if (ylos.isDown)
    {
        maila.body.velocity.y = -nopeus;
    } else if (alas.isDown){
        maila.body.velocity.y = nopeus;
    }
    else{
        maila.body.velocity.y = 0;
    }
}

function tarkistaOsoitin(osoitin, xRaja, yRaja, ekaLiikutettava, tokaLiikutettava, nopeus){
    if(osoitin.isDown){
        var suunta = 0.0;
        if (osoitin.position.y > yRaja){
            suunta = 1.0;
        }
        else{
            suunta = -1.0;
        }
        if(osoitin.position.x > xRaja){
            ekaLiikutettava.body.velocity.y = suunta * nopeus;
        }
        else{
            tokaLiikutettava.body.velocity.y = suunta * nopeus;
        }
    }
}


function kasitteleTekoaly(maila, kohde, liikutusNopeus){
    var suunta = 0.0;
    
    //if (maila.y > kohde.y){
    //    suunta = -1.0;
    //}
    //else{
    //    suunta = 1.0;
    //}
    suunta = (kohde.y - maila.y) / (korkeus * 0.01);
    suunta = Math.min(suunta, 1.0);
    suunta = Math.max(suunta, -1.0);
    maila.body.velocity.y = suunta * nopeus;
}


function paivitys() {
    game.physics.arcade.collide(pallo, maila1);
    game.physics.arcade.collide(pallo, maila2);
    
    
    tarkistaLiike(maila1, cursors.up, cursors.down);
    if(!tekoalyPaalla){
        tarkistaLiike(maila2, wButton, sButton);
    }
    else{
        kasitteleTekoaly(maila2, pallo, nopeus);
    }
    
    tarkistaOsoitin(game.input.mousePointer, leveys/2, korkeus/2, maila1, maila2, nopeus);
    tarkistaOsoitin(game.input.pointer1, leveys/2, korkeus/2, maila1, maila2, nopeus);
    tarkistaOsoitin(game.input.pointer2, leveys/2, korkeus/2, maila1, maila2, nopeus);
}

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.