Kirjoittaja: hunajavohveli (2005).
Opassarjan ensimmäisessä osassa saimme aikaan toimivan mallin siitä, miten luoda näytölle pelaajan liikuttama @-merkki. Jatkamme siitä, mihin jäimme, ja seuraava vaihe onkin luoda pelimaailma ukon ympärille. Pelimaailma luodaan taulukkoon, jonka mitat ovat 80x25 eli alkioita on yhtä monta kuin merkkejä näytöllä. Kutakin merkkiä vastaavaan alkioon tallennetaan luku, joka kertoo, mitä kyseisessä kohdassa on.
DIM Map(1 TO 80, 1 TO 25) AS INTEGER ' Luodaan taulukko Map(30, 10) = 1 ' Luodaan seinä kohtaan (30, 10)
Kokeillaanpa tällaista koodia. Aluksi luomme taulukon Map, jonka tyyppinä on pienin kokonaislukumuuttuja INTEGER
. Jokainen taulukon kohta voi saada kymmeniä tuhansia eri arvoja. Näin ollen on siis kymmeniä tuhansia eri mahdollisuuksia kunkin ruudun sisällölle, mikä on taatusti riittävästi. Taulukotkin voit nimetä, miten itse tahdot. Käytän tässä oppaassa niitä nimiä, joihin olen itse tottunut. Toisella rivillä asetamme alkion (30, 10) arvoksi yksi. Tässä esimerkissä sovimme, että yksi tarkoittaa seinää, ja nolla tyhjää. Pelkkä sopiminen ei tietenkään riitä, vaan se pitää myös koodata niin. :)
Kas niin. Nyt meillä on kenttää kuvaava taulukko, joten piirrämme seuraavaksi kentän taulukon arvojen mukaan.
DIM i AS INTEGER, i2 AS INTEGER ' Indeksimuuttujat CLS ' Tyhjätään näyttö FOR i2 = 1 TO 25 ' Käydään silmukoilla... FOR i = 1 TO 80 ' ...läpi jokainen ruutu LOCATE i2, i ' Asetetaan oikea kohta IF Map(i, i2) = 1 THEN PRINT "#"; ' Jos seinää, piirretään # NEXT i NEXT i2
Ensin määritellään indeksimuuttujat i ja i2 silmukoille, joiden avulla käydään läpi jokainen näytön ruutu. Huomaa, että i kuvaa x-akselia, ja i2 y-akselia, joten LOCATE
lle annamme ensin i2:n ja sitten vasta i:n. Jokaisen ruudun kohdalla tarkistetaan, onko sillä kohtaa taulukossa luku yksi. Ja jos taulukossa on yksi, piirretään seinä (merkki #), koska luku yksi merkitsee seinää. Useimmilla lienee tapana käyttää indeksimuuttujina kirjaimia i, j, k, l jne. Itse olen tottunut mielestäni loogisempaan sarjaan i, i2, i3 jne. Voit jälleen valita sen tavan, mikä itsestäsi tuntuu parhaimmalta.
Tässä vaiheessa alamme jakaa koodia aliohjelmiin ja teemme sellaisen myös tälle piirtokokonaisuudelle. Luomme siis aliohjelman (vaikkapa DrawMap-nimisen) ja siirrämme koodin siihen. Tällöin meidän on kuitenkin muutettava taulukko jaetuksi aliohjelmien kesken, mikä tehdään SHARED
illa. Eli muokkaamme aluksi luotua riviä pikkuisen:
DIM SHARED Map(80, 25) AS INTEGER
Sitten yhdistämme kaiken uuden oppaan ensimmäisessä osassa luotuun koodiin. Pääohjelmalistaus näyttää nyt siis tältä: (uudet rivit on merkitty tähdillä)
DECLARE SUB DrawMap () ' Aliohjelman määrittely * DIM SHARED Map(1 TO 80, 1 TO 25) AS INTEGER ' Luodaan taulukko * DIM x AS INTEGER, y AS INTEGER ' Pelaajan koordinaatit x = 40 y = 12 Map(30, 10) = 1 ' Luodaan seinä kohtaan (30, 10) * WIDTH 80, 25 CLS DrawMap ' Kutsutaan aliohjelmaa * DO ' Aloitamme pääsilmukan ' Ja tästä eteenpäin kaikki onkin jo tuttua
Ja aliohjelma tältä:
SUB DrawMap ' Aliohjelma alkaa DIM i AS INTEGER, i2 AS INTEGER ' Indeksimuuttujat CLS ' Tyhjätään näyttö FOR i2 = 1 TO 25 ' Käydään silmukoilla... FOR i = 1 TO 80 ' ...läpi jokainen ruutu LOCATE i2, i ' Asetetaan oikea kohta IF Map(i, i2) = 1 THEN PRINT "#"; ' Jos seinää, piirretään # NEXT i NEXT i2 END SUB ' Aliohjelma loppuu
Kun nyt kokeilet ajaa ohjelman, näet #-merkin ilmestyvän näytölle @:n seuraksi. Jos kuitenkin kävelet sitä päin, @-merkki vain liikkuu sen yli ja kumittaa sen. Meidän on siis luotava törmäystarkistus, jolla estämme tämän. Tarkistamme ensin, mihin ukko on liikkumassa. Sitten tutkimme, onko sillä kohtaa tyhjää vai kenties jokin este. Jos kohta vapaa, siirrämme ukon siihen. Muussa tapauksessa tietysti emme.
Muokkaamme siis hieman vanhaa koodia. Emme enää muutakaan suoraan ukon koordinaatteja, vaan tallennammekin uudet koordinaatit muuuttujiin ux ja uy (muista määritellä nämä uudet muuttujat koodin alussa samoin kuin x ja y). Muuttujat ux ja uy kertovat siis paikan, johon ukko on aikeissa siirtyä. Huomaa, että nyt täytyy määritellä molemmat koordinaatit, vaikka vain toista muutettaisiin. Jos x-koordinaattia muutetaan, y-koordinaatti pysyy tietysti samana ja päinvastoin. Jos koodasit omin päin myös vinottain liikkumisen ja nuolinäppäimet, muista vaihtaa ux ja uy myös niiden riveille.
ux = x ' Siltä varalta, ettei liikutakaan... uy = y ' ...pidetään samat koordinaatit SELECT CASE a$ CASE "4": ux = x - 1: uy = y ' Nyt uudet arvot tulevatkin CASE "6": ux = x + 1: uy = y ' muuttujiin ux ja uy CASE "8": uy = y - 1: ux = x CASE "5": uy = y + 1: ux = x ...
Jos nyt kokeilet ajaa ohjelman, et voi liikuttaa ukkoa, koska jokainen painallus muuttaa vain muuttujia ux ja uy. Seuraavaksi tarkistamme, onko näiden koordinaattien osoittamassa kohdassa estettä. Ja mikäli ei ole, muutamme x:n ja y:n vastaamaan näitä koordinaatteja, jolloin ukko liikkuu normaalisti. Jos kohdassa taas on este, varsinaisia koordinaatteja ei muuteta ja ukko pysyy siten paikallaan.
IF Map(ux, uy) = 0 THEN ' Jos kohdassa (ux, uy) on tyhjää... x = ux ' vaihdetaan varsinaiset y = uy ' koordinaatit END IF
Voit jälleen kokeilla ajaa koodin, ja jos kaikki toimii, kuten pitää, ukko ei tällä kertaa pääse seinän läpi. Voit testimielessä kokeilla vaikka arpoa seiniä sattumanvaraisesti ruudulle yksittäisen asettamisen sijaan. Kentän arpominen on järkevintä nyt, kun vasta testailemme pelimoottorin toimintaa. Arpomisesta ja satunnaisluvuista löydät lisää tietoa ohjelmointikielihakemistosta. Oppaan tulevissa osissa käsittelemme mahdollisesti kentän luomista loogisemmilla tavoilla ja/tai kentän lataamista tiedostosta. Elikkäs tämmöisen koodin voit pistää yksittäisen seinän luomisen tilalle:
FOR a = 1 TO 100 Map(INT(RND * 80) + 1, INT(RND * 25) + 1) = 1 ' Arvotaan paikka ja asetetaan kohtaan seinä NEXT a
Kutakuinkin tältä ohjelman pitäisi nyt näyttää:
Seinien lisäksi on hyvä huomioida myös ruudun reunat. Kenttähän on kokoa 80x25, joten pienin mahdollinen x:n arvo on 1 ja suurin 80. Vastaavasti y:n pienin mahdollinen arvo on 1 ja suurin 25. Meidän täytyy vain varmistaa, että pelaaja pysyy näiden rajojen sisäpuolella. Asia voidaan tarkistaa tällä tavalla:
IF ux < 1 OR ux > 80 OR uy < 1 OR uy > 25 THEN ux = x: uy = y ' Pidetään nykyiset koordinaatit
Ehtolauseke tarkistaa, ettei pelaaja liiku yhdenkään rajan yli. Eri rajojen tarkistukset erotellaan loogisella OR-operaattorilla. Joten jos yksikin rajoista on ylitetty, koko ehtolauseke on tosi. Tällöin muutetaan uusiksi koordinaateiksi nykyiset koordinaatit, jolloin pelaaja ei liiku mihinkään. Huomaa, että tämä tarkistus on sijoitettava koodiin ennen reunatarkistusta, sillä seinätarkistuksessa luetaan Map-taulukkoa, ja jos ux ja uy ovat jo silloin reunan ulkopuolella, tulee virhe yritettäessä lukea taulukkoa väärillä arvoilla. Jos taas reunatarkistus on ensimmäisenä, vialliset koordinaatit palautetaan edellisiksi, joiden kohdasta taulukkoa voidaan lukea. Lisää tämä tarkistus oikeaan kohtaan juuri ennen seinätarkistusta. Nyt pelaajan ei pitäisi päästä myöskään ruudun reunojen ulkopuolelle.
Huom! Törmäystarkistuksen voi toteuttaa myös niin, että laitetaan ensin muistiin ukon nykyiset koordinaatit, sitten liikutaan, ja jos uudessa paikassa onkin este, palautetaan vanhat koordinaatit takaisin. Periaatteessa tämä on aivan sama tapa kuin äsken esittelemäni, mutta ei välttämättä niin selkeä ymmärtää kuin ensimmäinen. Lisäksi se voi aiheuttaa myöhemmissä vaiheissa joitain ongelmia.
Koodia on nyt kertynyt jo useamman rivin verran. Koska koodin jokaista osa-aluetta tullaan tästä eteenpäin paisuttelemaan, tulee helposti hyvin epäselväksi pitää kaikki samassa pötkössä. Jaamme siis ohjelman pienempiin aliohjelmiin, joita kutsutaan pääohjelmassa vuorotellen siinä järjestyksessä, missä aliohjelmiin laitettavat kooditkin nyt ovat. Kävimme jo läpi aliohjelman luomisen kentän piirtämisen yhteydessä, joten homma sujunee helposti. Lähdetään liikkeelle koodin alusta. Ensimmäinen vastaan tuleva kokonaisuus, jolle kannattaa tehdä aliohjelma, on kentän arpominen. Luomme siis esimerkiksi RandomMap-nimisen aliohjelman ja sijoitamme koodin siihen:
SUB RandomMap FOR a = 1 TO 100 Map(INT(RND * 80) + 1, INT(RND * 25) + 1) = 1 ' Arvotaan paikka ja asetetaan kohtaan seinä NEXT a END SUB
Pääohjelmaan kirjoitamme koodin tilalle aliohjelmakutsun, joka tulee siis heti ennen DrawMap-kutsua.
... CLS RandomMap ' Arvotaan kenttä DrawMap ' Piirretään kenttä DO ' Pääsilmukka ...
Voit välissä varmistaa, että ohjelma toimii edelleen, kuten pitää. Sitten siirrymme eteenpäin. Ukon piirto ja kumitus hoituvat toistaiseksi melko lyhyesti, mutta näppäimen lukemisen voisimme laittaa aliohjelmaan ja tarkemmin sanottuna funktioon. Luomme funktion (vaikkapa nimellä GetKey$). Nimen perään tulee $, koska funktion tulee palauttaa merkkijono. Sitten siirrämme koodin funktioon.
FUNCTION GetKey$ DO a$ = INKEY$ LOOP UNTIL a$ <> "" GetKey = a$ ' Funktion palautusarvo END SUB
Funktiota kutsumme tällä tavalla:
a$ = GetKey ' Painettu näppäin välittyy a$:iin
Katsotaanpa nyt siis, mitä tapahtuu. Funktiota kutsuva rivi saa ohjelman ajamaan GetKey-funktion koodin. GetKey-funktiossa luetaan näppäin aivan normaalisti muuttujaan a$, kuten aikaisemminkin. Huomaa, että tämä ei kuitenkaan enää ole sama a$ kuin pääohjelmassa. Sillä aliohjelmilla ja pääohjelmalla on omat muuttujansa, vaikka muuttujat olisivat samannimiset (paitsi tietysti jos ne jaetaan SHARED
illa, kuten teimme Map-taulukolle). Funktioon laitamme kuitenkin yhden uuden rivin:
GetKey = a$ ' Funktion palautusarvo
Tämä rivi saa aikaan sen, että funktio palauttaa luetun näppäimen. Funktiota kutsuvan rivin kautta näppäin siis välittyy edelleen pääohjelman a$-muuttujaan, jossa sitä voidaan käyttää painetun näppäimen tarkistamiseen.
Teemme vielä yhden aliohjelman, joka on myös funktio. Yhdistämme törmäystarkistukset yhdeksi funktioksi. Annamme funktiolle parametriksi koordinaatit joihin pelaaja on aikeissa siirtyä (ux, uy). Funktion sisällä suoritetaan sekä reuna- että seinätarkistus. Kun tarkistukset on tehty, funktio palauttaa joko 0 tai -1. Jos tarkistuksessa ilmenee, että ruudussa on este, tai että se on näytön ulkopuolella, funktio palauttaa nollan. Muussa tapauksessa -1. Kutsumme funktiota tehtävänsä mukaisesti nimellä CanMove.
FUNCTION CanMove(x AS INTEGER, y AS INTEGER) CanMove = -1 ' Oletetaan aluksi, että ruutuun voi liikkua IF x < 1 OR x > 80 OR y < 1 OR y > 25 THEN ' Reunatarkistus CanMove = 0 ' Ei voikaan liikkua EXIT FUNCTION ' Pois funktiosta END IF IF Map(x, y) <> 0 THEN ' Seinätarkistus CanMove = 0 EXIT FUNCTION END IF END FUNCTION
Ensimmäisellä rivillä tietysti määritellään funktio ja ilmoitetaan sen parametrit, jotka ovat x ja y. Huom! Kyseessä ovat jälleen funktion sisäiset muuttujat, joilla ei ole mitään tekemistä pääohjelmassa esiintyvien pelaajan koordinaattien kanssa. Kun funktiota kutsutaan arvoilla ux ja uy, niiden arvot välittyvät funktion x- ja y-muuttujiin. Näin ollen joudumme vaihtamaan myös törmäystarkistusrivien muuttujat x:ksi ja y:ksi.
Aluksi oletetaan, että ruutuun voidaan liikkua, joten CanMoven palautusarvoksi asetetaan -1. Sitten suoritetaan tarkistukset. Jos tarkistuksissa ilmenee, ettei ruutuun voi liikkua, CanMoven arvoksi muutetaan nolla ja sen jälkeen funktiosta poistutaan EXIT FUNCTION
-komennolla. Sillä eihän meidän tarvitse enää tutkia, tapahtuuko seinätörmäystä, jos tiedämme jo, että koordinaatit ovat näytön ulkopuolella. Eli joka tapauksessa kulku siihen suuntaan estetään.
Seinätarkistusriviä joudumme myös hieman muokkaamaan. Aikaisemminhan tarkistimme sen avulla, onko ruutu, johon ollaan liikkumassa, tyhjä. Nyt tarkistammekin, onko ruudussa jokin este. Ja este on, mikäli ruutu ei ole tyhjä. Eli jos ruudun arvo on erisuuri kuin nolla. Tällöin ruudussa on este, ja CanMoven arvoksi asetetaan nolla. Saman tien myös poistutaan funktiosta.
Kun funktion suoritus lopulta päättyy, sen arvo on joko 0 tai -1, sen mukaan, onko sillä tarkistetussa ruudussa estettä vai ei. Sijoitamme funktiokutsun pääohjelmaan tällä tavalla:
IF CanMove(ux, uy) THEN ' Jos ruutuun voidaan liikkua x = ux y = uy END IF
Annamme funktiolle parametreiksi ux:n ja uy:n. Funktio tutkii, voidaanko ruutuun liikkua. Jos ruutuun voidaan liikkua, funktio palauttaa -1. Tällöin tapahtuu sama, kuin jos rivi olisi:
IF -1 THEN
Arvo -1 on ehtolausekkeelle sama kuin tosi, joten ehtolausekkeen koodi suoritetaan ja ux ja uy siirretään varsinaisiksi koordinaateiksi. Nolla taas on sama kuin epätosi, jolloin koodia ei suoriteta.
Pääohjelmalistauksen pitäisi näyttää kokonaisuudessaan nyt siis tältä:
' Aliohjelmamäärittelyt DECLARE SUB DrawMap () DECLARE FUNCTION GetKey$ () DECLARE SUB RandomMap () DECLARE FUNCTION CanMove! (x AS INTEGER, y AS INTEGER) DIM SHARED Map(1 TO 80, 1 TO 25) AS INTEGER ' Luodaan taulukko DIM x AS INTEGER, y AS INTEGER ' Pelaajan koordinaatit DIM ux AS INTEGER, uy AS INTEGER ' Uudet koordinaatit x = 40 y = 12 WIDTH 80, 25 CLS RandomMap ' Arvotaan kenttä taulukkoon DrawMap ' Piirretään kenttä taulukosta DO ' Pääsilmukka LOCATE y, x ' Piirretään ukko PRINT "@"; a$ = GetKey ' Luetaan näppäin funktiolla LOCATE y, x ' Kumitetaan ukko PRINT " "; ux = x ' Pysytään paikalla... uy = y SELECT CASE a$ CASE "4": ux = x - 1: uy = y ' ...ellei liikuta CASE "6": ux = x + 1: uy = y CASE "8": uy = y - 1: ux = x CASE "5": uy = y + 1: ux = x CASE CHR$(27): END END SELECT IF CanMove(ux, uy) THEN ' Jos ruutuun voi liikkua x = ux ' niin sitten liikutaan y = uy END IF LOOP
Kaikki muu on jaettu aliohjelmiin, jolloin koodi näyttää paljon selkeämmältä. Toivon mukaan ohjelma myös yhä toimii, kuten ennen aliohjelmiin jakamista. Jos havaitset virhetoimintaa, tutki ohjeita uudelleen ja yritä selvittää, missä kohtaa sattui virhe.
Tämän oppaan asiat on nyt käsitelty. Toivottavasti onnistut sisäistämään kaiken hyvin. Pelihahmo on tässä vaiheessa saatu liikkumaan ja törmäämään esteisiin. Mahdollisesti tulevissa osissa siirrytäänkin seuraavaksi tietokoneen ohjaamien hahmojen, hirviöiden yms. luomiseen. Jos sinua huvittaa, voit kokeilla omin päin, osaisitko lisäillä pelimoottoriin seuraavanlaisia asioita:
Valmiin koodin ja käännetyn ohjelman voit jälleen ladata tästä.
Hyvä opas! Onko tietoa, milloin seuraava osa olisi tulossa?
En osaa vielä sanoa, mutta jatkoa on lähes varmasti tulossa. Ei välttämättä aivan lähi aikoina, mutta pyrin jo alustavasti miettimään asioita, joita seuraava osa käsittelisi.
Erittäin hyvä opas! tässä kerrotaan kaikki mitä tarvitsee tietää.
tee sama opas C:lle.
C ei ole minulle vielä aivan tarpeeksi tuttu. Pystyisin sillä kyllä periaatteessa samaan, mutta kieli täytyisi saada rutiiniksi, ennen kuin rupeaa opasta tekemään. En ole lisäksi varma, mitä funktioita olisi järkevintä käyttää ei-rakenteellisiin asioihin (lähinnä tekstintulostus). Pienimuotoista pseudo-versiota voisin kyllä harkita jossain välissä.
Itseäni härnäsi tuo ux, uy tapa hoitaa asiat :) Itse kun tile pohjaisia pelejä kirjoittelen määrittelen usein vanhat x ja y koordinaatit. Hoituuhan tuo noinkin, kas kun en itse ole koskaan ajatellut :/
Itsekin kieltämättä olen aina käyttänyt tuota mainitsemaasi tapaa, mutta nyt kun kirjoitin asiasta oppaan, niin päätin miettiä uudestaan, miten päin se olisi järkevämpi toteuttaa. Tämä tuntui minusta paremmalta, kun otetaan huomioon, että liikkumisen voi estää moni eri asia. Toki saman voi tehdä myös vanhojen koordinaattien periaatteella, mutta mielestäni hieman purkkamaisemmin.
joo... on hyvä opas :) lisää tämmöisiä....
yksi pikku pulma tuli ton "satunnaispaikkojen" kanssa...
koodistasi puuttui nimittäin "RANDOMIZE TIMER"-juttu
muuten koodi oli hyvää ja selityksetkin löytyivät.....
d_((^.^))_b
Satunnaisluvuista en viitsinyt selittää tämän enempää ja RANDOMIZE TIMER löytyy kyllä antamastani linkistä. :)
Varsin selkeää ja mukavaa luettavaa.
Koko ruudun täyttämisen lisäksi voisi olla hyvä ottaa esille miten luoda maailma jota liikutella sellaisessa 25x25 "boksissa" niin että ukko on keskellä boksia ja maailma liikkuu, ei ukko tai boksi.
Itse teen tämän omalla tavallani mutta olisi mukava nähdä miten muut sen tekee :)
Itse laiskuuttani käytän IF tyyliä mutta pakko myöntää että tuo SELECT CASE on nätimpää...taidanpa tehdä "pikku" muutoksen omaan ohjelmaani =)
Tuon piirtotavan, että "kamera" seuraa ukkoa, joka näyttää siten pysyvän keskellä, esittelen kyllä myöhemmissä osissa, jos innostun sellaisia kirjoittamaan. IF:ä käytän oikeastaan aika paljon itsekin, mutta oppaaseen päätin laittaa siistimmän tavan. :)
Eikös QB:ssä ne kaksi viimeistä riviä jää aina tyhjäksi sitä "Press any key to continue"?
joo... tahtoo nähä seuraavan osan (mikäli aiot/ehdit tehdä sellaista muista kiireistäsi huolimatta)
PC-Master kirjoitti:
Eikös QB:ssä ne kaksi viimeistä riviä jää aina tyhjäksi sitä "Press any key to continue"?
Ne rivit lisää ainoastaan QB:n tulkki, kun ohjelman suoritus on loppunut. Ne eivät kuulu itse ohjelmaan, eivätkä näy käännetyssä exe:ssä.
Hieno opas.Olisin tosin tarvinnut tuota vuosi sitten.
kympin opas ;P
Milloins se seuraava osa tulee? Ei sillä sinänsä ole minulle enään mitään käyttöä, muutakun tarkistus mielessä. Kun jo netistä löytyi ihan hyvä englannin kielinen opas ^^
Kiitokset hunajavohveli! Tuli tarpeeseen.
Opin tuosta ainakin jotain.. Hieno opas!
Erittäin hyvä opas!
Pääsee aloittelijakin käsiksi peliohjelmointiin
Khaoralius, laitatko linkin eng-kiel oppaaseen
Miten ton saa käännetyks exelle?
Miks toi sanoo koko ajan että "Argumen-count mismatch"
Mä tein tuosta semmoosen version johona on 80x50 tekstitila
Todella mainio opas! Mielenkiintoista luettavaa itsellekin. Ohjelmoin tuossa taannoin matopelin JavaScriptillä (testimielessä ja huvinvuoksi). Tuli ihan toimiva, vaikka en varmasti parhaita ratkaisuja osannutkaan käyttää kaikissa tilanteissa. Ehkä tämän opassarjan lukemisen jälkeen osaan koodailla paremman matopelin ja kenties jotain muutakin! :) Jatkoakin tälle opassarjalle olisi kiva saada.
saisk tätä mitenkään javalle???
toivottavasti seuraavissa oppaissa kerrotaan miten saa ns. cheat jutut toimimaan.
Huomio! Kommentoi tässä ainoastaan tämän oppaan hyviä ja huonoja puolia. Älä kirjoita muita kysymyksiä tähän. Jos koodisi ei toimi tai tarvitset muuten vain apua ohjelmoinnissa, lähetä viesti keskusteluun.