
Tästä lähtee ohjelmoinnin perusteet osa 2! Tällä kertaa pureudutaan koodin osalta erityisesti olio-ohjelmointiin ja avataan lisää ohjelmoinnin käsitteitä, kulttuuria ja ajattelutapoja. Mutta ensin sana hyvin hyödyllisestä työkalusta.
Versionhallinta
Ennen kuin alat kirjoittamaan suuria peliprojekteja, on todella hyödyllistä opetella versionhallinnan (eng. version control) perusteet. Se on kuin aikakone koodillesi, ja ilman sitä joudut vaikeuksiin ennemmin tai myöhemmin. Tässä oppaassa käytämme Gitiä, joka on alan standardi (ja muuten suomalainen), ja GitHubia sen päällä. Versionhallintaan on useita muitakin vaihtoehtoja (esim. SVN, Perforce), mutta Git on todella hyvä, ilmainen vaihtoehto, jolla hallinnoidaan myös valtavia projekteja (esim. Linuxin kernel, Unreal Enginen lähdekoodi) ympäri maailmaa.
Miksi versionhallinta?
Kuvittele, että rakennat peliä. Koodaat uuden ominaisuuden, testaat sitä, ja kaikki toimii. Mahtavaa! Mikään ei takuulla tule menemään pieleen.
Sitten koodaat toisen ominaisuuden, ja yhtäkkiä mikään ei toimi enää. Et edes muista, mitä kaikkea ehdit muuttaa. Tai sitten innostut niin paljon, että poistat vahingossa jotain tärkeää, tai oma koodisi rikkoo kollegasi tekemän ominaisuuden.
Ilman versionhallintaa olet pulassa. Joudut ehkä käymään läpi koodirivi riviltä, yrittäen muistaa, mitä teit tai jopa pyytämään vanhaa versiota takaisin joltakin toiselta. Se on valtava ajanhukka ja täysin turhaa työtä.
Git ratkaisee tämän. Se tallentaa kaikki koodisi muutokset omiksi versioikseen. Se on kuin tallentaisit pelin tilanteen tasaisin väliajoin, mutta se tallentaa pelkän tilanteen sijaan myös tiedon siitä, mitä muutoksia teit ja milloin. Voit palata mihin tahansa aiempaan versioon koodistasi milloin haluat. Se on kuin ”kumoa”-painike, joka toimii kaikkien tekemiesi muutosten yli, viikkoja, kuukausia, tai vuosia myöhemmin. Tämä säästää sinulta lukemattomia tunteja päänraapimista, turhautumista ja mahdollisesti koko projektin pilaamisen.
Versionhallinnan edistyneempi käyttö mahdollistaa myös useiden ihmisten koodaamisen saman projektin parissa. Tämä onkin yksi sen käytetyimpiä ominaisuuksia.
Jos et käytä versionhallintaa, otat täysin turhia riskejä ja todennäköisesti haaskaat kallisarvoista aikaasi. Se on yksi ensimmäisistä ja tärkeimmistä asioista, jotka erottavat hyvän ohjelmoijan huonosta.
Repositoriot
Repositorio on käytännössä projektiesi ”koti”, eli erityinen kansio, jonka sisällä Git (tai muu versionhallintajärjestelmä) seuraa kaikkia tiedostojen muutoksia.
Repositorio sisältää kaikki projektisi tiedostot, kuten lähdekoodin, kuvat, äänitiedostot ja muut resurssit (joskin isojen tiedostojen kohdalla kannatta harkita, onko versionhallinta oikea paikka niille). Lisäksi se sisältää Gitin oman, piilotetun kansion (yleensä .git), joka tallentaa koko projektin muutoshistorian. Tämän historian ansiosta voit tarkastella vanhoja versioita, palata niihin, tai yhdistää eri kehityslinjojen muutoksia.
Kun luot repositorion esimerkiksi GitHubiin, se toimii koodiprojektisi pilvitallennustilana. Voit ”kloonata” sen paikalliselle koneellesi työskentelyä varten ja myöhemmin puskea (push) paikalliset muutoksesi takaisin pilveen, jotta ne ovat turvassa ja mahdollisesti muiden käytettävissä.
Eli lyhyesti:
- Sinulla saattaa olla jo valmiina omia kooditiedostoja koneellasi tai voit aloittaa puhtaalta pöydältä.
- Luot repositorion projektille, joka on siis säilytyslokero kaikille koodisi versiolle.
- Omalla koneellasi ja verkossa pidetään kloonattuna samat versiot tiedostoistasi. Yleensä työskentelet viimeisimmän version parissa.
- Kun teet muutoksia koodiisi, lähetät (push) muutokset repositorioon uuden version muodossa.
- Näin voit milloin tahansa palata tarkkailemaan vanhempaa versiota koodista.

Visual Studio ja GitHub
GitHubin käyttöön Windowsilla on muutamia vaihtoehtoja. Voit opetella käyttämään Gitiä komentoriviltä, mikä tarjoaa ylivoimaisesti eniten kontrollia, mutta on haastavampaa oppia, jos et ole tottunut komentorivimaailmaan. Jos käytät vain muutamia perustoimintoja, GitHubista on ladattavissa myös Gitille visuaalinen käyttöliittymä, GitHub Desktop, joka toimii hieman sähköpostiohjelman tavoin. Tässä oppaassa kuitenkin opettelemme käyttämään Git-toimintoja suoraan Visual Studion sisältä, koska sitä kautta nopeus ja tehokkuus paranevat. Suurin osa moderneista IDE:istä (ja monista muistakin työkaluista) sisältää sisäänrakennetun tuen Gitille.
Avaa projektisi Visual Studiolla. Jos se on Git-repositorion sisällä, Visual Studio tunnistaa sen automaattisesti. Näet alareunassa tai työkalupalkissa Git-valikon, josta voit suorittaa kaikki perustoiminnot.

Varmista ensin, että olet kirjautunut sisään GitHub-tilillesi Visual Studiossa. Tämä tehdään yleensä Git-valikosta tai oikean yläkulman profiilikuvakkeesta. Kirjautuminen sallii sinun pushata ja pullata muutoksia suoraan ilman erillistä todennusta.
Commitin tekeminen
Tehtyäsi muutoksia koodiin, voit tallentaa ne commitilla uudeksi versioksi paikalliselle levylle.
- Mene
Git Changes-ikkunaan (näkymä on oletusarvoisesti alareunassa tai löytyyView -> Git Changes-valikosta). Tässä ikkunassa näet listan muuttuneista tiedostoista. - Kirjoita commit-viesti ikkunan yläreunassa olevaan kenttään. Muista, että viestin on oltava lyhyt ja kuvaava. Esimerkki:
Lisätty pelaajan elämäpalkki ja vahingonottokyky. - Klikkaa Commit Staged -painiketta. Jos sinulla on useita tiedostoja, voit raahata haluamasi tiedostot Staged Changes -osioon ennen committaamista, mutta useimmiten Commit All (tai vain
Commit) on riittävä.
Muutosten siirtäminen eteenpäin
Kun olet tehnyt paikallisen commitin, se on vielä vain omalla koneellasi. Sen saamiseksi eteenpäin GitHubiin, sinun on pushattava se.
Git Changes-ikkunan oikeassa yläkulmassa on nuolipainike, joka osoittaa ylöspäin,Push.- Klikkaa sitä. Visual Studio lähettää paikalliset commitisi etärepositorioon GitHubiin.
Muutosten hakeminen
Jos työskentelet tiimin kanssa, on tärkeää saada kollegoiden tekemät muutokset omalle koneellesi. Tähän käytetään pull-komentoa.
Git Changes-ikkunan oikeassa yläkulmassa on nuolipainike, joka osoittaa alaspäin. Painike on merkittyPull.- Klikkaa sitä. Visual Studio hakee ja yhdistää (merge) kaikki etärepositorion muutokset omaan paikalliseen versioosi.
Huomio: Yksi yleisimmistä virheistä on unohtaa pullata ennen omien muutosten pushaamista. Jos joku on tehnyt muutoksia etärepositorioon ja sinä pushaat omasi, syntyy konflikti, koska Git ei tiedä, kummat muutokset ovat oikein. Muista siis aina pullaa ensin, sitten pushaa.
Versionhallinnan haasteet
Vaikka versionhallinta on elintärkeää, siinä on omat haasteensa.
Konfliktit: Jos useampi henkilö muokkaa samaa tiedoston osaa samaan aikaan, Git ei tiedä kumman muutokset ovat oikein. Silloin syntyy konflikti, joka pitää ratkaista käsin. Tämä voi olla aluksi hankalaa, mutta se on arkipäivää tiimityöskentelyssä. Tekstitiedostojen (kuten koodin) konfliktien kohdalla voidaan valita, mitkä rivit kustakin tiedostosta jäävät voimaan lopullisessa versiossa. Binääritiedostoissa (esim. exe, png) konfliktien ratkaisu on yleensä mahdotonta, jolloin muutoksista voidaan valita aina vain yksi kokonainen tiedosto.
Liian isot tiedostot: Git on suunniteltu pääasiassa tekstipohjaiselle koodille. Se ei ole paras ratkaisu isojen binaaritiedostojen (kuten kuvatiedostot, äänitiedostot, 3D-mallit) hallintaan, koska se tallentaa niistä jokaisen version erikseen, mikä kuluttaa paljon tilaa. Tätä varten on olemassa lisätyökaluja, kuten Git LFS (Large File Storage).
Huonot commit-viestit: Jos et kirjoita kuvaavia commit-viestejä, versionhallinnan hyödyt vähenevät dramaattisesti. Myöhemmin et muista, mitä ”korjattu bugi” tarkoitti.
Tämä on vasta jäävuoren huippu versionhallinnasta, mutta auttaa sinua pääsemään alkuun ammattimaisemmassa koodinhallinnassa.
Olio-ohjelmointi
Olio-ohjelmointi (Object-Oriented Programmin, OOP) on tapa jäsentää ohjelmia niin, että ne koostuvat objekteista eli olioista. Nämä oliot mallintavat todellisia asioita (vaikkapa pelimaailmassa) ja niillä on ominaisuuksia ja toimintoja. Esimerkiksi pelin vihollinen voi olla olio. Sen ominaisuuksia (muuttujia) ovat sen sijainti, elämäpisteet ja nopeus. Sen toimintoja (funktioita) taas voivat olla liikkuminen, hyökkääminen tai osuman ottaminen.
Luokka on kuin piirustus tai suunnitelma, jonka mukaan sentyyppisiä olioita rakennetaan. Yksi luokka voi määrittää, millaisia kaikkien vihollisolioiden ominaisuudet ja toiminnot ovat. Sen jälkeen voit luoda tästä luokasta monta eri vihollista pelimaailmaan, joilla jokaisella on omat arvonsa (esimerkiksi eri sijainti tai elämäpisteet), mutta ne toimivat samalla periaatteella.
Olio-ohjelmoinnin tärkein periaate on kapselointi. Kapselointi tarkoittaa sitä, että olion sisäiset tiedot eli ominaisuudet piilotetaan ulkopuolelta. Tämän ansiosta tietoja voidaan muuttaa vain olion omien, julkisten toimintojen kautta. Vihollisolion tapauksessa tämä tarkoittaisi esimerkiksi sitä, että sen elämäpisteitä ei voi muuttaa suoraan, vaan ainoastaan sen hyökkäys- tai osuma-toiminnon avulla.
Olio-ohjelmoinnissa on myös kaksi muuta tärkeää periaatetta:
Perintä mahdollistaa uusien luokkien luomisen, jotka perivät olemassa olevan luokan ominaisuudet ja toiminnot. Esimerkiksi voit luoda yleisen Vihollinen-luokan ja periyttää siitä kaksi uutta luokkaa: Goblin ja Peikko. Molemmat perivät Vihollinen-luokan ominaisuudet, kuten elämäpisteet ja nopeuden. Tämän lisäksi Goblin-luokalle voidaan lisätä uusi toiminto, kuten ”varasta”, ja Peikko-luokalle ”heitä kivi”. Tämä vähentää koodin toistamista ja tekee siitä helpommin hallittavan.
Polymorfismi tarkoittaa sitä, että samalla kutsulla voidaan suorittaa eri toimintoja riippuen siitä, mitä oliota kutsutaan. Esimerkiksi voit luoda kaikille vihollisille yhteisen Hyökkää-toiminnon, mutta jokainen vihollistyyppi toteuttaa sen omalla tavallaan. Goblin-luokan Hyökkää-toiminto voi olla ”lyö miekalla”, kun taas Peikko-luokan Hyökkää-toiminto voi olla ”murskaa mailalla”. Polymorfismi tekee koodista joustavampaa ja helpompaa laajentaa tulevaisuudessa.

Suorituskyky ja selkeys mielessä
Huomionarvoista on, että olio-ohjelmointi on ollut suuressa suosiossa viimeiset 30 vuotta, mutta sen aikana on myös alettu havaita useita sen ongelmakohtia. Monimutkaisissa järjestelmissä olio-ohjelmointi voi johtaa liialliseen abstraktioon ja monimutkaisiin luokkahierarkioihin, mikä tekee koodista vaikeasti ymmärrettävää ja ylläpidettävää.
Hullunkurinen esimerkki vaikeaselkoisesta abstraktiosta: vihollisen liike voisi olla abstrakti luokka Liike, joka perii luokan Perusliike ja joka periytyy edelleen luokille Kävely, Lentäminen ja Juokseminen. Samoin Vihollinen-luokka voisi periä luokat Hyökkäys, Puolustus ja Terveys. Tässä mallissa luodaan uusi alaluokka jokaiselle uudelle toiminnolle tai ominaisuudelle. Esimerkiksi Zombie-luokka ei peri suoraan Vihollinen-luokasta, vaan se perii HidastaKävelyä-luokasta, joka perii Kävely-luokan. Tämä taas perii Liike-luokan, joka perii Perusliike-luokan. Tämä luo syvän perintähierarkian, joka tekee koodin lukemisesta ja ymmärtämisestä vaikeaa. Tämän lisäksi uuden toiminnon, kuten Ryömintä, lisääminen vaatisi useiden uusien luokkien luomista ja hierarkian muokkaamista monimutkaisella tavalla. Proseduraalinen lähestymistapa olisi luoda yksi Vihollinen-luokka, jossa on yksinkertaisia metodeja, kuten kävele(), hyökkää() ja otaVahinkoa(damage).
Lisäksi olio-ohjelmoinnin piirteet, kuten kapselointi ja perintä, voivat luoda tiukkoja riippuvuuksia eri osien välille. Tämä tekee yksittäisten osien testaamisesta hankalaa ja muutosten tekemisestä riskialtista, koska yksi muutos voi aiheuttaa odottamattomia sivuvaikutuksia koko järjestelmässä. Tämän vuoksi kehittäjät ovat alkaneet painottaa uudelleen yksinkertaisempia malleja, kuten proseduraalista tai funktionaalista ohjelmointia, jotka keskittyvät selkeisiin toimintoihin ja tiedon käsittelyyn.
Olio-ohjelmointi voi täten aiheuttaa suorituskykyongelmia suurissa järjestelmissä muistin hallinnan ja prosessorin kuormituksen takia. Jokainen objekti vie muistia, ja monimutkaiset luokkahierarkiat voivat luoda valtavan määrän pieniä objekteja, jotka on luotava ja vapautettava dynaamisesti. Tämä voi johtaa roskienkeruun (garbage collection) aiheuttamiin viiveisiin, jotka pysäyttävät ohjelman suorituksen hetkellisesti. Esimerkiksi älytelevision toiminnot saattavat olla riippuvaisia monista perityistä luokista. ”Seuraava” -painikkeen painaminen saattaa käynnistää useita tapahtumankäsittelijöitä ja suurienkin objektien luomisia ja kirjastojen lataamisia eri toiminnallisuuksille, kuten mainoksen lataamiselle tai tilin varmistukselle, jolloin prosessori ja muisti ovat kuormitettuja ja toiminto ruudulla tapahtuu 1,5 sekunnin viiveellä, vaikka sen pitäisi olla reaaliaikaista.
Nämä ongelmat eivät itsessään johdu olio-ohjelmoinnista, vaan pikemminkin erilaisten arkkitehtuurisääntöjen seuraamisesta sokeasti. Jos tulet uutena koodaajana tiimiin mukaan ja sinun pitäisi alkaa välittömästi lisäämään tiukalla aikataululla uutta toiminnallisuutta miljoonien koodirivien sekaan, teet mitä pystyt. Kukaan tiimissä ei varmasti tiedä miten kaikki tuo koodi toimii, joten voit vain toivoa, että pohjalla oleva arkkitehtuuri on hyvä ja ladata mitä kirjastoja satut tarvitsemaan. Mitä enemmän koodaat, sitä syvempi ymmärrys sinulle syntyy koodin ongelmakohdista ja milloin abstraktio oikeasti auttaa sinua ja tiimikavereitasi luomaan helpommin hallittavan kokonaisuuden. Kaiken abstraktointi vakiona voi johtaa melkoiseen ylläpitosuohon.
Olio-ohjelmointi on yksi tapa ohjelmoida. Vasta-alkaja voi miettiä, että mikä on absoluuttisesti paras tapa ohjelmoida, mutta tällaiseen kysymykseen ei ole oikeaa vastausta. On vain eri tavoilla toimivia työkaluja ja periaatteita. Jos koodaat yksin, voit kokeilla eri työkaluja ja havainnoida, mikä toimii parhaiten missäkin tilanteessa ja mitkä asiat auttavat ja hidastavat sinua. Jos olet osana tiimiä, käytät todennäköisesti sitä työkalua, jonka joku toinen on aikaisemmin päättänyt.
Miten olio-ohjelmointi auttaa sinua?
Luokat ja oliot ovat C#:n perusta. Ne ovat työkaluja, joilla ohjelmoija voi järjestää koodinsa ja mallintaa monimutkaisia asioita, kuten pelimaailman hahmoja ja esineitä. Jos haluat tehdä pelin, jossa on enemmän kuin yksi vihollinen, luokkien ymmärtäminen on pakollista.
Kuvittele, että teet peliä ja haluat sinne kaksi vihollista: zombin ja luurangon. Jos et vielä osaa käyttää luokkia, joten teet asiat näin:
// Zombi int zombiHp = 100; string zombiNimi = "Zombi"; float zombiNopeus = 1.5f; // Luuranko int luurankoHp = 80; string luurankoNimi = "Luuranko"; float luurankoNopeus = 2.0f;
Tämä on täysin kestämätön tapa, koska joudut toistamaan samaa koodia yhä uudestaan. Mitä jos pelissä onkin 1000 vihollista? Mitä jos päätät, että kaikilla vihollisilla pitäisi olla uusi ominaisuus, kuten vaikkapa panssari? Joudut käymään läpi koko koodisi, lisäämään uuden muuttujan jokaiselle viholliselle erikseen (ja katsokin, että et tee yhtään virhettä). Tämä on DRY (Don’t Repeat Yourself, älä toista itseäsi) -periaatteen vastainen ja tekee koodista mahdottoman ylläpitää.
Hyvä ohjelmoija näkee pari vihollista koodattuaan, että zombilla ja luurangolla on paljon yhteisiä piirteitä: niillä on elämäpisteitä, nimi ja liikkumisnopeus. Näiden samanlaisten ominaisuuksien ja toimintojen avulla voidaan luoda luokka. Luokka on kuin piirustus tai suunnitelma siitä, millainen jokin asia on.
public class Vihollinen
{
// Luokan ominaisuudet (properties) eli muuttujat, jotka kuvaavat vihollista
public int elamapisteet;
public string nimi;
public float nopeus;
// Luokan metodi eli toiminto, jonka vihollinen voi tehdä
public void Liiku()
{
// Koodi, joka liikuttaa vihollista
}
}
Tässä koodi on järjestetty paremmin. Luokka Vihollinen on malli, joka määrittelee, mitä ominaisuuksia (elämäpisteet, nimi, nopeus) ja toimintoja (liiku) kaikilla vihollisilla on.
Nyt, kun haluat luoda konkreettisen vihollisen, luot luokasta olion. Olio on luokan esiintymä eli todellinen, toimiva versio suunnitelmasta.
// Luodaan uusi vihollinen-olio ja annetaan sille arvot Vihollinen zombi = new Vihollinen(); zombi.nimi = "Zombi"; zombi.elamapisteet = 100; zombi.nopeus = 1.5f; // Luodaan toinen, samasta luokasta tehty olio Vihollinen luuranko = new Vihollinen(); luuranko.nimi = "Luuranko"; luuranko.elamapisteet = 80; luuranko.nopeus = 2.0f; // Molemmat voivat tehdä saman toiminnon zombi.Liiku(); luuranko.Liiku();
Tässä tapauksessa zombi ja luuranko ovat olioita. Ne ovat erilaisia, koska niiden ominaisuuksilla on eri arvot, mutta ne ovat molemmat Vihollinen-tyyppiä ja niillä on samat ominaisuudet ja toiminnot. Huomaat koodista, että olioiden ominaisuuksia ja toimintoja käsitellään .-operaattorilla, esimerkiksi zombi.Liiku();
Jos päätät lisätä kaikille vihollisille panssarin, teet muutoksen vain kerran, Vihollinen-luokkaan, ja kaikki olemassa olevat ja tulevat viholliset saavat uuden ominaisuuden automaattisesti. Tällä tavalla koodisi pysyy siistinä ja helposti muokattavana.
Konstruktori: olion luominen siististi
Voimme vielä parantaa luokkien käyttöönottoa konstruktorin avulla. Konstruktori on erityinen metodi, joka ajetaan automaattisesti heti, kun luot uuden olion luokasta. Se on kätevä tapa antaa oliolle alkupisteet heti luontihetkellä.
public class Vihollinen
{
public int elamapisteet;
public string nimi;
public float nopeus;
// Konstruktori on metodi, jolla on sama nimi kuin luokalla
public Vihollinen(string alkunimi, int alkuelama, float alkunopeus)
{
nimi = alkunimi;
elamapisteet = alkuelama;
nopeus = alkunopeus;
}
// ...
}
Kaikilla metodeilla voi olla sulkeiden sisällä muuttujia. Niitä kutsutaan joko parametreiksi tai argumenteiksi riippuen kontekstista. Parametrit ovat muuttujia, jotka määritellään metodin otsikkorivillä. Ne toimivat paikkamerkkeinä arvoille, jotka metodi voi vastaanottaa. Argumentit ovat todellisia arvoja, jotka välitetään metodille, kun sitä kutsutaan. Arvot sijoitetaan parametrien paikoille. Voit siis itse määrittää, että minkätyyppistä dataa haluat metodin ottavan vastaan lisäämällä parametrimuuttujia sille. Ylläolevassa koodissa konstruktorimetodille on määritelty kolme parametria: string nimi, int elamapisteet, ja float nopeus. Kun olio luodaan, sen jäsenmuuttujiin sijoitetaan konstruktorin sisällä argumentteina saadut arvot (esim. Nimi = nimi).
Nyt, kun luot uuden olion, voit antaa sille arvot argumentteina heti:
// Täällä ei tarvitse asettaa muuttujia erikseen
Vihollinen zombi = new Vihollinen("Zombi", 100, 1.5f);
Vihollinen luuranko = new Vihollinen("Luuranko", 80, 2.0f);
Tämä tekee koodistasi vieläkin siistimpää ja helpompaa lukea. Kun näet new Vihollinen(…)-komennon, tiedät heti, että kyseessä on uusi vihollisolio, ja näet sen tärkeimmät ominaisuudet. Luokkien ja olioiden avulla koodaajaa autetaan ajattelemaan isompia kokonaisuuksia, ei vain yksittäisiä muuttujia.

Tarkista esimerkkikoodi GitHubista

On myös hyvä tietää, että luokilla on vakiona destruktori, jota kutsutaan kun olio tuhotaan. Voit tarvittaessa määritellä destruktorimetodin käyttäen aaltoviivaa alussa esim. nimellä ~Vihollinen().
~Vihollinen()
{
// Kirjoittaa poiston yhteydessä lokitiedostoon, että olio on poistettu
System.IO.File.AppendAllText("log.txt", "Vihollinen poistettu." + Environment.NewLine);
}
Harjoituksia
Harjoitus tekee mestarin!
SuomiGameHUBin GitHub-sivulla on harjoituksia omina kooditiedostoinaan. Voit kopioida harjoituksen koodin Visual Studioon ja koettaa suorittaa kommenteissa annetun tehtävän.
Kokeile, pystytkö tekemään SuomiGameHUBin GitHubissa olevat viisi oliotehtävää.
Näkyvyysmääritykset
Olemme törmänneet nyt useaan otteeseen C#-koodissa sanoihin public ja private. Ne ovat näkyvyysmäärityksiä (eng. access modifiers), jotka määrittelevät, kuka tai mikä saa käyttää tiettyä koodin osaa, kuten muuttujaa tai metodia. Aluksi tämä voi tuntua turhalta säännöltä, mutta sen tarkoitus on estää ongelmia ja helpottaa koodin hallintaa pitkällä aikavälillä.
Isoissa tiimeissä näkyvyysmääritykset estävät kaaosmaisen koodin. Kun 100 ihmistä työskentelee samassa projektissa, jokainen täytyy tietää, mitä osaa koodista saa muuttaa.
public-metodit ovat tiimin ”julkisia” sopimuksia. Ne kertovat, että tätä koodia on turvallista käyttää ja se toimii tietyllä tavalla.
private-metodit ja muuttujat ovat sisäisiä yksityiskohtia, joihin ei saa koskea. Ne antavat yksittäiselle koodaajalle vapauden refaktoroida omaa koodiaan rikkomatta muiden työtä.
protected on kuin private, paitsi että luokan lisäksi sen alaluokat (perintä) voivat käyttää näitä metodeja ja muuttujia.
Jos kaikki olisi public, kuka tahansa voisi vahingossa muuttaa toisen koodaamia arvoja, mikä aiheuttaisi ajonaikaisia virheitä ja hidastaisi projektia.
Tällä hetkellä, kun teet public-muuttujan, kaikki toimii. Se tuntuu hyvältä, koska pääset eteenpäin nopeasti. Tämä on kuitenkin oikotie, joka johtaa myöhemmin ongelmiin. Se on kuin rakentaisit talon ilman piirustuksia. Aluksi se nousee nopeasti, mutta sitten törmäät ongelmiin, joita et osaa ratkaista.
Miten public aiheuttaa ongelmia?
Oletetaan, että sinulla on pelaajaluokka.
public class Pelaaja
{
public int elamapisteet = 100;
}
Teet vihollisluokan ja hyökkäysmetodin.
public class Vihollinen
{
void Hyokkaa(Pelaaja kohde)
{
kohde.elamapisteet -= 10;
}
}
Tämä toimii. Aina kun vihollisen Hyokkaa-metodia kutsutaan, sille annetun Pelaajan elämä pienenee kymmenellä.
Nyt lisäät peliin parannusesineen.
public class Parannusesine
{
void Kayta(Pelaaja kohde)
{
kohde.elamapisteet += 20;
}
}
Tämäkin toimii. Mutta mitä jos haluat, että pelaaja ei voi parantua yli 100 elämäpisteen?
Koska elamapisteet on public, parannusesine voi yhä asettaa pelaajan elämäpisteet esimerkiksi 120:een, vaikka haluaisit sen maksimin olevan 100. Nyt sinun pitää muistaa lisätä tarkistus jokaisessa kohdassa, jossa elämäpisteitä muutetaan. Mitä jos sinulla on myös lääkintäpakkaus ja parantava loitsu, jotka vaikuttavat elämäpisteisiin? Joudut muistamaan tämän säännön joka paikassa. Rasittavaa!
Ratkaisu on luoda private-muuttuja ja public-metodi. Näin luot yhden ainoan paikan, jossa elämäpisteitä voidaan muuttaa. Näin sinun täytyy laittaa maksimielämän tarkistus vain tähän yhteen paikkaan.
public class Pelaaja
{
private int elamapisteet = 100;
private int maksimiElama = 100;
public void MuutaElamapisteita(int muutos)
{
elamapisteet += muutos;
if (elamapisteet > maksimiElama)
{
elamapisteet = maksimiElama;
}
}
}
Nyt muutat vihollisen ja parannusesineen koodia, jotta ne käyttävät metodia, eivätkä muuta muuttujaa suoraan.
public class Vihollinen
{
public void Hyokkaa(Pelaaja kohde)
{
kohde.MuutaElamapisteita(-10);
}
}
public class Parannusesine
{
public void Kayta(Pelaaja kohde)
{
kohde.MuutaElamapisteita(20);
}
}
Nyt et enää koskaan joudu murehtimaan siitä, onko elämäpisteiden maksimi ylitetty. Olet luonut yhden kontrollipisteen ja kaikki muu koodi, joka käyttää sitä, hyötyy tästä tarkistuksesta. Nyt kun sinulla on uusi parannusloitsu, sinun ei tarvitse muistaa lisätä tarkistusta. Riittää, että kutsut metodia.
public muuttuja on kuin antaisit avoimen sekkitilin kenelle tahansa, joka voi vapaasti nostaa rahaa. Sinun pitää seurata jokaista nostoa, ettei tilisi mene miinukselle tai ettei sitä nosteta liikaa. private muuttuja ja public metodi ovat kuin antaisit vain pankkikortin, jonka kautta voidaan nostaa vain tietty määrä rahaa ja kaikki nostot tallennetaan automaattisesti. Olet luonut kontrolloidun ja turvallisen tavan käyttää tiliä.
Tarkista esimerkkikoodi GitHubista

Toinen asia, miksi public muuttujat ovat huonoja, on se, että ne luovat riippuvuuksia. Jos joku muu koodi käyttää suoraan public muuttujaa, et voi enää koskaan muuttaa sen nimeä tai tyyppiä rikkomatta kyseistä koodia. Kuvittele, että koodaaja A tekee pelihahmon luokan ja tekee sen elamapisteet-muuttujasta julkisen. Koodaaja B luottaa tähän ja kirjoittaa oman koodinsa, joka käyttää tuota muuttujaa.
// Koodaaja B:n koodi pelaaja.elamapisteet = 50;
Myöhemmin, koodaaja A päättääkin, että muuttujan nimi on huono ja muuttaa elamapisteet nimiseksi hp:ksi. Koska muuttuja oli public, koodaaja B:n koodi ei tiedä muutoksesta. Koodi yrittää edelleen käyttää nimeä pelaaja.elamapisteet, mutta sitä ei enää ole olemassa. Tällöin ohjelma ei edes käänny, ja kääntäjä antaa syntaksivirheen.
Tämä on tietenkin todella pieni esimerkki, mutta voit kuvitella sen suuremmalla skaalalla. Jos vaikkapa Battlefield 6:n verkkokoodissa sadat eri paikat hakevat tietoja jostain tietyistä julkisista muuttujista, ja sitten yksi koodaaja päättääkin muuttaa niiden nimiä, kaikki nämä sadat paikat menevät rikki. Sitten joudut käymään läpi jokaisen yksittäisen kooditiedoston, joka kutsuu näitä muuttuja, ja muuttamaan nimet niissäkin. private on eri asia siinä mielessä, että kukaan ulkopuolinen ei edes voi kutsua sitä. Usein riippuvuuksia vähentämään tehdäänkin julkisia metodeja, jotka sitten hoitavat asioiden muuttamisen luokan sisällä turvallisesti. Muista käyttää hieman aikaa julkisten metodien nimien miettimiseen, ettet joudu muuttamaan niitä myöhemmin.
Niin ja riippuvuuksista puheenollen…
Riippuvuudet: kumppanuus ja sen riskit
Kun kirjoitat C#-koodia, teet asioista riippuvaisia toisistaan. Riippuvuus tarkoittaa sitä, että yksi luokka käyttää tai tarvitsee toista luokkaa toimiakseen. Tämä on täysin normaalia ja välttämätöntä ohjelmoinnissa. Kuten elämässä, kumppanuudet voivat olla joko vahvoja ja terveitä tai heikkoja ja haitallisia. Ohjelmoinnissa puhutaan tiukasta ja löysästä kytkennästä.
Tiukka kytkentä ei ole paha asia sinällään. Se on vain yksi tapa tehdä asioita. Jos se toimii ja ei aiheuta ongelmia, käytä sitä. Ongelmat alkavat, kun tiukka kytkentä tekee koodistasi hauraan. Kun pieni muutos yhdessä paikassa rikkoo toisen, se on todellinen ongelma.
Tiukka kytkentä
Kuvittele, että rakennat peliin hahmon, jonka pitää pystyä ampumaan. Hahmo tarvitsee aseen. Yksinkertaisin tapa toteuttaa tämä on luoda Hahmo-luokka ja luoda sen sisällä uusi Ase-olio.
public class Hahmo
{
public Ase ase;
public Hahmo()
{
ase = new Ase();
}
public void Ammu()
{
ase.Laukaise();
}
}
Tämä toimii täydellisesti. Kun luot uuden Hahmo-olion, se luo automaattisesti uuden Ase-olion. Hahmo-luokka on nyt tiukasti kytketty Ase-luokkaan. Hahmo-luokka on riippuvainen Ase-luokasta. Tämä toimii, kunhan Ase ei muutu.
Mutta mitä jos haluat tehdä peliin erilaisia aseita, kuten haulikon tai konepistoolin? Hahmo luo aina vain yhdenlaisen aseen, Ase-olion. Tämän rakenteen takia sinun pitäisi muuttaa Hahmo-luokan sisällä olevaa koodia joka kerta, kun haluat muuttaa aseen tyyppiä. Se on hankalaa, ja se tekee koodista hauraan. Kun muutat yhtä paikkaa, voit helposti rikkoa muuta toiminnallisuutta.
Tämä on todellinen ongelma, joka syntyy tiukasta kytkennästä. Se estää sinua käyttämästä samaa hahmoluokkaa uudelleen eri tarkoituksiin.
Löysä kytkentä
Terveempi vaihtoehto on käyttää löysää kytkentää. Se tarkoittaa, että luokka ei luo tarvitsemaansa kumppania itse, vaan kumppani annetaan sille. Tämä on kuin antaisit Hahmo-luokalle aseen käteen sen luomisen yhteydessä.
Muokataan Hahmo-luokkaa niin, että se ei enää luo asetta itse. Sen sijaan se ottaa aseen vastaan parametrina.
public class Hahmo
{
public Ase ase;
// Huomaa parametrina tuleva ase
public Hahmo(Ase annettavaAse)
{
ase = annettavaAse;
}
public void Ammu()
{
ase.Laukaise();
}
}
Nyt, kun luot hahmon, annat sille myös aseen. Tämän ansiosta voit antaa hahmolle minkä tahansa aseen, jonka olet luonut. Et ole sidottu yhteen asetyyppiin.
// Luodaan ensin aseet Ase pistooli = new Pistooli(); Ase haulikko = new Haulikko(); // Annetaan hahmolle pistooli Hahmo sankari = new Hahmo(pistooli); // Annetaan toiselle hahmolle haulikko Hahmo sankari2 = new Hahmo(haulikko);
Ylläolevassa esimerkissä on käytetty apuna perintää. Luokat Pistooli ja Haulikko on periytetty Ase-luokasta. Kun näistä luotuja olioita tarjotaan Hahmolle, se havaitsee: ”Ahaa, minulle annetaan Pistooli, noh, sekin on Ase.”
Tämän tavan etu on, että Hahmo-luokka ei enää välitä siitä, millainen ase sillä on. Sen tehtävä on vain käyttää saamaansa asetta. Jos myöhemmin päätät lisätä peliin uuden aseen, esimerkiksi raketinheittimen, sinun ei tarvitse muuttaa Hahmo-luokkaa. Koodi on joustavampaa ja helpompaa ylläpitää.
Tämän avulla voit myös testata Hahmo-luokkaa erikseen. Voit antaa sille ”testiaseen”, joka ei tee mitään, jotta voit varmistaa, että hahmon liikkuminen ja muut toiminnot toimivat oikein, ilman että aseen toiminnallisuus sotkee testiä.
Löysä kytkentä on usein parempi valinta, kun haluat luoda koodia, joka kestää muutoksia ja jota on helppo ylläpitää. Se estää yhden pienen muutoksen aiheuttaman ongelmien leviämisen koko koodiin. Se antaa sinulle joustavuutta ja mahdollisuuden laajentaa peliäsi ilman, että sinun tarvitsee rakentaa kaikkea alusta.
Perintä
Perintä on olio-ohjelmoinnin ominaisuus, jonka avulla voit luoda uuden luokan, joka perustuu olemassa olevaan luokkaan. Uusi luokka perii kaikki vanhan luokan ominaisuudet ja metodit. Tämä välttää saman koodin toistamista uudelleen.
Koska juuri käsittelimme Ase-esimerkkiä, pureudutaan siihen hieman syvemmin. Kaikille aseille voidaan määritellä yheisiä asioita, kuten että ne kaikki voi laukaista, niillä on jokin paino ja vahinko.
Aloitetaan luomalla yleinen Ase-luokka. Tämä on yliluokka, joka sisältää kaikkien aseiden yhteiset ominaisuudet.
public class Ase
{
public float paino;
public int vahinko;
public void Laukaise()
{
// Yleinen laukaisulogikka, esim. laukaise ääni
Console.WriteLine("Ase laukaistiin!");
}
}
Nyt luodaan spesifisempiä aseita. Emme luo niitä tyhjästä. Sen sijaan ”perimme” ne Ase-luokasta. Tätä varten käytämme kaksoispistettä (:) luokan nimen yhteydessä.
public class Pistooli : Ase
{
// Pistooli perii automaattisesti paino- ja vahinko-muuttujat ja Laukaise-metodin.
}
Tämä on perinnän perusidea. Pistooli-luokalla on nyt automaattisesti samat muuttujat ja metodit kuin Ase-luokalla.
Metodien ylikirjoittaminen ja monimuotoisuus
Mutta miten saamme pistoolin ja haulikon käyttäytymään eri tavalla? Molemmat perivät Laukaise()-metodin, mutta niiden laukaisuääni ja vahingon määrä ovat erilaisia. Siihen käytetään metodin ylikirjoittamista (override).
Tehdään ensin Ase-luokan Laukaise()-metodista virtuaalinen eli ylikirjoitettava lisäämällä virtual-avainsana.
public virtual void Laukaise()
{
Console.WriteLine("Ase laukaistiin!");
}
Nyt aliluokissa voimme korvata vanhempien metodin omalla versiolla käyttämällä override-avainsanaa. Voimme myös lisätä niille omia ominaisuuksia.
public class Pistooli : Ase
{
public int luotienMaara;
public override void Laukaise()
{
Console.WriteLine("Pistooli laukaistiin.");
}
}
public class Haulikko : Ase
{
public int haulikonKantama;
public override void Laukaise()
{
Console.WriteLine("Haulikko laukaistiin.");
}
}
Nyt kun luomme aseita ja kutsumme niiden Laukaise()-metodia, C# kutsuu oikean, spesifin version.
Ase pistooli = new Pistooli(); Ase haulikko = new Haulikko(); pistooli.Laukaise(); // Tulostaa "Pistooli laukaistiin." haulikko.Laukaise(); // Tulostaa "Haulikko laukaistiin."
Tämä kyky käsitellä erilaisia objekteja samalla tavalla, on polymorfismia eli monimuotoisuutta. Se tekee koodistasi joustavaa. Voisit esimerkiksi luoda listan aseista ja kutsua jokaisen aseen Laukaise()-metodia silmukassa, ja C# huolehtisi siitä, että oikea metodi ajetaan jokaiselle aseen tyypille.
Tarkista esimerkkikoodi GitHubista

Jotkin kielet (kuten C++) tukevat moniperintää, mikä tarkoittaa, että luokka voi periä useamman kuin yhden yliluokan ominaisuudet. C# ei tue tätä, koska moniperintä voi aiheuttaa monimutkaisia ongelmia, kuten timanttiongelman, jossa lapsiluokka perii saman ominaisuuden kahdelta eri yliluokalta, mutta ei tiedä, kumpaa versiota käyttää. C#-kielessä tämä ongelma vältetään sallimalla luokan periä vain yhdestä luokasta, mutta sen sijaan luokka voi toteuttaa useita rajapintoja (eng. interfaces), mikä tarjoaa moniperinnän edut ilman sen haittoja. Rajapinnoista puhumme lisää myöhemmin.

Perinnän heikkoudet
Perintä on voimakas työkalu, mutta sen käyttämisessä on joitain heikkouksia, jotka on hyvä tunnistaa.
Perintä luo todella tiukan kytkennän vanhempi- ja lapsiluokan välille. Lapsiluokka on täysin riippuvainen vanhemman luokan rakenteesta. Jos muutat jotain vanhempi-luokassa, esimerkiksi muutat metodia tai muuttujan tyyppiä, se voi rikkoa kaikki lapsiluokat. Tämä on ongelma isoissa projekteissa. Vaikka se voi toimia täydellisesti pienessä pelissä, isossa projektissa muutos yhdessä paikassa voi vaatia kymmenien muiden luokkien korjaamista.
C# tukee vain yksittäistä perintää. Tämä tarkoittaa, että luokka voi periä vain yhden yliluokan. Mitä jos haluat, että aseesi voi sekä ampua että olla kerättävissä pelaajan toimesta? Et voi periä Ase-luokkaa ja toista luokkaa samanaikaisesti. Tämä rajoittaa sitä, miten voit mallintaa asioita, ja voi johtaa monimutkaisiin ja vaikeasti hallittaviin luokkahierarkioihin, joita kutsutaan ”perintäpuiksi”.
Perintä luo ”on-tyyppiä” -suhteen (esim. pistooli on ase). Jos suhde ei ole tällainen, perinnän käyttäminen on väärä valinta. Esimerkiksi, jos ajattelet, että pelaajalla on ase, se ei ole perintää. Pelaaja ei peri asetta, vaan käytetään koostumusta (eng. composition) eli toisen luokan sisällyttämistä toiseen luokkaan, kuten aiemmassa esimerkissä, jossa Hahmo-luokalla oli Ase-muuttuja.
Perintä on täydellinen tapa luoda samanlaisten objektien hierarkioita ja välttää toistuvaa koodia. Mutta on myös tilanteita, joissa sen käyttö voi aiheuttaa enemmän ongelmia kuin se ratkaisee. Tämä on osa kokemuksen karttumista: tiedä, milloin mikäkin työkalu on paras.
Kokoelmat ja Listat
Kun ohjelmoit pelejä, joudut usein käsittelemään suuria määriä samanlaisia esineitä tai hahmoja. Esimerkiksi peli, jossa on vihollisia, ei ole hauska, jos koodissa on vain määritelty peräkkäin vihollinen1, vihollinen2 ja vihollinen3. Miten lisäät tai poistat niitä? Jos pelissä on satoja vihollisia, tällainen lähestymistapa on täysin kestämätön. Tässä kohdassa kokoelmat tulevat apuun. Ne ovat tietorakenteita, jotka auttavat organisoimaan ja hallinnoimaan useita samanlaisia objekteja yhtenä kokonaisuutena.
Taulukot: kankea mutta nopea
Taulukot (Arrays) ovat yksi alkeellisimmista kokoelmista C#:ssa. Alkeellinen ei välttämättä tarkoita huonoa, koska se viittaa yksinkertaisuuteen ja sitä kautta muistinkäytön vähyyteen. Taulukot ovat tiukka, ennaltamäärätyn kokoinen laatikko, johon mahtuu vain tietty määrä tavaraa, esim. korkea tarjottimenpalautuskärry, jossa on paikka 30, ja vain 30 tarjottimelle.
int[] numerot = new int[5];
Tämä luo taulukon, johon mahtuu tasan viisi kokonaislukua. Taulukot ovat erinomaisia suorituskyvyn kannalta, koska ne varaavat muistista tilaa jatkuvana lohkona. Ne ovat nopeita, jos tiedät tarkan koon etukäteen ja tarvitset nopeaa pääsyä tietoihin. Peleissä niitä voi käyttää esimerkiksi pelikentän ruudukon tai staattisen objektilistan mallintamiseen. Ne ovat kuitenkin kankeita: kun olet luonut taulukon, et voi muuttaa sen kokoa, mikä tekee niistä hankalia, kun lisäät tai poistat elementtejä kesken pelin.
Listat: joustava ja kätevä valinta
Useimmissa tilanteissa pelinkehityksessä haluat kokoelman, joka voi kasvaa ja kutistua dynaamisesti. Tähän tarkoitukseen List<T> on erinomainen. Kirjain T tarkoittaa tyyppiä (esim. int, string, Vihollinen).
Kuvittele, että haluat luoda listan vihollisista, joita tulee pelimaailmaan lisää koko ajan. Voisit tehdä sen näin:
// Luodaan lista, joka pystyy pitämään sisällään Vihollinen-tyyppisiä objekteja List<Vihollinen> viholliset = new List<Vihollinen>();
Tämä luo tyhjän listan, joka voi pitää sisällään Vihollinen-tyyppisiä olioita. Voit lisätä, poistaa, ja käsitellä niitä joustavasti. Seuraavaksi käydään läpi, miksi tämä on parempi kuin taulukot tai useiden muuttujien luominen.
Jos et käyttäisi listoja, voisit joutua tekemään näin:
Vihollinen vihollinen1 = new Vihollinen(); Vihollinen vihollinen2 = new Vihollinen(); // ... // Ja sitten kävisit ne läpi yksitellen. vihollinen1.Paivita(); vihollinen2.Paivita(); // ...
Tämä on täysin kestämätön lähestymistapa, jos vihollisia on monta. Koodisi on kankeaa, ja jos haluat lisätä uuden vihollisen, joudut lisäämään uuden muuttujan ja päivittämään kaiken koodin, jossa niitä käsitellään.
Sen sijaan listojen kanssa voit käyttää erityisesti niiden läpikäymiseen tarkoitettua foreach-silmukkaa, joka ottaa listasta järjesetyksessä elementtejä väliäaikaiseen muuttujaan (yksittäinen vihollinen viholliset-listasta), jolloin voit käsitellä jokaista elementtiä omalla vuorollaan silmukan sisällä.
// Luo lista ja lisää siihen muutamia vihollisia
List<Vihollinen> viholliset = new List<Vihollinen>();
viholliset.Add(new Vihollinen());
viholliset.Add(new Vihollinen());
viholliset.Add(new Vihollinen());
// Päivitä kaikki viholliset yhdellä silmukalla
foreach (Vihollinen vihollinen in viholliset)
{
vihollinen.Paivita();
}
Tässä näet, kuinka yksi silmukka käsittelee kaikki listan viholliset, olipa niitä kolme tai kolmesataa. Kun uusi vihollinen syntyy peliin, lisäät sen listalle, ja kun se poistuu peliympäristöstä, poistat sen listalta. Koodi on nyt skaalautuvaa.
Listan yleisimmät metodit
Add(): Lisää uuden elementin listan loppuun.Remove(): Poistaa elementin listalta.Count: Palauttaa listan elementtien määrän. Tämä on hyödyllinen, jos haluat käydä kaikki läpi silmukassa, tai tiedät montako on jäljellä.lista[indeksi]: Voit hakea elementin listalta indeksillä, esimerkiksiviholliset[0]hakee ensimmäisen vihollisen.Clear(): Poistaa kaikki elementit listalta.
Listoilla on myös paljon muita toiminnallisuuksia spesifeihin käyttötarkoituksiin. Voit selata näitä Microsoftin dokumentaatiosta.
Tässä yhteydessä on hyvä mainita, että ohjelmoinnissa yleinen käytäntö on, että järjestysluvut (esim. listan indeksit) alkavat nollasta. Tämä on tehokkain tapa viitata tietokoneen muistipaikkoihin. Taulukon ensimmäisen elementin osoite on sama kuin taulukon alkuosoite. Nolla tarkoittaa nollan askeleen siirtymää tästä alkuosoitteesta. Tämä tekee muistiosoitteiden laskemisesta nopeaa ja yksinkertaista, koska ylimääräisiä laskutoimituksia ei tarvita. Kerromme binäärilaskennasta lisää tulevaisuudessa.
Tarkista esimerkkikoodi GitHubista

Tehokkuus vai käytettävyys?
List<T> on kuin joustava taulukko. Kun lisäät siihen elementtejä ja se täyttyy, se luo taustalla uuden, isomman taulukon ja kopioi vanhat elementit sinne. Tämä on paljon tehokkaampaa kuin muuttujien luominen yksitellen, ja vaikka se vie hieman enemmän aikaa kuin pelkkä taulukko, List<T>:n joustavuus on sen arvo.
Ohjelmoinnissa ei aina tarvitse tavoitella täydellistä suorituskykyä, vaan joskus valitaan ratkaisu, joka toimii luotettavasti ja on helposti ylläpidettävissä. Esimerkiksi List on usein ”riittävän hyvä” valinta, koska sen joustavuus on tärkeämpää kuin Arrayn pieni suorituskykyetu, joka on toki merkityksellistä joskus, mutta ei läheskään aina. Jos vaikka käyt läpi pikseleitä, 3D-mallien pisteitä, tai tekstuureja, niitä voi olla niin paljon, että jokainen pieni suorituskykyparannus kerrottuna miljoonilla kasvaakin pian suureksi suorituskykyparannukseksi.
Ohjelmoinnissa kuitenkin harvoin tarvitaan sellaista suorituskykyä, että joustavuudesta luopuminen olisi perusteltua. Tämä on esimerkki ”riittävän hyvä” -ratkaisusta, jossa joustavuus ja ylläpidettävyys ovat tärkeämpiä kuin pieni suorituskykyhyöty. Ohjelmoijana sinun on hyvä osata ajatella, kuinka paljon tietyt ratkaisut vievät aikaa eri tavoilla. Joustava ratkaisu voi säästää kehitysaikaa, mutta joustamaton ratkaisu voi säästää käyttäjältä aikaa, kun ohjelmaa suoritetaan.
Yksinkertaisena esimerkkinä, voit käytännössä laskea suuntaa-antavasti, että paljonko tietyt ratkaisut kestävät koneelta suorittaa. Mietitään vaikka, että haluamme käydä läpi 10 miljoonaa pikseliä. Paljonko Array säästää aikaa verrattuna Listaan? Jos otetaan 3 GHz prosessori, joka siis voi suorittaa 3 miljardia kellojaksoa sekunnissa, eli yksi kellojakso kestää noin 0.33 nanosekuntia.
Oletetaan, että arrayn läpikäynti vaatii 10 kellojaksoa per elementti, koska haku osuu L2-välimuistiin (10 miljoonaa pikseliä ei mahdu L1-välimuistiin). 10 miljoonaa elementtiä * (10 kellojaksoa / elementti * 0.33 ns / kellojakso) = 33 000 000 ns eli 33 millisekuntia.
Oletetaan, että listan läpikäynti vaatii 50 kellojaksoa per elementti, koska jokainen haku osuu keskusmuistiin. 10 miljoonaa elementtiä * (50 kellojaksoa / elementti * 0.33 ns / kellojakso) = 165 000 000 ns eli 165 millisekuntia.
Ero ei edelleenkään ole valtava, mutta jos sinun pitäisi pävittää ruutua 60 kertaa sekunnissa (tai vaikkapa 240Hz monitorilla paljon enemmänkin), niin 33 ms ei enää olekaan tarpeeksi hyvä, ja 165 ms on suorastaan hidas. Tärkein asia tässä esimerkissä on huomata, että puhutaan 10 miljoonasta elementistä. Jos listassasi on kymmenen vihollista, tai jopa tuhat vihollista, säästöllä ei luultavasti ole mitään väliä.
Kommenteilla koodia pois päältä
Puhuimme aiemmassa osassa koodin kommentoinnin tärkeydestä, mutta kommentteja voi käyttää myös kätevänä tapana poistaa koodia väliaikaisesti suorituksesta, ilman että sitä tarvitsee poistaa kokonaan tiedostosta. Tämä on hyödyllistä esimerkiksi virheiden etsinnässä tai kun kokeilet eri toteutustapoja. Jos poistat koodia pysyvästi, on parempi oikeasti poistaa se ja luottaa versionhallintaan (esim. Git) vanhan koodin palauttamiseksi tarvittaessa.
C#:ssa on kaksi perustapaa kommentoida koodia:
Yhden rivin kommentti (//):
Voit käyttää tätä nopeasti poistaaksesi yhden koodirivin:
// pelaajanLiikenopeus = pelaajanLiikenopeus + 5.0f;
Monen rivin kommentti (/* ... */):
Voit käyttää tätä kääntämään laajemman osan koodia pois päältä:
/*
// Vanha hyökkäyslogiikka, testataan uutta.
if (vihollinen.Onkantama())
{
vihollinen.otaVahinkoa(10);
}
*/
Suunnittelu ja todellisuus
Saatat ehkä ajatella, että hyvä ohjelmoija suunnittelee kaiken etukäteen. Ei pidä paikkaansa! Hyvät ohjelmoijat kyllä suunnittelevat (esimerkiksi mitä käyttäjän pitää voida tehdä ohjelmalla), mutta he tietävät, että mikään suunnitelma ei kestä ensikosketusta todellisuuteen. Varsinkin pelinkehityksessä, jossa asiat muuttuvat jatkuvasti, on erittäin kyseenalaista käyttää liikaa aikaa täydellisen suunnitelman tekemiseen. Sen sijaan, todelliset ammattilaiset noudattavat ketterää filosofiaa: tee vain tarpeeksi, refaktoroi, ja jatka eteenpäin.
Tämä ei tarkoita, että sinun pitäisi heittää kaikki suunnittelu romukoppaan ja alkaa vain hakata koodia. Se tarkoittaa, että hyväksyt sen tosiasian, että koodi on dynaamista ja elävää, ja sen pitää muuttua ajan myötä. Liika suunnittelu johtaa turhautumiseen, kun joudut muuttamaan pitkälle vietyjä, mutta todellisuudessa hyödyttömiä suunnitelmia.
Mikä ihmeen refaktorointi?
Refaktorointi on prosessi, jossa koodin sisäistä rakennetta parannetaan sen ulkoista käyttäytymistä muuttamatta. Se ei siis lisää uusia ominaisuuksia tai korjaa bugeja. Ohjelma ei parane tai edisty refaktoroinnin seurauksena, mutta koodi paranee. Refaktorointi tekee olemassa olevasta koodista selkeämpää, helpommin luettavaa ja ylläpidettävää. Tämän ansiosta tulevien ominaisuuksien lisääminen on nopeampaa ja virheiden löytäminen helpottuu.
Kirjasuositus: Refactoring: Improving the Design of Existing Code (Martin Fowler)
Mieti vaikka huoneen siivoamista. Kun siivoat huoneen, et saa uusia tavaroita, mutta sinun on helpompi löytää tarvitsemasi ja liikkua siellä. Sama pätee koodiin: siivoamalla sen rakenteen, teet siitä tehokkaamman työympäristön. Refaktorointi on osa ohjelmoijan työtä, sillä se estää koodin muuttumasta ”spagettikoodiksi”, jota kukaan ei halua tai osaa työstää.
Ohjelmoinnissa vuorottelevat kaksi vaihetta: sotkeminen ja järjesteleminen.
Kun haluat lisätä ominaisuuden ohjelmaasi, sinun pitäisi tehdä mitä tahansa, että saat sen toimimaan. Tämä on väärä hetki yrittää olla siisti ja elegantti. Käytä jesaria, purkkaa, klemmareita ja vasaroi tarvittaessa seinään reikä (kuvainnollisesti tietenkin). Tässä vaiheessa sinun tehtäväsi on saada kyseinen ominaisuus pelittämään.
Kun ominaisuus pelittää, on aika järjestellä koodisekamelska, jonka juuri tuotit. Tämä on se kohta prosessia, jossa varmistat, että ohjelmaan pystytään lisäämään ominaisuuksia myös tulevaisuudessa.
Mietipä, jos kokkaat valtavan juhla-aterian, mutta et siivoa sen jälkeen keittiötä. Kippoja, kulhoja, pannuja, kattiloita, vuokia, tyhjiä tölkkejä ja vihannesten kuoria on kasaantunut jokaiselle työtasojen neliösenttimetrille. Nyt keittiössä ei pysty lämmittämään edes hernekeittoa, ennen kuin edelliset jäljet on putsattu. Toisaalta, jos sinun pitäisi tehdä valtava ateria 12 hengelle, mutta et saisi sotkea mitään, et luultavasti saisi vettäkään keitettyä ilman hermoromahdusta.
Koodaajana sinulla pitää olla vapaus testailla ja luoda sotku, mutta myös vastuu siivota jälkesi. Älä yritä tehdä molempia samanaikaisesti.
On myös mahdollista ylirefaktoroida koodia niin, että siihen lisätään uutta arkkitehtuuria, joka ei auta sinua koodaamaan. Ajattele, että siivottuasi huoneesi, sinulla olisi tuhat identtistä valkoista laatikkoa, joissa kaikissa on yksi tavara. Kyllä, lopputulos on varmaan siistimmän näköinen, mutta huoneen käyttäminen sen jälkeen on todella hankalaa.
Refaktorointi ja ohjelmistoarkkitehtuuri eivät siis ole tavoitteita itessään. Niiden tarkoitus on vain auttaa sinua. Kun ohjelmoit tarpeeksi pitkälle ja huomaat, että koodissa alkaa olla toistoa ja eri asiat sotkeutuvat toisiinsa, silloin on aika refaktoroida. Nimenomaan ongelmatilanteet kertovat sinulle, milloin parempi ratkaisu on paikallaan. Kaikilla on ideoita siitä, millaista siistin koodin pitäisi olla, ja nämä ideat voivat toki auttaa huomaamaan hankalaselkoisia piirteitä omassa koodissasi, jotka aiheuttavat sinulle usein kompastelua. Kuitenkin, jos ongelmaa ei ole, älä yritä ratkaista sitä ennakoivasti. Teet vain elämästäsi hankalampaa. Ratkaise ongelmat kun niitä ilmenee, tai kun tiedät kokemuksesta, että niitä tulee ilmenemään. Yritä myös tunnistaa ongelmat varhaisessa vaiheessa, ennen kuin niistä tulee täysin mahdottomia hallita.
Ylisuunnittelu ja vesiputousmalli
Kuvittele, että haluat tehdä peliin pelaajahahmon, joka voi liikkua. Huono lähestymistapa tähän olisi, että käytät kaksi viikkoa pelkästään sen suunnittelemiseen. Teet yksityiskohtaisia kaavioita, joissa on satoja eri luokkia, jotka mallintavat kaikki mahdolliset tilanteet, kuten liikkumisen, hyppäämisen, juoksemisen, kävelemisen, uimisen ja lentämisen, vaikka peliin ei olisi edes tarkoitus lisätä uintia tai lentämistä.
Tämä on vesiputousmallin mukainen kehitys. Se on jäykkä ja byrokraattinen. Jokaisen vaiheen (suunnittelu, toteutus, testaus) on oltava täydellinen ennen seuraavaan siirtymistä. Se voi toimia hyvin rakennusalalla, jossa et voi testata talon kestävyyttä ennen sen valmistumista, mutta se on täysin toimimaton ohjelmoinnissa ja varsinkin pelinkehityksessä.
Et tiedä, mikä on hauskaa tai toimivaa, ennen kuin sinulla on jotain pelattavaa. Voit suunnitella täydellisen liikkumisjärjestelmän paperilla, mutta kun koodaat sen ja pelaat sitä, huomaat, että se tuntuu jäykältä ja epäintuitiiviselta. Tässä vaiheessa olet jo tuhlannut viikkoja työhön, josta suuri osa oli turhaa. Joudut heittämään osan suunnitelmista pois ja aloittamaan alusta. Tämä tappaa motivaation.
Liika suunnittelu aiheuttaa myös sen, että käytät aikaa sellaisten asioiden suunnitteluun, joita et edes tule tarvitsemaan tai et ehdi kehittää. Ajattele, jos suunnittelet inventaariojärjestelmän, joka tukee kymmeniä eri esinetyyppejä ja monimutkaisia yhdistelmiä, vaikka loppupelissä olisi vain kolme erilaista esinettä. Turhaa työtä. Muista, että kaupallisissa projekteissa, aika on arvokkain resurssi. Varsinkin jos tahdot töihin pelialalle, sinun on osattava käyttää jokainen tunti hyödyksi.
Ketterä kehitys ja refaktorointi
Hyvä ratkaisu on aloittaa pienestä ja rakentaa sen päälle. Tämä on ketterän kehityksen periaate. Sen sijaan, että suunnittelet koko pelin, suunnittele lista sen osista, keskity yhteen pieneen osaan, tee siitä toimiva, ja siirry eteenpäin.
On helppo jumiutua olemaan tekemättä mitään, koska saatat kirjoittaa huonoa koodia.
Tee jotain toimivaa. Tavoitteesi on saada mahdollisimman nopeasti jotain pelattavaa, vaikka se olisi vain raakile. Älä yritä kirjoittaa täydellistä koodia. Kirjoita koodia, joka pelaa edes jotenkin. Se ei ehkä ole paras mahdollinen pätkä koodia, mutta se toimii. Tämän jälkeen, testaa ja tunnista ongelmat. Pelaa tekemääsi ja mieti, missä on parantamisen varaa. Ehkä liike on epämääräistä, tai huomaat, että pelihahmon nopeuden muuttaminen on hankalaa, koska koodi on sotkuista.
Kun olet tunnistanut ongelman, ota askel taaksepäin ja refaktoroi koodi. Refaktorointi siis tarkoittaa koodin sisäisen rakenteen parantamista muuttamatta sen ulkoista toimintaa. Siirrä liikkumiseen liittyvä koodi omaksi metodikseen. Tee muuttujista selkeämpiä. Tee rakenteesta loogisempi. Et ole tuhonnut mitään, vaan parantanut olemassa olevaa.
Kun koodi on siistimpi ja helpompi työstää, voit lisätä uusia ominaisuuksia.
Tämä on jatkuva sykli: Tee -> Testaa -> Refaktoroi -> Lisää uusi ominaisuus.
Erota toisistaan huono ja tyhmä koodi.
Huono koodi on sellaista, joka on hetkellisesti sotkuista tai epäoptimaalista, mutta jota voi parantaa refaktoroimalla.
Tyhmä koodi on sellaista, jota ei ole koskaan pitänyt kirjoittaa. Se on koodia, joka ratkaisee ongelman, jota ei ole olemassa, tai on niin ylisuunniteltu, että se estää kehitystä. Se on esimerkiksi sitä, että rakennat monimutkaisen järjestelmän, joka mahdollistaa kymmenen eri vihollistyypin, kun et ole vielä päättänyt, onko pelissä edes vihollisia. Se on sitä, että rakennat oman koodisi, kun voit käyttää jo olemassa olevaa, vakaata kirjastoa.
Voit tehdä mitä tahansa, mutta et voi tehdä kaikkea
Soolokoodaajallakin on oma hommansa projektin pitämisessä kasassa. Koodissa on monta eri näkökulmaa: toimiiko se, onko se ymmärrettävää, onko se ylläpidettävissä, ja onko se laajennettavissa. Usein käy niin, että kun kohennat yhtä parametria, niin muut niistä heikentyvät. Kuin koettaisit Dungeons & Dragonsissa päättää, mihin ominaisuuksiin laitat pisteet.
Yksi ihminen voi helposti myös ajatella: ”jos minulla olisi 100 hengen tiimi, pystyisin tekemään mahtavan pelin!” Mutta isot tiimit tuovat omat ongelmansa. Jos sinulla on uusia systeemejä, kaikkien pitää opetella käyttämään niitä. Osaavatko kaikki toimia keskenään ja myös yhtenä tiiminä? Koodin ylläpidettävyys ja ymmärrettävyys nousevat arvoon arvaamattomaan, kun 30 koodaria sörkkivät sitä keskenään ja koettavat saada siitä selvää. Miten pidetään huolta siitä, ettei projekti ole umpisolmussa ensimmäisen viikon jälkeen?
Kaikkiin näihin ongelmiin löytyy vastauksia, mutta useimpia ongelmia pystyy ymmärtämään vasta kun on paljon kokemusta koodaamisesta. Tämän oppaan tärkein anti tuleekin tässä:
Koodaa paljon projekteja ja vie niitä loppuun, niin saat kokemusta.
Jos olet ikinä pelannut roolipelejä, tiedät niissä olevat kokemuspistemekaniikat. Ne pohjautuvat oikeaan elämään. Jos vasta aloittelet ohjelmointia, olet tasolla 1. Ensimmäiset projektisi pitäisi vastata jonkin roolipelin ensimmäisiä tehtäviä, koska et voi tarttua isompiin haasteisiin, ennen kuin olet kehittänyt osaamistasi ja saanut kokemusta. Mutta saat kokemusta vain kun koodaat. Lukeminen voi auttaa selventämään ajatuksia ja saamaan tietoa, mutta ymmärrät asioita vasta kun teet itse. Jos sinulla on 8 tuntia päivässä aikaa, lue 1 tunti ja koodaa 7 tuntia. Näin kehityt.
Jos haluat selata oppaasta löytyviä esimerkkejä, suuntaa SuomiGameHUBin Githubiin.

