Kirjautuminen

Haku

Tehtävät

Opasarkisto: Merkkigrafiikkapelien ohjelmointi QBasicilla: Osa 1 - Pelimoottorin alkeet

  1. Osa 1 - Pelimoottorin alkeet
  2. Osa 2 - Alkeellinen pelimaailma
  3. Osa 3 - Hirviöiden luominen

Kirjoittaja: hunajavohveli. Vuosi: 2005.

Tämä opas(sarja) käsittelee tilepohjaisen pelin toteuttamista merkkigrafiikalla. Tilepohjainen peli tarkoitaa peliä, jossa pelimaailma koostuu ruuduista ja hahmot liikkuvat pykälittäin ruudusta toiseen. Merkkigrafiikalla toteutetut pelit eroavat muista tilepohjaisista peleistä vain siten, että ruudut piirretään tavallisen grafiikan sijaan merkeillä. Tutuimpia tällaisista peleistä ovat mm. Nethack, Rogue ja ADOM. Joten jos olet pelannut yhtäkin näistä, tiedät mistä on kyse (Ja jos et ole, niin suosittelen että kokeilet, jos haluat opetella tekemään vastaavia tämän oppaan avulla). Pelimoottorin yksityiskohtaisen kehityksen yhteydessä käsittelemme myös asioita, joita tarvitaan useissa muissakin peleissä, kuten esimerkiksi ukon liikuttamista näytöllä, törmäystarkistuksia, pelin tallentamista ja lataamista yms.

Esimerkki pelistä Ancient Domains of Mystery:

Mitä pitäisi jo osata?

Ennen tällaisen pelin aloittamista sinulla pitäisi olla jonkinlainen pohja valmiina ohjelmoinnista yleensä. Lähdemme tässä liikkeelle aika lailla alkeista, joten valtavaa määrää kokemusta ei vaadita. Mikäli oppaassa käsiteltävät asiat kuitenkin tuntuvat sinusta vierailta, suosittelen lukemaan ensin Ohjelmointiputkan Aloittelijan QBasic-oppaan. Tuossa oppaassa esiteltävät asiat, muuttujat, ehtolausekkeet, silmukat ym. tärkeät rakenneasiat sinun tulisi hallita. Jos ne ovat hallussasi, voit siirtyä eteenpäin:

Pelin rakenteen suunnittelu

Yritämme aluksi luoda yksinkertaisen esimerkin, jossa pelaaja pystyy liikuttamaan @-merkkiä näytöllä, kuten useimmissa tämänlaisissa peleissä. Aluksi on hyvä tehdä suunnitelma siitä, mitä pelissä tapahtuu ja missä järjestyksessä. Peruslista suoritettavista asioista on seuraavanlainen:

  1. Piirrämme pelaajan liikuttaman ukon näytölle.
  2. Annamme pelaajan painaa näppäintä, jolla hän ohjaa ukkoa.
  3. Kumitamme ukon näytöltä.
  4. Liikutamme ukon paikkaan, johon pelaaja sen näppäimellä ohjasi siirtyvän.

Neljännen kohdan jälkeen siirryymme jälleen kohtaan yksi, jolloin ukko piirretään uuteen paikkaan, siihen, johon pelaaja sen halusi siirtyvän.

Ukon piirtäminen

Nyt käymme läpi, miten edellä mainitut neljä kohtaa toteutetaan koodina. Ennen näitä kohtia luomme kuitenkin kaksi muuttujaa, x ja y, joihin tallennamme ukon koordinaatit näytöllä. Asetamme näiden muuttujien arvoiksi aluksi 40 ja 12, jolloin ukko on suurin piirtein keskellä näyttöä. WIDTH-komento varmistaa, että kyseessä on 80x25-suuruinen tekstitila.

DIM x AS INTEGER, y AS INTEGER   ' Esittelemme muuttujat ja määritämme niiden tyypiksi kokonaisluvun (INTEGER)
x = 40                           ' Asetamme x:n arvoksi 40
y = 12                           ' Ja y:n arvoksi 12
WIDTH 80, 25                     ' 80x25 tila
CLS                              ' Näyttö on myös hyvä tyhjätä kaiken varalta

Siirrymme listan ensimmäiseen kohtaan eli ukon piirtämiseen. Tunnet oletettavasti PRINT-käskyn, jolla tulostetaan tekstiä näytölle. Käytämme tätä tuiki tavallista käskyä myös tässä, mutta meidän täytyy myös kertoa käskylle, että haluamme tulostaa siihen kohtaan, missä ukko on. Tähän käytämme LOCATE-käskyä. LOCATE-käskylle annetaan (tässä tapauksessa) kaksi parametriä, rivi ja sarake. Meidän tapauksessamme rivi on sama kuin ukon y-koordinaatti, ja sarake sama kuin x-koordinaatti. Huomaa, että rivi annetaan ennen saraketta, joten ensin annamme y:n ja sitten vasta x:n.

LOCATE y, x    ' Asetamme PRINT-käskyn tulostamaan oikeaan kohtaan
PRINT "@";     ' Tulostamme @-merkin tähän kohtaan

Tässä vaiheessa voit yhdistää nämä läpikäydyt kaksi koodinpätkää ja todeta, että merkki todellakin ilmestyy suunnilleen keskelle näyttöä. Jos näin ei tapahdu, käy nämä ohjeet läpi uudelleen ja katso, mikä meni vikaan. Puolipiste PRINT:n perässä estää rivinvaihdoin. Tämä siksi, että muuten aiheutuisi ongelmia liikuttaessa ruudun alalaidassa (minkä tosin estämme mahdollisesti tulevissa osissa).

Näppäimen lukeminen

Tämän jälkeen siirrymme kohtaan kaksi eli annamme pelaajan painaa näppäintä, jonka on tarkoitus ohjata ukkoa. Tunnet arvatenkin INPUT-käskyn, joka antaa pelaajan kirjoittaa luvun tai tekstiä, joka tallentuu muuttujaan. Tässä tilanteessa emme kuitenkaan voi käyttää INPUT:ia, jos haluamme, että pelkkä näppäimen painallus riittää jatkamaan suoritusta. INPUT:han vaatii aina Enterin painalluksen kuittaukseksi. Siksi luemme näppäimen INKEY$-funktiolla. Tämä funktio ei kuitenkaan keskeytä ohjelman suoritusta siksi aikaa, kunnes näppäintä on painettu, joten tämä keskeytys meidän on tehtävä itse. Keskeytyksen teemme silmukalla, josta poistutaan vasta, kun näppäintä on painettu.

DO                     ' Aloitetaan silmukka
  a$ = INKEY$          ' Luetaan painettu näppäin muuttujaan a$
LOOP UNTIL a$ <> ""    ' Poistutaan, kun jotain näppäintä on painettu

Luomme siis DO-silmukan, jota toistetaan niin kauan, kunnes a$ on erisuuri kuin ei-mitään eli toisin sanoen niin kauan, kunnes a$:ssa on jokin arvo. Ja koska a$:n arvo otetaan INKEY$:sta, silmukka loppuu vasta, kun INKEY$ saa jonkin arvon, mikä taas tapahtuu vain, kun jotain näppäintä painetaan. Niin kauan, kuin mitään näppäintä ei paineta, INKEY$:ssä ei ole mitään arvoa eikä silmukasta päästä pois (Pääsilmukan sisään tulee siis toinen DO-silmukka. Älä sekoita niitä keskenään). Itse olen tottunut käyttämään a$-nimistä muuttujaa kysyttäessä näppäintä, mutta sillehän voit tietysti pistää ihan sen nimen, minkä itse haluat. Käyttäjän painaman näppäimen näppäinkoodi on nyt siis tallessa muuttujassa, jossa se saa odottaa, kunnes sitä tarvitaan.

Ukon kumittaminen

Tässä välissä siirrymme kohtaan kolme eli ukon kumittamiseen. Muutenhan näytölle tulisi aina vain lisää ukkoja, kun ukko jäisi näkyviin jokaiseen kohtaan, missä se on ollut. Ukon voimme kumittaa aivan samalla tavalla, kuin piirsimmekin sen. Ukon tilalle vain tulostetaan tällä kertaa välilyönti, joka on siis tässä esimerkissä sama merkki kuin taustassa.

LOCATE y, x          ' Asetetaan tekstintulostus jälleen oikeaan kohtaan
PRINT " ";           ' Tulostetaan välilyönti ukon paikalle

Kaikki tapahtuu kuitenkin niin nopeasti, ettei pelaaja ehdi huomata, että ukko välissä katoaa, ennen kuin se piirretään uudelleen (Tässä vaiheessa voit jälleen yhdistää koodirivit peräkkäin ja ajaa ohjelman. Tässä vaiheessa se siis piirtää ukon näytölle, antaa painaa jotain näppäintä ja kumittaa sitten ukon näytöltä).

Ukon siirtäminen uuteen paikkaan

Nyt kun ukko on siis kumitettu nykyisestä paikastaan, on aika siirtää se uuteen paikkaan, sen mukaan, mitä näppäintä pelaaja painoi. Miten ukkoa sitten liikutetaan? Se tehdään muuttamalla sen koordinaatteja. Koordinaatit ovat siis merkkigrafiikan tapauksessa rivejä ja sarakkeita.
Tekstitila on kutakuinkin tällainen:

Kuten kuvasta näkyy, x-koordinaatti kasvaa oikealle päin mentäessä ja y-koordinaatti alaspäin mentäessä, joten jos haluamme liikuttaa ukkoa oikealle, kasvatamme x-koordinaattia. Ja jos taas haluamme liikuttaa vasemmalle, niin päinvastoin vähennämme x-koordinaattia. Huomaa, että normaalissa matemaattisessa koordinaatistossa arvot kasvavat ylöspäin mentäessä ja vähenevät alaspäin mentäessä, mutta näytöllä tämä tapahtuu toisinpäin eli alaspäin mentäessä y-koordinaatti kasvaa.

x = x + 1

Tällaisella koodilla voimme kasvattaa muuttujan arvoa yhdellä. Jos x on tässä tilanteessa vaikkapa 40, tämä luku sijoitetaan tuohon koodiin, jolloin se olisi sama kuin:

x = 40 + 1

Ja 40 + 1 on tietysti 41, ja tämä luku sijoitetaan nyt muuttujaan sen edellisen arvon tilalle. X-koordinaatti kasvaa, ja kun ukko seuraavan kerran piirretään, se näyttää liikkuneen yhden pykälän oikealle. Sama temppu tehdään myös y-koordinaatille. Nyt meidän on vain ehtolausekkeella tarkistettava, mitä näppäintä pelaaja painoi, ja sen mukaan kasvatettava tai vähennettävä oikeaa koordinaattia. Tarkastellessa yhden muuttujan arvoja on yleensä hyvä idea käyttää CASE-rakennetta.

SELECT CASE a$
CASE "4"
  x = x - 1   ' Painettiin 4, liikutaan vasemmalle
CASE "6"
  x = x + 1   ' Painettiin 6, liikutaan oikealle
CASE "8"
  y = y - 1   ' Painettiin 8, liikutaan ylöspäin
CASE "5"
  y = y + 1   ' Painettiin 5, liikutaan alaspäin
CASE CHR$(27)
  END         ' Lopetetaan ohjelman suoritus
END SELECT

Viimeinen vaihtoehto CHR$(27) tarkoitaa Esc-näppäintä. CHR$-funktio palauttaa parametriksi annettua numeroa vastaavan merkin. Muiden hyödyllisten näppäimien numerokoodeja löytyy tästä taulukosta. Jos haluat koodista tiiviimpää, voit halutessasi kirjoittaa sen vaikkapa näin:

SELECT CASE a$
  CASE "4": x = x - 1    ' Painettiin 4, liikutaan vasemmalle
  CASE "6": x = x + 1    ' Painettiin 6, liikutaan oikealle
  CASE "8": y = y - 1    ' Painettiin 8, liikutaan ylöspäin
  CASE "5": y = y + 1    ' Painettiin 5, liikutaan alaspäin
  CASE CHR$(27): END     ' Lopetetaan ohjelman suoritus
END SELECT

Koodin kokoaminen

Nyt koodi on valmis, joten voimme yhdistää sen. Koko koodissa olemme siis piirtäneet ukon, kysyneet näppäintä, kumittaneet ukon ja siirtäneet sen uuteen paikkaan, joten voimme jälleen aloittaa koko koodin alusta ja piirtää ukon uuteen paikkaan. Tämän teemme DO-silmukalla. Eli lisäämme toistettavan koodin alkuun DO ja loppuun LOOP. On myös hyvä osata sisentää koodi, niin siitä tulee helpommin luettavaa. Perussääntöihin kuuluu mm. silmukan sisäisen koodin sisentäminen, mutta niistä voi toki halutessaan poiketa. Kokonaisuutena koodi näyttäisi siis tältä:

DIM x AS INTEGER, y AS INTEGER   ' Esittelemme muuttujat ja määritämme niiden tyypiksi kokonaisluvun (INTEGER)
x = 40                           ' Asetamme x:n arvoksi 40
y = 12                           ' Ja y:n arvoksi 12
WIDTH 80, 25                     ' 80x25 tila
CLS                              ' Tyhjäämme näytön

DO                               ' Aloitamme silmukan

  LOCATE y, x                    ' Asetamme PRINT-käskyn tulostamaan oikeaan kohtaan
  PRINT "@";                     ' Tulostamme @-merkin tähän kohtaan

  DO                             ' Aloitetaan silmukka
    a$ = INKEY$                  ' Luetaan painettu näppäin muuttujaan a$
  LOOP UNTIL a$ <> ""            ' Poistutaan, kun jotain näppäintä on painettu

  LOCATE y, x                    ' Asetetaan tekstintulostus jälleen oikeaan kohtaan
  PRINT " ";                     ' Tulostetaan välilyönti ukon paikalle

  SELECT CASE a$
    CASE "4": x = x - 1          ' Painettiin 4, liikutaan vasemmalle
    CASE "6": x = x + 1          ' Painettiin 6, liikutaan oikealle
    CASE "8": y = y - 1          ' Painettiin 8, liikutaan ylöspäin
    CASE "5": y = y + 1          ' Painettiin 5, liikutaan alaspäin
    CASE CHR$(27): END           ' Lopetetaan ohjelman suoritus
  END SELECT

LOOP                             ' Silmukka päättyy ja hyppäämme jälleen DO-riville, josta suoritus jatkuu

Kun ajat ohjelman, ruudulle ilmestyy @-merkki, jota voi nyt siis liikuttaa näppäimillä 4, 6, 8 ja 5. On tärkeää, että ymmärrät tähän asti käsitellyt asiat etkä vain kopioi suoraan tätä valmista koodia. Sillä tämä on pohja kaikille tulevaisuuden lisäyksille ja on siksi syytä osata hyvin.

Nuolinäppäimet

Haluaisit kenties, että ukkoa voisi ohjata myös nuolinäppäimillä. Näillekin näppäimille on olemassa omat numerokoodinsa, jotka löytyvät em. taulukosta. Nuolinäppäinten koodit koostuvat kahdesta merkistä. Merkkiyhdistelmät ovat seuraavanlaiset:

(taulukossa myös kirjaimet on esitetty niiden ASCII-koodeina)
Vasen: CHR$(0) + "K"
Oikea: CHR$(0) + "M"
Ylös: CHR$(0) + "H"
Alas: CHR$(0) + "P"

Sinun tarvitsee vain vaihtaa tai lisätä nämä näppäimet CASE-rakenteeseen tähän tapaan:

SELECT CASE a$
  CASE "4", CHR$(0) + "K": x = x - 1          ' Painettiin 4, liikutaan vasemmalle
  CASE "6", CHR$(0) + "M": x = x + 1          ' Painettiin 6, liikutaan oikealle
  CASE "8", CHR$(0) + "H": y = y - 1          ' Painettiin 8, liikutaan ylöspäin
  CASE "5", CHR$(0) + "P": y = y + 1          ' Painettiin 5, liikutaan alaspäin
  CASE CHR$(27): END                          ' Lopetetaan ohjelman suoritus
END SELECT

Lisäyksiä ja huomautuksia

Nyt olemme kasanneet peruspohjan pelimoottorille, jota kehitämme oppaan seuraavassa osassa. Jos aliohjelmat, funktiot ja taulukot tai niiden käyttäminen tuntuvat vierailta, sinun kannattaa lukea em. alkeisopasta ja harjoitella näiden asioiden käyttämistä. Tulevaisuudessa koodi nimittäin kasvaa, ja tulemme siksi pilkkomaan sen aliohjelmiksi selkeyden vuoksi.

Lopuksi sanon vielä, että monet merkkigrafiikkapelien harrastajat eivät juuri käytä nuolinäppäimiä, koska lähes koko ajan on tarve liikkua myös vinottain eli näppäimillä 1, 3, 7 ja 9. Nekin on siis hyvä lisätä CASE-rakenteeseen.

CASE "9"                         ' Vaikkapa näin
  x = x + 1
  y = y - 1

CASE "9": x = x + 1: y = y + 1   ' Tai näin

Kaksoispiste erottelee lausekkeet toisistaan, ja molemmat suoritetaan vain, jos painettiin 9. Tuosta ymmärrät luultavasti idean, joten saat yrittää omin päin koodata kolme muuta näppäintä. Mietit vain, pitääkö koordinaatteja vähentää vai kasvattaa. Mikäli hallitset satunnaisluvut, voit ennen opassarjan seuraavaa osaa myös yrittää tehdä koodin, joka saa ukon aloittamaan aina satunnaisesti eri paikasta näytöllä.

Valmiin koodin ja käännetyn ohjelman ilman em. lisäyksiä voit ladata tästä.


Kommentit

tgunner [10.04.2005 18:15:28]

Lainaa #

No niin juuri tälläistä odotettiinkin, mäkin oon tekemässä merkkigrafiikka peliä mutta teen tuon liikkumisen aivan eri tavalla.

hunajavohveli [10.04.2005 20:18:29]

Lainaa #

Minkäslaista tapaa käytät?

tgunner [11.04.2005 09:09:39]

Lainaa #

No se on kyllä huonompi menetelmä, mutta kun kerran aloitin sillä en tohtisi kirjoittaa uutta. Siinä ei käytetä CASE komentoa vaan IF :D, ja se looppaa kokoajan, joten jos pelaaja ei paina mitään se siltikin looppaa.

herkko [17.04.2005 01:14:52]

Lainaa #

Eli peliruutu "vilkkuu" kokoajan? Huh.

msdos464 [17.05.2005 09:24:26]

Lainaa #

Jos liikkuu nopeesti niin vois näyttää paremmalta jos eka piirretään uus ukko ja vasta sitten pyyhitään edellinen...

Puhveli [22.05.2005 14:41:37]

Lainaa #

näin

herkko kirjoitti:

Eli peliruutu "vilkkuu" kokoajan? Huh.

Voihan sen estää käyttämällä eri muistisivuja ja PCOPYä

hunajavohveli [29.05.2005 23:10:01]

Lainaa #

Mutta jatkuva luuppaaminen ei siltikään kannata, ellei peli ole reaaliaikainen, ts. elleivät muut pelihahmot voi liikkua silloinkin, kun pelaaja ei paina mitään.

squid [14.10.2005 09:15:23]

Lainaa #

Tuossahan on ideaa :)

Tore [06.11.2005 11:57:20]

Lainaa #

mites tuolle ukolle tehää seinä niinku ettei pääsis joka paikasta läpi.. vai lukiks jottai semmosta tuolla

hunajavohveli [04.12.2005 16:16:12]

Lainaa #

Katsopa opassarjan seuraava osa.

Nitros [31.12.2005 12:09:28]

Lainaa #

Mulla on 1.1 versio ja se ei haluu tehä noita $ ja @ ja muita vastaavia merkkejä miksi`?

Mobel [02.05.2006 20:15:58]

Lainaa #

Hyvä opas!
Nitro´s:
Hanki jostain uudempi versio.

Juhko [17.07.2006 21:40:11]

Lainaa #

Kiitti! Täst oppaast oli hyötyy! Mä osaan QB:tä jo kyllä muutenki aika hyvin mut mä opin täst viel uutta!

Codeprofile [17.12.2006 22:13:23]

Lainaa #

Nonniin! Nyt tuli jälleen yksi omista projekteistani pelastettua!
Ja totta tosiaan: tästähän oli mulle muutenkin hyötyä!

EDIT: Ja nyt tuli yksi mshta-sovellus omalle koneelle lisää. Keräilen oppaita omalle koneelle, jotta voisin niitä myöhemmin lukea.

Herrasir [09.08.2007 16:27:21]

Lainaa #

Hunajavohveli kirjoitti:

Lopuksi sanon vielä, että monet merkkigrafiikkapelien harrastajat eivät juuri käytä nuolinäppäimiä, koska lähes koko ajan on tarve liikkua myös vinottain eli näppäimillä 1, 3, 7 ja 9. Nekin on siis hyvä lisätä CASE-rakenteeseen.

Ajattelin vain, että näinkin voi tehdä:

SELECT CASE A$
    CASE "4", CHR$(0) + "K": ux = x - 1: uy = y          'vasemmalle
    CASE "6", CHR$(0) + "M": ux = x + 1: uy = y          'oikealle
    CASE "8", CHR$(0) + "H": uy = y - 1: ux = x          'ylös
    CASE "5", CHR$(0) + "P": uy = y + 1: ux = x          'alas
    CASE "5+6", CHR$(0) + "M+P": ux = x + 1: uy = y + 1      'oikealle alas"
    CASE "4+8", CHR$(0) + "K+H": ux = x - 1: uy = y - 1      'vasemmalle ylös
    CASE "6+8", CHR$(0) + "M+H": ux = x + 1: uy = y - 1      'oikealle ylös
    CASE "5+4", CHR$(0) + "K+P": uy = y + 1: ux = x - 1      'vasemmalle alas
  END SELECT

Eli liikkuminen nuolinäppäimillä ylä- ja alaviistoon.

EDIT: Ei näköjään toimi kaikilla koneilla toivotulla tavalla.

New Samppi [06.01.2009 12:19:21]

Lainaa #

Joo ei kuule toimi!

sammakkomies [30.11.2009 23:12:19]

Lainaa #

mie oon tuhma ja muutan @ merkin ¥ merkiksi.

Kirjoita kommentti

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.

Muista lukea kirjoitusohjeet.
Tietoa sivustosta