Eevert Saukkokoski, 20.1.2009
Studio Wars on ruutukoordinaatistossa pelattava 2D-strategia, jossa tarkoituksena on vallata pelialuetta vastustajalta siirtelemällä pelialueella sijaitsevia yksiköitä. Pelissä on kaksi joukkuetta joita kumpaakin voidaan valita ohjaamaan joko ihmis- tai tietokonepelaaja sekä kolme erilaista kenttää.
Pelin idea on, että yksiköt kohdistavat ympäristöönsä vaikutusvaltaa ja kuhunkin ruutuun kohdistuvat vaikutusvaltojen summat määrittävät, mille joukkueelle ruutu kuuluu. Yksiköitä on eritasoisia yhdestä viiteen; tason nousu näkyy vaikutusvallan kasvuna. Jos onnistuu valtaamaan ruudun, jossa on vihollisen yksikkö, vaihtaa myös yksikkö puolta. Peli päättyy kun toinen joukkueista on saavuttanut riittävän paljon enemmän valloitettuja ruutuja kuin toinen.
Pelin käyttöliittymä koostuu asetusruudusta, itse pelinäkymästä ja päätösruudusta. Asetusruutu näytetään heti pelin käynnistyttyä ja siinä on esillä lyhyt ohjeistus sekä pelaaja- ja kenttävalitsimet. Ruudussa on pelinkäynnistysnappi, joka tuo esille itse pelipaneelin.
Pelipaneelissa on näkyvillä pelikenttä, jossa ovat yksiköt esittettynä niiden tason mukaisella määrällä pisteitä ja niiden tuottamat vaikutusalueet, molemmat joukkueittain värjättynä. Alalaidassa on teksti joka kertoo lyhyesti pelin tilan eli esimerkiksi kenen pelaajan toimimista seuraavaksi odotetaan. Peli käynnistyy välittömästi.
Jos vuorossa on ihmispelaaja, pelaajan hallitsemat yksiköt korostetaan hitaasti näkyviin sykkivällä vaaleammalla taustavärillä. Näitä yksiköitä voi liikuttaa vetämällä hiiren avulla. Vetämisen aikana korostetaan kohteet joihin yksikön voi liikuttaa tiheämmin sykkivän taustavärin avulla. Tietokonepelaajan vuoron aikana visualisointia ei näytetä, jotta käy ilmi ettei käyttäjä voi vaikuttaa yksiköihin. Pelaajat siirtävät yksiköitä yhden kerrallaan vuoron siirtyessä aina seuraavalle. Tätä jatketaan kunnes toinen pelaajista voittaa. Aina voidaan myös aloittaa uusi peli tai poistua ohjelmasta peli-ikkunan ylävalikon kautta.
Pelin päätyttyä pelaajan voittoon tulee näkyviin päätösruutu, josta käy ilmi voittajajoukkue sekä täyttynyt voittoehto ja josta voi samalla aloittaa uuden pelin. Käyttöliittymä muodostaa hyvin yksinkertaisen kokonaisuuden, jonka kanssa toimiminen on kaikille hiirenkäyttötaitoisille helppoa.
Toteutuksessa pyrin aktiivisesti huomioimaan hyvät olio-ohjelmoinnin käytänteet kuten yhden yhden vastuun periaatteen ja testattavuuden. Oleellisille osille koodista tuotin automatisoidut yksikkötestit JUnit 4:n, Hamcrestin ja Mockiton avulla. Testattavuusvaatimuksen perusteella otin käyttöön myös Googlen avoimen lähdekoodin Guice-kirjaston, jonka avulla saatoin vähentää oleellisesti tarvittavien oliotehdasluokkien määrää mutta nauttia silti samalla täysin rinnoin dependency injectionin riemuista.
Pelin käyttöliittymä on toteutettu Swingillä ja grafiikat Java2D:llä reaaliaikaisesti renderöitynä. Tarkoituksena oli aluksi etsiä reaaliaikaiseen renderöintiin ja pelinkehitykseen paremmin sopivia kirjastoja, mutta lupaavimmilta näyttäneiden LWJGL:n ja Slickin dokumentaatio oli liian vajanaista. Totesin opetteluun käytetyn vaivan olevan projektin mittakaavan ulkopuolella ja tyydyin Javan oletusvälineisiin.
Pelin kartat on tallennettu Yaml-tiedostoina ja niitä luetaan JYamlin avulla. Valitsin formaatin sen selkokielisen ulkoasun, suhteellisen tuttuuden ja itse JYaml-kirjaston helppokäyttöisyyden perusteella.
Ohjelmoinnissa käytin laajalti hyväkseni Eclipseä ja sen hyväksi todettuja automatisointi- ja refaktorointitoiminnallisuuksia. Alustayhteensopivuuden varmistamiseksi kehitin ohjelmaa sekä Windows XP:n että Ubuntu Linuxin päällä.
Projekti on jaettu luokkien käyttötarkoitusten mukaan useisiin paketteihin. Päätasolla niiden sisällöt jakautuvat seuraavasti:
Oleellisin pelissä käytetty tietorakenne on Layer. Layer
käsittää joukon pisteitä tasossa, joilla on jokaisella
jokin arvo. Layerilla on java.awt.Rectangle-objekti joka määrittelee sen rajat ja
jonka perusteella Layer toteuttaa myös Iterable-rajapinnan. Sen avulla
voidaan kaikki Layerin pisteet ja niiden arvot käydä läpi.
Layerista on paitsi implementaatioita joissa pisteiden arvot on annettu
suoraan esimerkiksi Map-rakenteessa, myös implementaatioita jotka
laskevat pisteiden arvoja dynaamisesti esimerkiksi muihin Layereihin
perustuen. Erikoistapaus jossa arvot ovat Boolean-tyyppisiä on merkitty
omalla ActivityLayer-rajapinnallaan. Tälle voidaan
suorittaa esimerkiksi leikkaus- ja yhdisteoperaatioita toisten
ActivityLayer-olioiden suhteen.
Käytännössä kaikki pelimekaaniset toiminnallisuudet nojaavat
Layer-objektien manipulointiin eri tavoin. World määrittelee
pelimaailman, jossa on kerros yksiköille ja
ruututyypeille erikseen. Yksiköiden perusteella generoidaan
myös vaikutusvaltakerros, josta käy ilmi mikä joukkue
minkäkin ruudun omistaa. Tämä tapahtuu laskemalla kunkin ruudun kohdalla
yhteen yksikkökerroksen yksiköiden ruutuun kohdistamat vaikutusvallat
joukkueittain - joka onnistuu tietysti sekin, koska yksiköillä on
joukkueen lisäksi kerros, josta voidaan lukea sen mihin tahansa pisteeseen
kohdistaman vaikutusvallan määrä.
Pelin reaaliaikainen moottori koostuu pelilogiikka- ja renderöintiluupista, jota ajetaan omassa säikeessään. Jokaisella ajokerralla päivitetään peli ja piirretään pelimaailma uudelleen ruudulle. Vaikka pelin tilaa muuttavat tapahtumat ovat verraten harvinaisia, näin huolehditaan syötteen ajantasaisesta näkymisestä pelitilanteessa ja grafiikan - esimerkiksi animaatioiden - sujuvasta esittämisestä.
Pelilogiikka sisältää kolme sisäkkäistä suoritusvaihetta: Peli suorittaa pelikierroksen joka suorittaa pelivuoron. Peli luo yhä uusia kierroksia kunnes päättymisehto on täyttynyt. Kierros suorittaa jokaista pelin pelaajaa kohden oman pelivuoron ja pelivuoro ottaa vastaan kulloinkin vuorossa olevan pelaajan antamat komennot. Pelaajan vuoro todetaan päättyneeksi aina kun pelaaja on antanut komennon, jolloin vuoro siirtyy seuraavalle. Mikäli pelaaja oli kierroksen viimeinen eikä peli ole vielä päättynyt, aloitetaan uusi - ja niin edelleen.
Pelin kulusta kiinnostuneet saavat näiden eri prosessien alkamisesta ja loppumisesta sekä esimerkiksi komentojen suorittamisesta tiedon keskitetyn tapahtumanvälittäjän välittämien tapahtumaolioiden kautta.
Renderöintiprosessissa muodostetaan Graphics-olioon maailmanrenderöijän avulla pelimaailmasta kuva, jossa ovat esitettyinä joukkueiden vaikutusvallat ruuduissa sekä niissä sijaitsevat yksiköt. Renderöinti tapahtuu kerroksittain - ensin ruututyyppikerros, sitten vaikutusvaltakerros ja viimeiseksi yksikkökerros. Jokaiseen kerrokseen voidaan lisätä erityisiä renderöintiefektejä, jotka käydään läpi kerroksien elementtejä renderöitäessä. Näin saadaan aikaan esimerkiksi pelaajan hallitsemat yksiköt korostava sykkivä taustaefekti.
Ohjelma sisältää lukuisia luokkia, joiden tehtävänä on luoda objekteja.
Olen erotellut nämä nimien suhteen kahteen pääjoukkoon, Provider- ja
Factory-mallisiin luokkiin.
Provider-luokat saavat nimensä Guicen vastaavasta rajapinnasta, joka
määrittelee argumentittoman get-objektinluontimetodin. Näiden luokkien
tarkoitus on siis luoda objekteja silloin, kun niihin ei vaadita
luontiaikaisia riippuvuuksia, vaan ne voidaan syöttää Provider-oliolle
etukäteen (yleensä konstruktorissa).
Factory-luokka on kyseessä silloin kun tietynlaisen objektin luominen
vaatii luontiaikaisten riippuvuuksien syöttöä. Jokaisella on erilaiset
riippuvuudet, joten yhteistä rajapintaluokkaa ei ole, mutta tehdasmetodin
nimi on käytännön mukaan create. Yleensä nämä on yhdistetty
etukäteen syötettäviin riippuvuuksiin silloin kun mahdollista, eli
luotavan objektin riippuvuudet harvemmin saadaan kokonaisuudessaan tietoon
vasta luonnin yhteydessä.
Mikäli luokka ei huolehdi pelkästään riippuvuuksien keräämisestä ja syöttämisestä, on se nimetty muuksi kuin Provider- tai Factory-luokaksi sekaannuksien välttämiseksi.
Yksikkötestausluokat on sijoitettu oikeiden luokkien oheen, mutta erotettu toteutusluokista nimen Test-päätteen avulla.
Peliin sisältyy vain kaksi yksittäisenä mainitsemisen arvoista algoritmia. Yksiköiden liikuttelu kentällä nojaa Dijkstran algoritmiin, jolla selvitetään kuinka paljon nopeuspisteitä liikkuminen tietystä lähtöpaikasta eri ruutuihin vie. Tietokonepelaajan tekoälyalgoritmi taas toimii suorittamalla täydellinen indeksointi mahdollisista siirroista ja arvioimalla niiden lopputulosta numeerisesti - arviointiperusteena on valloitettujen ruutujen lukumäärä.
Pelistä on myös netistä käynnistettävä Web Start -versio. Ohjelma vaatii
kaikki suoritusoikeudet ja siten myös digitaalisen allekirjoituksen, koska
Guice suorittaa ajon aikana generoitujen luokkien tilapäistä varastointia
levylle. Tätä varten loin XML-muotoisen Ant-konfiguraatiotiedoston
joka pakkaa ja allekirjoittaa kaikki tarvittavat resurssit projektin
webstart-kansioon.
Alkuperäinen pelisuunnitelma koski eräänlaista Advance Wars-kloonia, johon oli tarkoitus tulla selkeästi toisistaan eroavia yksiköitä, maastotyyppejä ja pelaajan toimintoja. Projektin edetessä ajatukset selkeytyivät ja ominaisuudet karsiutuivat, ja pelistä tuli lopulta täysin omanlaisensa. Tällaisen suunnitteluprosessin hallinta oli minulle täysin uusi kokemus eikä luonnollisesti ollut täysin ongelmatonta.
Lähtöajatus sisälsi piilo-oletuksen siitä, että saan teemaan sopivat tekstuurit käyttööni jostakin. Se kuitenkin olisi estänyt pelin nettilevityksen, ja peli-idean kehittyessä kyseeseen tullut täysin kotikutoinen graafinen ilme sopi projektiin huomattavasti paremmin.
Ohjeellinen 80 tunnin aikataulu venyi noin neljäsosalla. Tätä en pidä kovinkaan yllättävänä, sillä olin alunperinkin otaksunut mokien todennäköisesti kasautuvan. Lisäksi ylitykseen vaikutti useamman viikon tauti, jonka podin joulun tienoilla. Työskentelin siis varsin pitkään alentuneella teholla.
Itse toteutus onnistui jälkeenpäin tarkastellen suhteellisen hyvin. Lähtösuunnitelman megalomaanisuus oli vielä projektin loppuneljännekseen tultaessa kuitenkin paha este ajankäytön optimoinnin kannalta. Käytin lopulta suhteettomasti aikaa valmistellessani selkeitä abstraktioita ominaisuuksia varten, joita ei lopulta tullut peliin lainkaan. Tästä esimerkkinä erilaiset ruututyypit sekä yksiköihin kohdistuvat monipuoliset toiminnot kuten hyökkääminen ja valtaaminen, joista hankkiuduin täysin eroon. Lisätoiminnallisuuksien sisällyttäminen on ehkä helpompaa, mutta siitä ei tämän projektin yhteydessä ole aivan äärettömästi iloa. Olisi siis ollut aikataulun kannalta hyödyllistä, jos olisin pystynyt kaventamaan tavoitteitani heti alkuun.
Suurin osa tekemistäni ratkaisuista ohjelman rakenteessa tuntuu jälkeenpäinkin onnistuneilta. Eritoten Layer-tietorakenteeseen olen tyytyväinen, sillä se mahdollisti varsin monipuolisen tavan käsitellä ja yhdistellä tietoja pelimaailmasta. Pettymysten joukkoon on laskettava pelimoottorin kerrosteinen event dispatching -mekanismi, josta en saanut ollenkaan niin luonnollisen ja loogisen oloista kokonaisuutta kuin olisin halunnut. Ratkaisu toki toimii, mutta arkkitehdin sydämeni tuntee piston pohtiessaan mahdollisia selkeyttä edistäviä refaktorointeja, joita en kuitenkaan nähnyt välttämättömäksi lähteä toteuttamaan.
Yhden kiireisen illan projektiksi jäänyt tekoäly on osoittautunut virkistäväksi ominaisuudeksi, jota ilman peli olisi ollut varsin raakileinen. Vaikka tekoäly on suorastaan naivistinen toteutukseltaan, olen siihen varsin tyytyväinen - suhteessa sijoitettuun aikaan siitä on ollut reilusti iloa jo nyt. En kadu lainkaan, että suuremmat suunnitelmani tekoälyalgoritmeja koskien jäivät tässä projektissa toteuttamatta.
Yksikkötestaus piti varsinaiset logiikkavirheet äärimmäisen vähissä. Pari ilmennyttä hairahdusta testattiin äkkiä nekin olemattomiin. Merkittävimmät ilmenneet bugit liittyivät muistinkäyttöön. Alkuperäinen reaaliaikainen renderöintimoodi vuoti tuhottomasti muistia, sillä Javan virtuaalikone ei tajunnut ajaa roskienkerääjää samalla kun peliluuppia suoritettiin. Tähän keksin lääkkeeksi tallentaa välimuistiin eniten vuotaneen Point-luokan objektit, joita kertyi heapiin nopeasti jopa satoja tuhansia. Samantapainen ongelma syntyi tekoälyn toteutuksen kanssa, kun suoritettavien toimintojen tuloksia arvioiva koodinpätkä joutui kloonaamaan pelitilannetta moneen otteeseen, mutta roskienkerääjä ei suostunut päästämään mäkeen heitetyistä klooneista irti. Päteväksi konstiksi osoittautui roskienkeruun pakottaminen käsin.
Vaikeusasteeltaan pahin asia koko projektissa oli WebStart-version toimintakuntoon saattaminen. Törmäsin aivan yllättäen rajoituksiin jar-paketoitujen tiedostojen lukutavassa ja Guicen vaatima suoritusoikeustaso aiheutti lisähuolia. Koska aihealue oli täysin vierasta, dokumentaatio puutteellista ja debug-työkalusto alkeellista, jouduin jonkin aikaa etenemään puhtaalla yritys-erehdys-menetelmällä. Selvisin kuitenkin - toisin kuin Appletin tekemisestä kurssin Java-tehtävissä.
Pelinkehitys toi oikein hyvää kokemusta paitsi itselleni aikaisemmin vieraasta Java-ympäristöstä myös siihen soveltuvista ketterän ohjelmistokehityksen välineistä kuten testaus- ja muista työkaluista. Tulin törmäilleeksi myös Javan rajoihin ja sain uutta perspektiiviä dynaamisempiin, funktionaalisimpiin kieliin Javan "puhtaaseen" olioihin nojaavan ohjelmointimallin siimeksestä. Eclipsen tarjoamat Javan staattisen tyypitykseen nojaavat automaattitäydennykset ja muut ominaisuudet tuntuivat yllättäen lopulta lähes erottamattomalta osalta koko ohjelmointiprosessia ja työnkulkua.
Web Start -version avulla olen jo ennen projektin päätöstä onnistunut keräämään positiivista käyttäjäpalautetta. Ilmeisesti onnistuin saamaan aikaan ainakin tyydyttävästi iskevän lautapelimäisiä piirteitä omaavan älypelin, jossa on haastetta pariksi hetkeksi viimeistään ihmisvastustajan seurassa. Tämä oli juuri se mitä haettiin. Koen projektin tarkoituksen tulleen täytetyksi.