Pong-peli, vaihe 3

Tämä on Pong-pelin tutoriaalin osa 3/7. Tämän vaiheen aikana

  • Jaetaan ohjelma pienempiin palasiin (aliohjelmiin)
  • Lisätään peliin maila (jota ei voi vielä liikuttaa)

Vaihe on melko pitkä, mutta sitäkin tärkeämpi, joten jaksathan lukea ohjeet huolellisesti loppuun saakka.

1. Aliohjelman tekeminen

Lisäämme pian pelikenttään lisää olioita, tällä kertaa mailan. Jotta koodi pysyisi hallittavan kokoisissa palasissa, teemme kentän luomisesta oman aliohjelman nimeltä LuoKentta.

Aliohjelma on pienempi osa koodia, jota voidaan kutsua jostain muusta kohtaa koodia. Aliohjelman tarkemman luonteen mukaan käytetään myös nimityksiä funktio (jos aliohjelma palauttaa arvon) tai metodi (jos aliohjelma tarvitsee luokan olio-viitettä this).

Tässä luvussa aliohjelma on yleisnimi kaikille eri tyyppisille aliohjelmille. Tämän luvun eri tyyppisiä aliohjelmia ovat:

  • metodi = aliohjelma jolla on käytössä itse olioon (tässä luvussa itse peliin) liittyvä viite nimeltä this. Oletkin jo nähnyt Begin-metodin. Metodin esittelyssä ei ole static-sanaa.
  • funktio = aliohjelma joka palauttaa jonkin arvon. Ihannetilanteessa funktio ei tarvitse muuta tietoa kuin parametrinsa ja silloin se voidaan sanoa static-avainsanalla staattiseksi. Koska se on olemassa ennen olioiden luomista, sillä ei voi olla käytössään this-viitettä ellei sitä viedä parametrina.

Kirjoita metodi LuoKentta. Aliohjelman koodi on seuraavassa merkattu vihreällä värillä. Muu koodi pitäisi olla jo koodissasi, joten sitä ei pidä kirjoittaa.

Kiinnitä erityisesti huomiota siihen, miten aaltosulut tulevat. Rivin void LuoKentta() jälkeen tulee yksi aaltosulku auki { ja toinen kiinni }. Niiden väliin kirjoitetaan LuoKentta-aliohjelmaan kuuluvat koodirivit.

public class Pong : PhysicsGame
{
    public override void Begin()
    {
        PhysicsObject pallo = new PhysicsObject(40.0, 40.0, Shape.Circle);
        pallo.X = -200.0;
        pallo.Y = 0.0;
        pallo.Restitution = 1.0;
        this.Add(pallo);

        this.Level.CreateBorders(1.0, false);
        this.Level.Background.Color = Color.Black;

        this.Camera.ZoomToLevel();

        Vector impulssi = new Vector(500.0, 0.0);
        pallo.Hit(impulssi * pallo.Mass);

        Keyboard.Listen(Key.Escape, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
    }
    
    
    private void LuoKentta()
    {
    }
}


Aliohjelman esittelyrivin alussa, ennen sen nimeä, kerrotaan

  • kuinka julkinen aliohjelma on
  • onko aliohjelmassa käytössä olioviite this (jos ei lue mitään, on, jos lukee static ei ole).
  • minkä tyyppistä tietoa aliohjelma palauttaa.

Jos metodi ei ole tarkoitettu muiden käytettäväksi, sen näkyvyys kannattaa olla private. Eihän muissa peleissä luoda samanlaista kenttää.

this-viite: Edellä olevaan koodiin on lisätty this-viitteitä niihin kohti joissa sen oikeasti pitäisi olla. Eli olemme nimenomaan lisäämässä palloa meidän peliimme. Ja siksi se sanotaan this.Add(pallo). Taso on meidän pelimme taso, samoin kuin kamera. C#-koodissa this-viitettä ei ole pakko kirjoittaa ja siksi se yleensä jätetään koodista pois. Tämä voi sitten vähän hämätä koska sama rivi voi olla toimimatta paikassa missä this-viitettä ei ole (static-aliohjelmat).

Kentän luomisessa haluamme viitata pelin Level ja Camera-olihion, joten tarvitsemme siellä this-viitettä. Siksi static-sana jätetään pois. Toinen mahdollisuus olisi tuoda peli parametrina, kuten myöhemmin tehdään PiirraPallo tapauksessa.

Koska metodi LuoKentta ei palauta mitään arvoa, tyypin kohdalle tulee vain sana void. Aliohjelman nimen jälkeen tulee sulut, joiden väliin tulee mahdolliset parametrit (joita tässä ei ole vielä yhtään).

Nyt meillä on tyhjä aliohjelma, jonne haluaisimme siirtää kaikki kentän luomiseen liittyvät koodirivit.

2. Aliohjelman kutsuminen

Aliohjelma ei itsessään tee mitään ennen kuin sitä kutsutaan jostakin.

Aliohjelman kutsuminen tarkoittaa, että tietokonetta käsketään suorittamaan aliohjelmaan kuuluvat koodirivit.

Mistä voisimme kutsua tuota aliohjelmaa? Tietysti Begin-metodista, jota olemme edellisissä vaiheissa tehneet. Tuo metodihan suoritetaan ensimmäisenä kun peli käynnistetään.

Siirrä kentän luontiin liittyvät rivit Begin:ista metodiin LuoKentta alla olevan kuvan osoittamalla tavalla. Eli leikkaa punainen koodi pois ja liitä se vihreäksi merkittyyn kohtaan LuoKentta-metodin aaltosulkujen väliin.

Muista että Ctrl+X leikkaa valitun tekstin ja Ctrl+V liitää sen kursorin kohdalle.

Huomaa, että omassa koodissasi rivit saattavat olla hieman eri järjestyksessä kuin kuvassa.

Pallon luominen ja pallon liikuttamiseen liittyvät kaksi riviä (vektorin luominen ja pallon töytäisy) ja lopetusnapin tekevä rivi jäävät Begin-metodiin ja kaikki muut rivit siirretään uuteen LuoKentta-metodiin.

public class Pong : PhysicsGame
{
    public override void Begin()
    {
        PhysicsObject pallo = new PhysicsObject(40.0, 40.0, Shape.Circle);
        pallo.X = -200.0;
        pallo.Y = 0.0;
        pallo.Restitution = 1.0;
        Add(pallo);
        Level.CreateBorders(1.0, false);
        Level.Background.Color = Color.Black;

        Camera.ZoomToLevel();

        Vector impulssi = new Vector(500.0, 0.0);
        pallo.Hit(impulssi * pallo.Mass);

        Keyboard.Listen(Key.Escape, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
    }
    
    
    private void LuoKentta()
    {
        Level.CreateBorders(1.0, false);
        Level.Background.Color = Color.Black;

        Camera.ZoomToLevel();
    }
}


Kirjoitetaan sitten aliohjelmakutsu. Aliohjelman kutsu on käsky tietokoneelle käydä suorittamassa aliohjelmalle kuuluvat koodirivit.

Kutsu tapahtuu yksinkertaisesti kirjoittamalla aliohjelman nimi, jonka jälkeen tulee sulut ja sulkujen sisään mahdolliset parametrit (joita aliohjelmallamme ei ole yhtään) ja lopuksi puolipiste ;.

Kirjoita siirrettyjen rivien tilalle aliohjelman (metodin) LuoKentta kutsu kuten seuraavassa:

public class Pong : PhysicsGame
{
    public override void Begin()
    {
        PhysicsObject pallo = new PhysicsObject(40.0, 40.0, Shape.Circle);
        pallo.X = -200.0;
        pallo.Y = 0.0;
        pallo.Restitution = 1.0;
        Add(pallo);
        
        LuoKentta();
        
        Vector impulssi = new Vector(500.0, 0.0);
        pallo.Hit(impulssi * pallo.Mass);
        
        Keyboard.Listen(Key.Escape, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
    }
    
    
    private void LuoKentta()
    {
        Level.CreateBorders(1.0, false);
        Level.Background.Color = Color.Black;
        
        Camera.ZoomToLevel();
    }
}


Koska kirjoitimme aliohjelmakutsun ennen rivejä, joilla luodaan vektori nimeltä impulssi ja sysätään pallo liikkeelle, LuoKentta-aliohjelman rivit suoritetaan ennen impulssin luomista ja pallon liikuttamista. Pelimme toiminta ei siis muutu oikeastaan millään tavalla. Jäsentelemme vain koodia pienempiin osiin.

Pallon luomiseen voi kannattaa vielä lisätä rivit joilla pallon käytöstä saattaa voida "rauhoittaa". Lisää vaikka ennen Add-kutsua:

        pallo.KineticFriction = 0.0;
        pallo.MomentOfInertia = double.PositiveInfinity;

3. Pallo aliohjelmaksi

Pallon luomisesta haluamme tehdä sellaisen aliohjelman, jota voisivat muutkin halutessaan käyttää. Siksi sanomme että se on julkinen, public. Jos joku haluaa omaan peliinsä pallon, on meidän kerrottava mihin peliin se tulee. Ja sen jälkeen emme tarvitsekkaan aliohjelmassa this-viitettä, koska peli korvaa sen. Eli aliohjelma tulee pärjäämään omilla parametreillaan, siksi kirjoitetaan että se on aina olemassa, eli staattinen, static.

Kun pallo on luotu, voidaan sitä tarvita luomisen jälkeen. Siksi aliohjelmasta tehdäänkin funktio, joka palauttaa viitteen luomaansa palloon. Siksi annamme paluutyypiksi PhysicsObject. Aliohjelman parametreiksi viedään pallon keskipisteen haluttu x- ja y-koordinaatti.

Eli tee kuten edellä. Lisää vihreät rivit ja siirrä punaisella maalatut rivit LuoPallo-funktion sisälle. huomaa vaihtaa this tilalle peli.

public class Pong : PhysicsGame
{
    public override void Begin()
    {
        LuoPallo(this, -200, 0);
        PhysicsObject pallo = new(40.0, 40.0, Shape.Circle);
        pallo.X = x;
        pallo.Y = y;
        pallo.Restitution = 1.0;
        pallo.KineticFriction = 0.0;
        pallo.MomentOfInertia = double.PositiveInfinity;
        this.peli.Add(pallo);
        
        LuoKentta();
        
        Vector impulssi = new Vector(500.0, 0.0);
        pallo.Hit(impulssi * pallo.Mass);
        
        Keyboard.Listen(Key.Escape, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
    }
    
    
    private void LuoKentta()
    {
        Level.CreateBorders(1.0, false);
        Level.Background.Color = Color.Black;
        
        Camera.ZoomToLevel();
    }
    
    
    public static PhysicsObject LuoPallo(PhysicsGame peli, double x, double y)
    {
        PhysicsObject pallo = new PhysicsObject(40.0, 40.0, Shape.Circle);
        pallo.X = x;
        pallo.Y = y;
        pallo.Restitution = 1.0;
        pallo.KineticFriction = 0.0;
        pallo.MomentOfInertia = double.PositiveInfinity;
        peli.Add(pallo);
        return pallo;
    }
}

Huomaat että nyt aliohjelma (siis funktio) loppuu riviin, jossa palautetaan se tieto mitä kutsuva ohjelman osa siltä haluaisi. Eli

        return pallo

Kääntäjä valittaa nyt kuitenkin rivistä pallo.hit. Koska meillä ei nyt ole pallo-viitemuuttujaa. Siksi meidän pitää ottaa vastaan LuoPallo-funktion palauttama pallon viite, eli vaihda rivi muotoon:

        PhysicsObject pallo = LuoPallo(this, -200, 0);

Nyt meillä on jälleen palloviite käytössä.

Seuraavassa kuvassa on havannollistettu parametrin välistystä LuoPallo-funktioon ja miten se palauttaa luomansa pallon viitteen Begin-metodille.

# dipallo

4. Mailan lisääminen kenttään

Lisätään kenttään maila. Haluaisimme, että maila on paikallaan pysyvä eli staattinen vaikka pallo törmäilee siihen. Vaikka maila on "paikallaan pysyvä", voidaan sitä silti itse toki liikuttaa. Mutta törmäyksistä se ei liiku.

Staattisen fysiikkaolion luominen tapahtuu aliohjelmakutsulla

PhysicsObject.CreateStaticObject(leveys, korkeus, muoto)

Lisää vihreällä merkityt rivit LuoKentta-aliohjelmaan:

    private void LuoKentta()
    {
        PhysicsObject maila = PhysicsObject.CreateStaticObject(20.0, 100.0, Shape.Rectangle);
        maila.X = Level.Left + 20.0;
        maila.Y = 0.0;
        maila.Restitution = 1.0;
        Add(maila);

        Level.CreateBorders(1.0, false);
        Level.Background.Color = Color.Black;
        
        Camera.ZoomToLevel();
    }


Pong-pelissä on maila sekä ruudun vasemmassa että oikeassa reunassa. Lisätään oikeanpuoleinen maila myöhemmin.

Y-koordinaatin asetamme nollaksi, jotta maila menee pystysuunnassa ruudun keskelle.

X-koordinaattia varten kysymme pelikentältä sen vasemman reunan x-koordinaatin (Level.Left) ja lisäämme siihen 20.0, jotta maila pysyy kentän rajojen sisällä.

Olisimmeko voineet sijoittaa x-koordinaattiin yksinkertaisesti jonkun arvon, esimerkiksi -300.0? Olisimme toki. Äsken käyttämämme tapa on kuitenkin siitä parempi, että maila tulee aina kentän vasempaan reunaan vaikka päättäisimme myöhemmin muuttaa kentän kokoa. Näin peli on helpommin muokattavissa.

Laitamme myös mailalle Restitution-ominaisuuden arvoon 1.0, koska törmäykseen vaikuttaa kummankin törmäävän kappaleen ominaisuudet.

Kun nyt käynnistät pelin, siinä näkyy pallo sekä yksi maila.

5. Pelin aloittaminen

Kentän luomisen lisäksi pelissä on monia muita toimenpiteitä, jotka voisimme tehdä omina aliohjelminaan. Kun pelitilanne on muuten alustettu, voidaan aloittaa peli, joka tämän pelin tapauksessa tarkoittaa pallon laittamista liikkeelle. Tehdään aliohjelma, joka aloittaa pelin.

Aliohjelmien keskinäisellä järjestyksellä ei sinänsä ole merkitystä, kunhan ne ovat toistensa ulkopuolella (eli muiden aliohjelmien aaltosulkujen ulkopuolella) mutta luokan (eli äärimmäisten aaltosulkujen) sisäpuolella.

Lisää koodiisi uusi aliohjelma AloitaPeli koko ohjelman viimisen sulun yläpuolelle.

    private static void AloitaPeli(PhysicsObject pallo)
    {
    }

Tässä aloita pelille tuodaan parametrina mikä pallo pistetään liikkeelle. Ja koska mitään muuta tietoa ei tarvitakkaan, voidaan aliohjelma sanoa staattiseksi.

Siirretään koodirivejä aliohjelmasta toiseen samoin kuin kentän luomisen yhteydessä.

Siirrä pelin aloitukseen liittyvät rivit (Vector impulssi ja pallo.Hit) Begin-metodista AloitaPeliin:

    private static void AloitaPeli(PhysicsObject pallo)
    {
        Vector impulssi = new Vector(500.0, 0.0);
        pallo.Hit(impulssi * pallo.Mass);
    }

Lisää Begin-aliohjelmaan siirrettyjen rivien tilalle AloitaPeli-aliohjelmakutsu:

    public override void Begin()
    {
        PhysicsObject pallo = LuoPallo(this, -200, 0);
        
        LuoKentta();
        AloitaPeli(pallo);
    
        Keyboard.Listen(Key.Escape, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
    }

Huomaa, että koodin voi jakaa aliohjelmiin monin eri tavoin, tämä on vain yksi tapa.

6. Lopputulos

Näiden muutosten jälkeen luokka Pong eli Pong-pelimme on tämän näköinen.

Huomaa että pallo saattaa kimmota seinästä oudolla kulmalla tai hidastua osuessaan mailaan. Et ole tehnyt mitään väärin, vaan vika on Jypelin käyttämässä fysiikkamoottorissa jota ei ole tarkoitettu tämän tyylisille peleille.

using Jypeli;

namespace Pong;

/// @author vesal
/// @version 20.09.2024
/// <summary>
/// Peli jossa kaksi palaajaa yrittää saada pallon toisen päätyyn. 
/// </summary>
public class Pong : PhysicsGame
{
    public override void Begin()
    {
        PhysicsObject pallo = LuoPallo(this, -200, 0);
        
        LuoKentta();

        AloitaPeli(pallo);
        
        Keyboard.Listen(Key.Escape, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
    }
    
    
    private void LuoKentta()
    {
        PhysicsObject maila = PhysicsObject.CreateStaticObject(20.0, 100.0, Shape.Rectangle);
        maila.X = Level.Left + 20.0;
        maila.Y = 0.0;
        maila.Restitution = 1.0;
        Add(maila);

        Level.CreateBorders(1.0, false);
        Level.Background.Color = Color.Black;
        
        Camera.ZoomToLevel();
    }
    
    
    public static PhysicsObject LuoPallo(PhysicsGame peli, double x, double y)
    {
        PhysicsObject pallo = new PhysicsObject(40.0, 40.0, Shape.Circle);
        pallo.X = x;
        pallo.Y = y;
        pallo.Restitution = 1.0;
        pallo.KineticFriction = 0.0;
        pallo.MomentOfInertia = double.PositiveInfinity;
        peli.Add(pallo);
        return pallo;
    }

    
    private static void AloitaPeli(PhysicsObject pallo)
    {
        Vector impulssi = new Vector(500.0, 0.0);
        pallo.Hit(impulssi * pallo.Mass);
    }
}

Huomaa että on hyvä tapa kirjoittaa pari tyhjää riviä aliohjelmien välille.

Vastaus kysymykseen

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