Grafik i Racket. Rasmus Andersson. Senast uppdaterad 30 mars 2015

Relevanta dokument
Grafik i DrRacket AV TOMMY KARLSSON

Lab5 för prgmedcl04 Grafik

Institutionen för matematik och datavetenskap Karlstads universitet. GeoGebra. ett digitalt verktyg för framtidens matematikundervisning

Föreläsning 5-6 Innehåll. Exempel på program med objekt. Exempel: kvadratobjekt. Objekt. Skapa och använda objekt Skriva egna klasser

Föreläsning 5-6 Innehåll

3. Välj den sprajt (bild) ni vill ha som fallande objekt, t ex en tårta, Cake. Klicka därefter på OK.

Objektorienterad programmering i Racket

Objektorienterad programmering i Racket

Börja med att kopiera källkoden till din scheme-katalog (som du skapade i Laboration 1).

GeoGebra i matematikundervisningen - Inspirationsdagar för gymnasielärare. Karlstads universitet april. Liten introduktionsguide för nybörjare

Tentamen i TDP004 Objektorienterad Programmering Praktisk del

6. Nu skall vi ställa in vad som skall hända när man klickar på knappen samt att markören skall ändra sig till en hand när markören är på knappen.

Detaljbeskrivning av Player

TUTORIAL: KLASSER & OBJEKT

Programmering A C# VT Ett kompendie över Programmering A (50p) i c# Stefan Fredriksson

TDP005 Projekt: Objektorienterat system

Programmering. Den första datorn hette ENIAC.

Manual till Båstadkartans grundläggande funktioner

Mikael Bondestam Johan Isaksson. Spelprogrammering. med CDX och OpenGL

3.5 Visuell programmering

Gran Canaria - Arbetsbeskrivning knapplänkar (Mediator 8)

Tentamen, EDAA20/EDA501 Programmering

Tentamen i TDP004 Objektorienterad Programmering Praktisk del

Rullningslisten. Klicka på rullningslistpilar (pil upp eller pil ner) 1 för att förflytta dig i önskad riktning, en liten bit i taget.

Programmeringsteknik II - HT18. Föreläsning 6: Grafik och händelsestyrda program med användargränssnitt (och Java-interface) Johan Öfverstedt

Vi börjar med en penna som ritar när du drar runt den på Scenen.

Polygoner. Trianglar på tre sätt

Scratch Junior. makeandshape.com. by MIT. Gränssnitt Scratch Junior

Labb i Datorsystemteknik och programvaruteknik Programmering av kalkylator i Visual Basic

EDAA20 Programmering och databaser. Mål komprimerat se kursplanen för detaljer. Checklista. Föreläsning 1-2 Innehåll. Programmering.

Högskolan Dalarna sid 1 av 7 DI-institutionen Hans-Edy Mårtensson Sten Sundin

Laboration: Grunderna i MATLAB

Exempel på användning av arv: Geometriska figurer

Att prova på en enkel Applet och att lära sig olika sätt att hämta data från tangentbordet. Du får även prova på att skapa din första riktiga klass.

Laboration: Grunderna i Matlab

Objektorientering i liten skala

Optisk bänk En Virtuell Applet Laboration

Alla datorprogram har en sak gemensam; alla processerar indata för att producera något slags resultat, utdata.

Programmering. Scratch - grundövningar

Aktivitetsbank. Matematikundervisning med digitala verktyg II, åk 1-3. Maria Johansson, Ulrica Dahlberg

Manual TorTalk version 1.3

Lathund CallCenter 2010

Elevuppgift: Bågvinkelns storlek i en halvcirkel

Switch Driver 4. Programvara för Radio Switch, JoyBox och JoyCable. Sensory Software

Föreläsning 13 Innehåll

Fyra i rad Javaprojekt inom TDDC32

Manual TorTalk Mac 1.0

NU NÄR DU BEKANTAT DIG MED RAMARNAS EGENSKAPER OCH VET. hur man markerar och ändrar dem, är det dags att titta lite närmare på

Starta ett fönster... Hur håller tkinter reda på musklick? Olika sätt att organisera fönsterinnehåll. Och för att placera våra widgets

Grafiska användargränssnitt i Java

Träff 1 Skissa & Extrudera

Lär dig programmera! Prova på programmering med enkla exempel! Björn Regnell

Mathematica. Utdata är Mathematicas svar på dina kommandon. Här ser vi svaret på kommandot från. , x

Malmö högskola 2007/2008 Teknik och samhälle

Kort om klasser och objekt En introduktion till GUI-programmering i Java

3. Hämta och infoga bilder

Kom igång med. Windows 8. DATAUTB MORIN AB

KPP053, HT2015 MATLAB, Föreläsning 1. Introduktion till MATLAB Skript Inläsning och utskrift av variabler Ekvationssystem Anonyma funktioner

Grafiska användargränssnitt i Java

Image Converter. Användarhandbok. Innehåll: Version: Läs följande innan du använder Image Converter. Översikt av Image Converter

Bilaga 4, Skapa grafiskt användargränssnitt med guide

Fönster och dörr. Kapitel 3 - Fönster och dörr... 3

På den här övningen ska vi träna på att förflytta en figur med hjälp av piltangenterna.

Tentamen i TDP004 Objektorienterad Programmering Praktisk del

Manual fö r webbkartörnas grundla ggande funktiöner

1. Öppna InteraktivBok.fla och Main.as från förra övningen. 2. Gå till Main.as. Den bör se ut som följer:

Innehåll INNEHÅLL. Teckenförklaring Komma igång

HI1024 Programmering, grundkurs TEN

Konvexa höljet Laboration 6 GruDat, DD1344

Tynker gratisapp på AppStore

Fönsterbeteende. Mike McBride Jost Schenck Översättare: Stefan Asserhäll

FÖRSLAG TILL LÖSNINGAR FÖR TENTAMEN I INTERNETPROGRAMMERING MED JAVA, 5p för SY , kl

TANA17 Matematiska beräkningar med Matlab

Detta dokument är ett exempel, cirka andra hälften av en tentamen för TDA545 Objektorienterad programvaruutveckling

Manual till webbkartornas grundläggande funktioner

Guide till att använda Audacity för uttalsövningar

F8 - Arv. ID1004 Objektorienterad programmering Fredrik Kilander

Kravspecifikation TDP005 Projekt: Objektorienterat system

SCENER. Att ändra i en scen

Objektorientering: Lagring och livstid

public och private Obs: private inte skyddar mot access från andra objekt i samma klass.

Laboration 4: Digitala bilder

KPP053, HT2016 MATLAB, Föreläsning 1. Introduktion till MATLAB Skript Inläsning och utskrift av variabler Ekvationssystem Anonyma funktioner

Manual för banläggning i OCAD IF ÅLAND

Datorövning 1 Calc i OpenOffice 1

Föreläsning 4. Klass. Klassdeklaration. Klasser Och Objekt

ANVÄNDARGUIDE. ViTex

Sida Kapitel 3 Fönster och dörr... 3

Kravspecifikation. TDP005 Projekt: objektorienterade system. Version 4.0 Datum Anna Ahlberg Johan Almberg

Inkapsling tumregler. Åtkomstmodifikatorer, instantiering, referenser, identitet och ekvivalens, samt klassvariabler. public och private

TAIU07 Matematiska beräkningar med Matlab

Programmera i Block Editor

AVR 3 - datorteknik. Avbrott. Digitala system 15 hp. Förberedelser

Tentamen FYTA11 Javaprogrammering

Mäta rakhet Scanning med M7005

Lektion 7, del 1, kapitel 15 Filter och Transformationer

TDDC74 Lab 02 Listor, sammansatta strukturer

Laboration 1 Introduktion till Visual Basic 6.0

TDDC74 Programmering: Abstraktion och modellering Tentamen, onsdag 9 juni 2016, kl 14 18

Att skriva på datorn

Transkript:

Grafik i Racket Rasmus Andersson Senast uppdaterad 30 mars 2015 1

Innehåll 1 Inledning 3 1.1 Rackets grafikpaket..................................... 3 2 Skapa ett grafikfönster 4 3 Knappar och paneler 5 3.1 Knappar........................................... 5 3.2 Paneler........................................... 6 4 Målaren och målarduken 8 4.1 canvas............................................ 8 4.2 dc.............................................. 8 5 Koordinattransformationer 10 5.1 translate........................................... 10 5.2 rotate............................................ 10 5.3 scale............................................. 11 6 Skapa egna bitmaps 12 7 Att använda input 13 7.1 Tangentbordet....................................... 13 7.2 Musen............................................ 14 8 Timers 16 9 Ett lite större exempel 17 10 Att tänka på 20 2

1 Inledning Detta kompendium innehåller en introduktion till grafikprogrammering i Racket och är tänkt att användas i projektdelen av kursen TDDC74. Upplägget på kompendiet är sådant att vi börjar med att skapa ett fönster och testar att lägga in lite knappar som kan användas för att bygga menyer och liknande. Därefter går vi stegvis in på hur vi ritar ut saker på skärmen, hur vi tar emot input från tangentbordet och musen samt hur vi använder timers för att få skärmen att uppdateras regelbundet. Slutligen följer ett lite större exempel som kombinerar det mesta av det som vi gått igenom. Det hela avslutas med ett nytt avsnitt med lite saker som kan vara bra att tänka på. I detta dokument hanterar vi enbart 2D grafik då det är vad de flesta av er kommer att använda i era projekt och som stöds av Rackets egna grafiksystem på ett smidigt sätt. Det finns dock möjlighet att använda det plattformsoberoende grafikbiblioteket OpenGL för att programmera 3D grafik i Racket. Information om detta kan hittas på http://docs.racket-lang.org/sgl/. Generellt sett skulle jag dock rekommendera att man istället väljer språk som exempelvis C/C++ för att skapa 3D grafik. För den som är intresserad av 3D grafik kan jag starkt rekommendera kursen TSBK07 - Datorgrafik, som är valbar för både Y, MED och MMAT i årskurs 4. 1.1 Rackets grafikpaket Alla de klasser och funktioner vi kommer att använda finns i Rackets inbyggda grafikpaket som kan kommas åt genom att använda (require racket/gui/base) Alternativt kan vi istället byta vårt språkval till #lang racket/gui Om ni glömmer detta tillägg kommer Racket klaga på att den inte känner igen en del av de klasser vi kommer att använda framöver. Mer information finns på http://docs.racket-lang.org/gui/ vilket är en guide som innehåller allt som ni kan tänkas vilja göra när det kommer till grafik i Racket. Detta kompendium är tänkt att vara mer utförligt på de delar som är viktigast för er, men det finns mycket intressant som jag inte tar upp, som ni kan läsa mer om i den där. 3

2 Skapa ett grafikfönster När vi jobbar med grafik vill vi gärna kunna visa den för användaren också, annars är det ju ingen större poäng med det hela. Det första steget är då att skapa ett fönster. Detta görs genom att vi skapar en instans av klassen frame%. Vi kan kalla fönstret *a-window* och definerar denna enligt nedan (define *a-window* (new frame% [label "Detta är ett fönster"])) (send *a-window* show #t) Den första raden skapar ett nytt fönster. Värt att notera är att klassen frame% har en obligatorisk initieringsvariabel label. Med andra ord måste man ge fönstret en rubrik för att kunna skapa det. Den andra raden gör fönstret synligt genom skicka show och #t till fönstret. Om man vill dölja ett synligt fönster använder man samma kommando med #f istället för #t. Det fönstret som nu skapas ser inte mycket ut för världen. Eftersom vi inte har talat om hur stort vi vill att fönstret ska vara så skapas ett så litet fönster som möjligt. För att ställa in höjd och bredd på fönstret använder vi initieringsvariablerna width och height enligt nedan. (define *a-window* (new frame% [width 300] [height 200] [label "Detta är ett fönster"])) (send *a-window* show #t) Figur 1: Fönstret *a-window* Detta resulterar i ett fönster som är 300 pixlar brett och 200 pixlar högt. Klassen frame% har dock ytterligare ett antal initieringsvariabler 1. Figur 2: Den nya versionen av *a-window* 1 För mer information om dessa se http://docs.racket-lang.org/gui/frame_.html 4

3 Knappar och paneler Det absolut enklaste sättet att skapa exempelvis en meny är att använda Racket s färdiga button% klass. Det går naturligtvis att skapa egna knappar genom att helt enkelt rita en figur eller bild och lägga ett mus klick event inom det område som knappen/figuren täcker (mer om det längre fram). Det kräver dock betydligt mer arbete och dessutom måste man ha skapat en underklass till canvas 3.1 Knappar (new button% [parent *a-window*] [label "En knapp"]) En enkel knapp skapas på detta sätt. Parent talar om att knappen är underordnad det fönster vi skapade tidigare och att den således ska placeras inuti detta. Parametern label talar som tidigare om vilken text som ska stå på knappen. Den knappen vi nyss skapade ser ju snygg ut och den går att klicka på, men det händer ingenting när man klickar på den. För att detta ska Figur 3: Ett fönster med en knapp ske behöver vi lägga till en variabel, nämligen callback som ska innehålla en procedur som tar in en knapp och ett knapp-event som argument. Denna procedur anropas automatiskt varje gång knappen klickas på. Vi kan lägga till en sådan procedur som exempelvis ändrar texten på knappen. För att anropa den knapp som man klickat på använder vi den variabel button som callback-funktionen får in. För att ändra texten använder vi kommandot set-label. För att den nya texten ska få plats i knappen behöver vi även utöka storleken på denna vilket görs med hjälp av min-width. (new button% [parent *a-window*] [label "En knapp"] [callback (lambda (button event) (send button set-label "Du klickade på mig"))] [min-width 130]) 5

Vi kan med en enkel rekursiv procedur skapa flera knappar. Hur dessa placeras inom fönstret beror på alignment 2 -inställningarna för fönstret. Har inga sådana angetts så placeras knapparna på rad uppifrån och ned. Om vi anropar proceduren nedan med inargument 5 får vi således fem knappar placerade under varandra. Klickar man på en av knapparna ändras bara texten på denna eftersom det är just den knappen som hamnar i callback-funktionens button-variabel. (define (create-buttons number-of-buttons) Figur 4: (if (> number-of-buttons 0) Ett fönster skapat med (create-buttons 5) (begin där den andra knappen har blivit klickad på (new button% [parent *a-window*] [label "En knapp"] [callback (lambda (button event) (send button set-label "Du klickade på mig"))] [min-width 130] ) (create-buttons (- number-of-buttons 1))) (void))) Naturligtvis kan callback-funktionen vara betydligt mer avancerad och/eller vara definierad separat 3.2 Paneler Det går tyvärr inte att ange en exakt position för en knapp genom att ange x- och y-koordinater. Däremot går det att kontrollera vart knapparna hamnar ganska bra ändå med hjälp av alignment inställningarna på fönstret. Om man vill ha flera knappar och inte ha dem enbart på en rad kan man utnyttja så kallade paneler. En panel är ett objekt vars uppgift är att fördela objekten i ett fönster geometriskt. Det finns två olika panelklasser i Racket: pane% och panel%. Dessa fungerar i stort sett lika dant. En instans av klassen panel% har dock några extra funktioner 3. Både pane% och panel% har två underklasser som är användbara: horizontal-pane% och vertical-pane% respektive horizontal-panel% och vertical-panel%. Dessa arrangerar sina objekt (som namnen antyder) horisontellt respektive vertikalt. Defaultinställningarna på ett fönster beter sig som en vertical-pane(l) i hur den arrangerar sina objekt, vilket vi kunde se med knapparna i det tidigare exemplet. Genom att lägga paneler med olika orienteringar i varandra så kan vi placera knapparna i princip hur vi vill i fönstret. För att få knapparna arrangerade som 3*3 kan vi exempelvis lägga tre horisontella paneler i en vertikal panel. Därefter lägger vi tre knappar i vardera av de horisontella panelerna. 2 Se http://goo.gl/lblxjr för information om hur alignment fungerar 3 se http://docs.racket-lang.org/gui/pane_.html respektive http://docs.racket-lang.org/gui/panel_.html för mer information. 6

Det finns en hel hög med valbara inställningar för panelerna 4. En av dessa som är värd att nämna är alignment som anger hur centreringen av knapparna inom panelen ska vara. Den anges som en lista med två symboler. Den första ska ange hur vi vill centrera i sidled ( right, center eller left) och den andra hur vi vill centrera i höjdled ( bottom, center eller top). Defaultinställningarna för en horizontal-pane(l) är (left center) medan de för en vertical-pane(l) (och för själva fönstret) är (center top). Nedan följer ett enkelt exempel där vi kombinerar några paneler för att se hur detta påverkar knapparnas positioner i fönstret. (define *a-window* (new frame% [width 300] [height 200] [label "Utspridda knappar"])) (define (create-button button-parent) (new button% [parent button-parent] [label "0"] [callback (lambda (button event) (send button set-label (number->string (+ 1 (string->number (send button get-label))))))])) (let ((horizontal-1 (new horizontal-pane% [parent *a-window*] [alignment (right center)])) (horizontal-2 (new horizontal-pane% [parent *a-window*] [alignment (center center)]))) (create-button horizontal-1) ;1 (create-button horizontal-1) ;2 (create-button horizontal-2) ;3 (let ((vertical-1 (new vertical-pane% [parent horizontal-2] [alignment (center center)]))) (create-button vertical-1) ;4 (create-button vertical-1));5 (create-button horizontal-2));6 (create-button *a-window*) ;7 (send *a-window* show #t) Figur 5: Resultatet av koden till vänster För enkelhetens skull har vi skapat en create-button som tar in en panel eller ett fönster som ska fungera som knappens parent. I detta fall räknar knapparna hur många gånger de har blivit klickade på. Eftersom label måste vara en sträng eller en bitmap så får vi konvertera fram och tillbaka mellan strängar och nummer. Vi har sedan skapat två horisontella paneler (med olika alignment) och lagt dessa i fönstret. Därefter har vi fyllt dessa med knappar och i den andra panelen har vi även lagt en vertikal panel med två knappar i. Den sista knappen (7 på bilden) är placerad direkt i fönstret. 4 För mer information om dessa se: http://docs.racket-lang.org/gui/vertical-pane_.html http://docs.racket-lang.org/gui/vertical-panel_.html http://docs.racket-lang.org/gui/horizontal-pane_.html http://docs.racket-lang.org/gui/horizontal-panel_.html 7

4 Målaren och målarduken Det blir lite tråkigt att ha ett spel som bara består av fönster med knappar. För att kunna rita ut saker på skärmen behöver vi, precis som för att skapa ett konstverk, en målarduk och en målare. 4.1 canvas Målarduken (canvas på engelska) är en instans av klassen canvas%. När vi skapar en ny canvas måste vi ange två saker: vart målarduken ska ligga och hur den ska målas. Vi vill med största sannolikhet att duken ska ligga i vårt fönster. Därför sätter vi parent till det fönster vi tidigare skapat. Hur den ska målas anges genom en paint-callback-funktion. Denna anropas varje gång vi säger till canvas att uppdateras (refresh). Den proceduren (här kallad render-fn) måste ta två inargument: den canvas som den anropades ifrån och tillhörande målare. (define *a-canvas* (new canvas% [parent *a-window*] [paint-callback render-fn])) 4.2 dc Målaren (drawing-context, normalt förkortad till dc) är det objekt som utför målandet på en canvas. En målare är en instans av klassen dc% och skapas automatiskt då vi skapar en ny canvas och skickas som sagt med i paint-callback. Det är målaren som tar emot alla kommandon av typen rita en cirkel, skriv hej eller rita den här bilden. Den finns normalt sett inte definierad globalt på det sätt som vår canvas eller vårt fönster. Detta innebär att vi måste se till att alltid skicka med vår dc till alla procedurer från vilka något ska kunna ritas ut. Den kan dock kommas åt genom (send *a-canvas* get-dc). ;Ritande kommandon (send dc draw-rectangle x y width height) (send dc draw-rounded-rectangle x y width height radius) (send dc draw-arc x y width height start-radians end-radians) (send dc draw-ellipse x y width height) (send dc draw-line x1 y1 x2 y2) (send dc draw-spline x1 y1 x2 y2 x3 y3) (send dc draw-lines list-of-points) (send dc draw-polygon list-of-points) (send dc draw-text text x y) (send dc draw-bitmap source x y) ;Koordinatsystemsförändrande kommandon (send dc translate dx dy) (send dc rotate angle) (send dc scale x-scale y-scale) 8

;Färginställningar och liknande (send dc set-pen color-name width style) (send dc set-brush color-name style) (send dc set-alpha opacity) (send dc set-background color) (send dc set-font font) ;Rensa skärmen (send dc clear) Fler kommandon finns på http://docs.racket-lang.org/draw/dc.html Det har nu blivit dags att definiera själva paint-callback-funktionen. Den som vi har valt att kalla render-function. Denna kommer alltså att anropas direkt när programmet körs, och sedan varje gång vi använder(send *a-canvas* refresh). Genom att kombinera kommandona i tabellen ovan kan vi bygga upp vår grafik. Något som är viktigt att tänka på är att koordinat-systemet inte riktigt är vad vi är vana vid från andra sammanhang. Koordinatsystemet är ett vänster-on-system, det vill säga att positiva x-axeln är riktad till höger, medan den positiva y- axeln är riktad nedåt. Systemet har sitt origo i fönstrets (eller egentligen canvasens) övre vänstra hörn. Samma sak gäller för de objekt man ritar ut. De koordinater man anger för vart objektet ska ritas anger vart det övre vänstra hörnet ska ritas (för exempelvis en cirkel så anger det vart det övre högra hörnet på den tänkta kvadrat som cirkeln ryms i ska ritas). Bilden visar resultatet av (send dc draw-ellipse 4 3 2 2) med en röd brush. Figur 6: En cirkel utritad på koordinat (4,3) 9

5 Koordinattransformationer Ibland är det inte praktiskt att utgå från fönstrets övre vänstra hörn, eller också kanske vi vill rotera allting eller ändra storlek på något. Därför finns det tre praktiska transformationsfunktioner för dc. 5.1 translate Det första av dem är translate som används för att förflytta koordinatsystemets origo. Detta är särskilt praktiskt när vi vill rita ut flera objekt som har samma avstånd inbördes men förflyttade över skärmen. Om man vill förflytta alla objekt ett avstånd dx i x-led och dy i y-led kan vi då använda Figur 7: En bitmap föreställande Musse Pigg utritad på koordinat (4,3) (send dc translate dx dy) istället för att hela tiden plussa på dx och dy på alla koordinater för vart och ett av objekten. Fler praktiska användningsområden kommer att uppdagas när vi tittar på de andra transformationskommandona. 5.2 rotate Nästa är rotate som roterar hela koordinatsystemet runt origo. Exempelvis roterar (send dc rotate 2) hela koordinatsystemet 2 radianer moturs. För att inte allting som ritas ut fortsättningsvis ska vara roterat är det viktigt att rotera tillbaka när man är färdig med utritningen av det som ska vara roterat för att återgå till normalt koordinatsystem. Tämligen ofta vill man rotera något 90, dvs π/2 radianer. För att kunna göra detta med hög precision (och för den delen andra beräkningar som kräver bra approximationer av π) finns det en fördefinierad konstant pi inbyggd i racket som är en approximation med 15 decimalers noggrannhet. Figur 8: (send dc rotate 0.2) och därefter utritad på (4,3) Figur 9: (send dc rotate 0.2) och därefter utritad på (4,3) 10

Oftast vill man kanske inte rotera ett objekt runt origo. Då får man kombinera translate och rotate. Då kommer allting istället att roteras kring det nya virtuella origo som fås genom translate. Om man tillexempel vill rotera en bild så är det enklaste att translatera till bildens tilltänkta koordinat och sedan rotera och rita ut bilden på den virtuella koordinaten (0,0). Detta gör dock att bilden roteras kring sitt övre vänstra hörn, vilket kanske inte alltid är vad vi önskar. För att få den mest naturliga rotationen (en rullande rörelse istället för en propellerliknande rörelse) måste vi rotera bilden kring dess centrum. För exempelvis en bitmap med bredden w och höjden h som vi vill rita ut på position (x,y) roterad en vinkel v radianer runt sitt centrum använder vi följande algoritm. (send dc translate (+ x (/ w 2)) (+ y (/ h 2))) (send dc rotate v) (send dc draw-bitmap *a-bitmap* (- (/ w 2)) (- (/ h 2))) (send dc rotate (- v)) (send dc translate (- (+ x (/ w 2))) (- (+ y (/ h 2)))) Figur 10: (send dc translate 4 3) därefter (send dc rotate 0.2) och utritad på (0,0) 5.3 scale Den tredje och sista av de transformationskommandon som vi tar upp är scale. Kommandot tar in en skalningsfaktor för x-led och en för y-led (kan vara samma eller olika) och skalar sedan om hela koordinatsystemet utifrån dessa. Om skalningsfaktorn är större än 1 får vi en förstoring av koordinatsystemet och dess objekt. På samma sätt får vi, om faktorn är mindre än 1, en förminskning. Om skalningsfaktorn är negativ får vi en spegling i den aktuella axeln. Något som är viktigt att komma ihåg är att skalningen även påverkar koordinaterna. En skalning med faktor 2 gör att något som tidigare ritats ut på (1,1) ritas ut på det som tidigare var (2,2). För att förhindra detta får vi kompensera genom att translatera. Figur 11: (send dc scale 0.5 0.5) därefter utritad på virtuell koordinat (4,3) Ytterligare en sak att notera är att när koordinatsystemet förminskas så krymper inte pixlarna på skärmen, men alla inställningar (linjetjocklekar och liknande). Därmed förlorar man ofta upplösning på det som ritas ut (det är svårt att exempelvis rita en halv pixel tjocka linjer). 11

6 Skapa egna bitmaps I stället för att skapa egna procedurer som varje gång ritar upp en komplex samling av figurer med en serie av kommandon av typen (send dc draw-xxx...) kan det vara praktiskt att skapa en bitmap av resultatet av dessa kommandon. Det är mycket mer resurseffektivt i de fall där man vill rita ut samma bild flera gånger. För att kunna rita en egen bitmap måste vi först skapa en tom instans av bitmap%. Det kan göras på ett flertal olika sätt 5. Det effektivaste och oftast mest praktiska är att använda make-bitmap. Default läget är då att bitmapen får en alpha-kanal, det vill säga att det som är tomt på bitmapen blir genomskinligt. Vill vi inte det så lägger vi till ett extra argument #f efter bredden och höjden. Därefter måste vi skapa en instans av bitmap-dc% 6 och koppla denna till den tidigare skapade bitmapen. (define *my-bitmap* (make-bitmap 100 100)) (define *dc* (new bitmap-dc% [bitmap *my-bitmap*])) Nu kan vi använda samma kommandon som vi sedan tidigare är vana vid att använda för instanser av vanliga dc% även för vår *dc* som dock ritar på bitmapen istället för på canvasen som är det normala. När bitmapen sedan är färdigritad kan vi rita ut den med det vanliga draw-bitmap kommandot till en canvaskopplad dc. Då vi ogärna vill definiera en dc globalt let:ar vi den med fördel i en procedur som vi definierar för att göra allt ritande till bitmapen. Ett exempel på hur vi kan skapa en enkel bitmap finns nedan där vi har en procedur som skapar en vattenmolekylliknande figur. (define *my-bitmap* (make-bitmap 100 100)) (define (create-h2o-image bitmap-target) (let ((dc (new bitmap-dc% [bitmap bitmap-target]))) (send dc set-brush (make-object brush% "white" solid)) (send dc draw-ellipse 10 10 40 40) (send dc draw-ellipse 50 10 40 40) (send dc set-brush (make-object brush% "red" solid)) (send dc draw-ellipse 25 25 50 50))) Figur 12: *my-bitmap* utritad på en svart bakgrund (create-h2o-image *my-bitmap*) Om vi istället för att rita en egen bitmap vill använda en vanlig bildfil gör man lämpligen om den till en bitmap genom att använda något på den här formen: (make-object bitmap% "Exempelbild.png"). För att behålla en eventuell alpha-kanal lägger man till en flagga efter adressen. Flaggorna är beroende av filtyp. 5 I det här fallet bör man använda png/alpha. 5 Se http://docs.racket-lang.org/draw/bitmap_.html 6 Se http://docs.racket-lang.org/draw/bitmap-dc_.html 12

7 Att använda input Nu har vi lärt oss hur vi ska rita ut den grafik vi vill ha i vårt spel. För att det ska bli någorlunda intressant vill vi ju även gärna kunna interagera med spelet också, annars blir det ju snarare en simulering än ett spel. Vi såg tidigare hur man kan använda knappar, men ibland (för det mesta) vill man kanske kunna klicka på saker direkt på spelplanen eller också vill man kunna använda tangentbordet. Det man får göra då är att man får syssla med lite eventbaserad programmering. Event kan man på svenska översätta till händelse. Med andra ord handlar det om att funktioner ska anropas när något visst händer (i vårt fall att en tangent trycks ned eller att man klickar med musen), istället för från en given plats i programmet. Hur detta går till under huven behöver vi som tur är inte bry oss allt för mycket om. 7.1 Tangentbordet När ett så kallat key-event inträffar (en knapp på tangentbordet trycks ned eller släpps upp) så anropas det aktuella canvasets funktion on-key. Den är dock som default inställd att göra ingenting. För att kunna ändra detta är vi tvungna att skapa en subklass (underklass) till canvas% (se avsnittet om arv i OOP-kompendiet). Vi kan exempelvis välja att kalla den game-canvas%. En subklass ärver alla metoder från sin förälder förutom de som overridas. Därmed vill vi alltså overrida on-key vilket görs med hjälp av define/override enligt nedan. Eftersom vi eventuellt i framtiden skulle kunna tänkas vilja ha flera olika instanser av vår game-canvas% så lägger vi till en initierbar variabel keyboard-handler som tar in en funktion som vi sedan använder i vår on-key. En init-field variabel bör dock ha ett defaultvärde, som gäller om inte användaren anger annat vid initiering (exempelvis om tangentbordet inte ska användas). I vårt fall kan vi exempelvis sätta den till display. (define game-canvas% (class canvas% (init-field [keyboard-handler display]) (define/override (on-char key-event) (keyboard-handler key-event)) (super-new))) (define *a-game-canvas* (new game-canvas% [parent *a-window*] [paint-callback render-fn] [keyboard-handler handle-key-event])) Instansieringen av game-canvas% ser ut precis som tidigare instansieringar av canvas%, förutom att vi skickar in en funktion till keyboard-handler också. I exemplet skickar vi med en funktion handle-key-event som precis som render-fn skrivs separat. Nu har vi alltså skapat en ny typ av canvas som kan hantera key-event. Då gäller det bara att kunna göra något av den informationen också. Det är med andra ord dags att skriva funktionen handle-key-event som vi skickar in vid initieringen. 13

När det gäller tangentbordet är man oftast intresserad av att veta vilken knapp det var som trycktes ned. Med hjälp av (send key-event get-key-code) får vi tillbaka en key-code för den knapp som trycktes ned. En key-code kan vara antingen ett tecken (char på engelska) eller en symbol. Tecken skrivs i racket med #\ medan symboler skrivs med. Exempelvis har vi för bokstäverna key-codes som #\a, #\s, #\d och så vidare. För de tangenter vars namn inte finns i ASCII-tabellen används symboler, exempelvis up, down, shift, control och så vidare. Vissa tangenter skrivs dock ändå med #\, exempelvis #\space, #\return, #\backspace och liknande. 7 När en knapp släpps upp skapas det också ett key-event. Om man använder get-key-code på ett sådant key-event får man tillbaka svaret release. För att få veta vilken knapp det var som släpptes upp får man då istället använda get-key-release-code. Detta kommando returnerar istället down om eventet skapades för att en knapp trycktes ned. En liten varning: för get-key-release-code är down både En knapp har tryckts ned och Nedpilsknappen släpptes upp. Det är därför vettigt att först kolla om get-key-code returnerar release för att veta vilken typ av down det gäller. 7.2 Musen På samma sätt som vi får ett anrop till on-key när en tangent trycks ned, får vi ett anrop till on-event varje gång musen flyttas eller klickas på. För att kunna hantera detta så lägger vi till en override på den metoden också i vårt game-canvas% tillsammans med en init-field för en mouse-handler. (define game-canvas% (class canvas% (init-field [keyboard-handler display] [mouse-handler display]) (define/override (on-char key-event) (keyboard-handler key-event)) (define/override (on-event mouse-event) (mouse-handler mouse-event)) (super-new))) (define *a-game-canvas* (new game-canvas% [parent *a-window*] [paint-callback render-fn] [keyboard-handler handle-key-event] [mouse-handler handle-mouse-event])) 7 Fler exempel på key-codes finns på http://docs.racket-lang.org/gui/key-event_.html 14

För mouse-events är metoderna uppbyggda lite annorlunda än för key-events. Om man vill veta huruvida en knapp på musen blev nedtryckt använder man (send mouse-event button-down? any). Den returnerar då #t eller #f beroende på om någon knapp var nedtryckt eller inte. Man kan även byta ut button-down? mot button-up? som då i stället returnerar #t om eventet orsakades av att en knapp släpptes upp. Vill vi göra skillnad på höger-klick, vänster-klick och mitten-klick (oftast att man trycker ned skrollen på moderna möss) byter man ut any mot antingen left, right eller middle. Det genereras även mouse-event varje gång musen flyttas (förutsatt att fönstret som canvasen är kopplat till är markerat). För att få reda på om eventet orsakades av att musen förflyttades kan man använda (send mouse-event moving?). Vill man dessutom veta om det berodde på en förflyttning samtidigt som en knapp var nedtryckt använder man (send mouse-event dragging?). Värt att notera är att moving? Returnerar #t om musen förflyttas oavsett om den är nedtryckt eller inte. Det finns även en lång rad andra specialevent och liknande 8. När vi har noterat att ett mouse-event har ägt rum vill vi oftast veta vart musen var vid detta tillfälle (exempelvis vart man klickade någonstans). För att avgöra detta använder man (send mouse-event get-x) respektive get-y. För att sedan avgöra om man exempelvis klickade på en bild får man jämföra bildens koordinater med resultaten från get-x och get-y. 8 Läs mer om dem på http://docs.racket-lang.org/gui/mouse-event_.html 15

8 Timers I ganska många sammanhang kan man vara intresserad av att mäta hur lång tid det har gått sedan en viss händelse inträffade, eller snarare göra saker med vissa intervall. Det allra viktigaste för vår del kanske är att veta när vi ska uppdatera skärmen. Men i vissa sammanhang kan man även vilja ta tid på vissa andra saker. Sådana mätningar görs med instanser av klassen timer%. En timer är ganska lättskött. Den har bara tre inparametrar. Dels har vi notify-callback som är den procedur som ska anropas varje gång timern går ut. Sedan har vi interval som anger hur ofta timern ska gå ut, det vill säga hur ofta notify-callback ska anropas. Observera att tiden ska anges i millisekunder. Den tredje är en boolesk variabel just-once? som alltså tar in antingen #t eller #f. Om den är #t så kommer timern stoppas efter att notify-callback har anropas första gången. Om den är #f (vilket den är om inget annat anges) så startar timern om när notify-callback har anropats. Om man matar in interval redan när man instansierar timer% så startas den direkt. Oftast vill man kanske inte att den går igång fören alla variabler är definerade och allt är inladdat. Därför är det en bra princip att bara ange notify-callback när man skapar timern och sedan starta timern separat med de övriga variablerna när allt annat är färdigladdat (interval måste i alla fall anges när man startar timern). Om vi utgår från att vi vill använda timern för att hålla koll på när skärmen ska uppdateras så bör vi först fundera lite grann på hur ofta vi vill uppdatera. För att få en så naturlig rörelse som möjligt bör vi naturligtvis uppdatera så ofta som möjligt, men det finns alltid en gräns för hur mycket datorn klarar av. Den varierar dock kraftigt mellan olika datorer, något som kan vara värt att tänka på om man vill att spelet ska fungera på datorer som är sämre än ens egen. För dataspel brukar man försöka uppdatera 60ggr per sekund för att undvika att spelaren upplever spelet som laggigt. Vi vill alltså om möjligt uppdatera med en frekvens på 60Hz. Det motsvarar en tid på ca 16,66ms mellan varje uppdatering. Om vi sätter vår interval till 16 bör vi med andra ord vara på den säkra sidan. För att sedan starta timern så använder vi start med inparameter interval mätt i ms och en valbar just-once? (define *timer* (new timer% [notify-callback update])) (send *timer* start 16 #f) I exemplet har vi kopplat notify-callback till en procedur update som vi definierar separat. Om vi har kommit till ett läge där vi inte längre vill använda timern, eller exempelvis vill pausa spelet använder vi (send *timer* stop). 16

9 Ett lite större exempel Nu har det blivit dags att sätta samman det vi har lärt oss. Vi tar ett lite längre exempel där vi utnyttjar allt vi har gått igenom tidigare förutom knappar. I exemplet skapar vi en roterande bild som går att förflytta med piltangenterna. För enkelhetens skull så använder vi vattenmolekylen från avsnitt 7 som bild. (define rotating-image% (class object% (init-field [image (make-bitmap 100 100)] [x 0] [y 0] [angle 0] [speed 1]) (define/public (get-image) image) (define/private (move-left) (set! x (- x speed))) (define/private (move-right) (set! x (+ x speed))) (define/private (move-up) (set! y (- y speed))) (define/private (move-down) (set! y (+ y speed))) (define/public (rotate) (set! angle (+ angle 0.1))) (define/public (render dc) (let ((w (send image get-width)) (h (send image get-height))) (send dc translate (+ x (/ w 2)) (+ y (/ h 2))) (send dc rotate angle) (send dc draw-bitmap image (- (/ w 2)) (- (/ h 2))) (send dc rotate (- angle)) (send dc translate (- (+ x (/ w 2))) (- (+ y (/ h 2)))))) (define/public (key-down key-code) (cond ((equal? key-code up) (move-up)) ((equal? key-code down) (move-down)) ((equal? key-code left) (move-left)) ((equal? key-code right) (move-right)))) (super-new))) (define game-canvas% (class canvas% (init-field [keyboard-handler display] [mouse-handler display]) (define/override (on-char key-event) (keyboard-handler key-event)) (define/override (on-event mouse-event) (mouse-handler mouse-event)) (super-new))) 17

(define (create-h2o-image bitmap-target) (let ((dc (new bitmap-dc% [bitmap bitmap-target]))) (send dc set-brush (make-object brush% "white" solid)) (send dc draw-ellipse 10 10 40 40) (send dc draw-ellipse 50 10 40 40) (send dc set-brush (make-object brush% "red" solid)) (send dc draw-ellipse 25 25 50 50))) (define *my-rotating-image* (new rotating-image% [x 100] [y 50])) (define *my-window* (new frame% [label "Rolling water"] [width 300] [height 200])) (define *my-game-canvas* (new game-canvas% [parent *my-window*] [paint-callback (lambda (canvas dc) (send *my-rotating-image* rotate) (send *my-rotating-image* render dc))] [keyboard-handler (lambda (key-event) (let ((key-code (send key-event get-key-code))) (if (not (equal? key-code release)) (send *my-rotating-image* key-down key-code) (void))))] [mouse-handler (lambda (mouse-event)void)])) (define *my-timer* (new timer% [notify-callback (lambda () (send *my-game-canvas* refresh))])) (create-h2o-image (send *my-rotating-image* get-image)) (send *my-window* show #t) (send *my-timer* start 16) (send *my-game-canvas* focus) Det första vi gör är att skapa en klass som hanterar den information vi behöver ha om vår roterande bild nämligen vilken bild som används, vilken position den befinner sig på, vilken vinkel bilden har för närvarande samt hur snabbt den ska förflyttas. Den innehåller en publik metod get-image som returnerar den bild som tillhör klassen, några privata metoder för att hantera förflyttningen av bilden och den publika metoden rotate som uppdaterar bildens vinkel. Dessutom har vi metoden render som hanterar själva utritningen av bilden enligt den sista algoritm som beskrivs i avsnitt 5.2, d.v.s. genom att rotera bilden runt sitt centrum. Den sista metoden key-down beskriver vad som ska göras när de olika knapparna trycks ned. Därefter skapar vi game-canvas% som en underklass till canvas% precis som i avsnitt 7 och proceduren create-h2o-image som i avsnitt 6. Vi skapar sedan instanser av de aktuella klasserna. *my-rotating-image* är en instans av vår egen klass för roterande bilder som får utgångspunkt i (100,50). *my-window* är fönstret som 18

allt kommer att ritas i (se avsnitt 2) och *my-game-canvas* är en instans av game-canvas%. Vi låter paint-callback uppdatera vinkeln för bilden och sedan rita ut den genom att anropa *my-rotating-image* med rotate och render. Sedan låter vi key-board-handler plocka fram key-code för den knapp som tryckts ned och skickar den till *my-rotating-image* s metod key-down samtidigt som vi låter mouse-handler endast anropa void (d.v.s. göra ingenting) då vi inte kommer att använda musen. Slutligen instansierar vi en timer som vid varje utgång uppdaterar *my-game-canvas* med hjälp av refresh som gör att skärmen rensas och paint-callback anropas. Därefter anropar vi create-h20-image med den tomma bilden som finns i *my-rotating-image* för att skapa vattenmolekylbilden, aktiverar fönstret och startar timern. Sist av allt ger vi vår canvas fokus för att vi direkt ska kunna börja styra med knapparna utan att först klicka på fönstret. 19

10 Att tänka på I detta avsnitt listar jag några punkter som är bra att tänka på när man jobbar med lite större projekt med grafikinslag. Skapa inte nya objekt kontinuerligt under spelets gång utan försök att skapa de objekt som behövs vid initiering och återanvänd sedan dessa. Ett tydligt exempel på problematiken med detta kan ses om vi betraktar de skott som skjuts av ett rymdskepp i det klassiska spelet Asteroids. Under en spelomgång avfyrar en spelare kanske tusen skott. Om vi skapar ett nytt skott-objekt för vart och ett av dem fyller vi snabbt upp RAM-minnet, eftersom skotten ju inte försvinner bara för att de försvinner utanför skärmen. Skapa istället ett fixt antal skott motsvarande det maximala antalet som spelaren kan hinna skjuta innan det första försvinner utanför skärmen (kanske 20) och återanvänd sedan dessa genom hela spelet. Motsvarande gäller naturligtvis för alla andra objekt i spelet också. När vi ändå pratar om att saker inte försvinner bara för att de inte syns längre, så kan vi åtminstone låta bli att rita ut dem. Lägg till ett boolskt fält (true/false) i klasserna och ett predikat alive? som avgör om de ska ritas ut eller inte. När exempelvis ett skott avfyras sätter ni således fältet till true och när det paserar utanför skärmen sätter ni det till false. Detta gör att vi inte ritar ut saker som inte syns och således spar vi en hel del beräkningstid där, speciellt om vi har stora världar. Oavsett om ni arbetar grafiskt eller inte så undvik i största möjliga utsträckning att använda globala variabler. Det kan ofta kännas smidigt att lägga en variabel globalt, men riskerna som detta medför är så pass stora att sådana enbart ska användas i fall där det är mycket väl motiverat. Se till att (i enlighet med kursens coding guidlines ) namnge alla globala variabler med asterixer (ex. *globalvariabel* ) för att tydliggöra att ändrande av den variabeln kan medföra konsekvenser för resten av programmet. Lägg exempelvis alla objekt som ska ritas ut på skärmen i en lista som sedan kan gås igenom med exempelvis en for-each. I exemplet nedan plockar vi dessutom enbart ut de objekt som vi faktiskt vill rita ut med hjälp av filter: (for-each (lambda(obj) (send obj draw *dc*)) (filter (lambda(obj) (send obj alive?)) *game-objects*)) eller (for-each (lambda(obj) (send *dc* draw-bitmap (send obj get-bitmap))) (filter (lambda(obj) (send obj alive?)) *game-objects*)) beroende på om objekten ansvarar för sitt eget utritande eller inte. Om ni har flera klasser som har stora likheter kan ni med fördel utnyttja arv (se OOP-kompendiet) för att få tydligare struktur och slippa upprepa kod. Exempelvis kan ni ha en basklass character% från vilken sedan de två klasserna player% och enemy% ärver. För att sedan skapa olika typer av fiender kan ui sedan låta mer specifika klasser ärva från klassen enemy%. 20