Processing Objekter

Fra HTX Arduino
Spring til navigation Spring til søgning

Siden her beskriver hvordan Processing kan håndtere objekter. Siden er skrevet ud fra best pracsis omkring objekter.

Grundlæggende set er et objekt en selvstændig del af et program, som kan løse en bestemt opgave - det kan være at den kan håndtere en knap på en skærm, at forbinde til et stykke hardware, at håndtere en spil-figur eller en fjende i et spil.

Denne side omtaler objekter ud fra to små eksempler der anvender objekter. Det giver mest mening hvis man henter eksemplerne og afvikler dem i Processing[1], der er open source software til Windows, MAC og Linux.

Eksempler til denne side ligger i Denne ZIP-fil. Der beskrives to forskellige klasser - en Button klasse, der kan lave objekter til knapper på skærmen og en snowflake klasse der demonstrerer hvordan man kan håndtere mange objekter i en datastruktur.

Klasser laver objekter

For at kunne oprette objekter skal man have en klasse der indledes med følgende kode:

class Button {

Hvor Button er navnet eller typen på klassen.

I klassen skal der være en constructor, som er den funktion der opretter det aktuelle objekt. Denne constructor kan i Button-eksemplet se ud som følger:

  Button(float xPos, float yPos, float wid, float hgh, String tx) {
    x = xPos;
    y = yPos;
    w = wid;
    h = hgh;
    txt = tx;
    r = 0;
    c = 0;
    textColor = 255;
    hover = 0;
    txtSz = 25;
    frame = 0;
    frameColor = 0;
  }

Kaldet til denne constructor ser ud som følger:

Button btn1 = new Button(30, 400, 140, 40, "Klik Her");

Der oprettes et objekt btn1 med de parametre i kaldet, som bliver til egenskaber der er angivet i kaldet til Button-constructoren.

Et objekts egenskaber

Hver objekt der oprettes ud fra en klasse vil have en række egenskaber.

Hvert enkelt objekt der oprettes har sine egne individuelle egenskaber, men de er angivet inde i klassens kode.

En typisk klasse vil oprette objektet med de egenskaber der er nødvendige, mens de mere sekundære egenskaber sættes til en standard-værdi der giver mening i anvendelsen af objektet.

Et objekts metoder

Ud over constructoren der opretter objektet, så vil der normalt være en del metoder som kan anvendes til brug i det program man skriver.

Metoderne i klassen er det der danner objekternes funktionalitet. Altså alt det som objekterne skal kunne skal lægges ind i metoder, så man kan anvende disse metoder i sit program.

Set-metoder

Hvis man vil tilrette objektet ved at rette på de sekundære egenskaber, så vil man normalt oprette set-metoder i klassen, hvor man sender de nye egenskaber over som parametre.

Et eksempel på en set-metode kunne være setColor() eller setRadius() som vist her:

  void setColor(color cl, color hv) {
    c = cl;
    hover = hv;
  }
  
  void setRadius(float round) {
    r = round;
  }

Metoder der anvendes i afviklingen af programmet

For at man skal kunne anvende objekterne i programmet, så skal man kunne vise objektet med en draw() metode eller man skal kunne aflæse om der er klikket på musen med en clicked() metode. De kan se ud som følger:

  void draw() {
    rectMode(CORNER);
    if (mouseIn()) {
      fill(hover);
    } else {
      fill(c);
    }
    strokeWeight(frame);
    stroke(frameColor);
    rect(x, y, w, h, r);
    textSize(txtSz);
    textAlign(CENTER);
    fill(textColor);
    text(txt, x + w/2, y + txtSz / 3 + h/2);
  }

  boolean clicked() {
    if (mouseIn()) {
      if (mousePressed && (mouseButton == LEFT)) {
        return true;
      }
    }
    return false;
  }

Oprettelse af filer med klasser

For at oprette en klasse, så er det en god ide at placere dem i selvstændige filer og navngive dem efter den klasse der er i filen, som det er vise i følgende billede:

Koden til en grundlæggende klasse

For at oprette filen, så kan man klikke på den lille pil ved siden af Tab-navnene, så man får de viste muligheder:

Menuen der giver mulighed for at oprette ny Tab

Man kan godt have flere klasser i samme fil, men det er en rigtig god praksis at kun have en klasse i hver fil, og at filen hedder det samme som klassen. Det gør det også meget nemmere at genbruge sine klasser i andre projekter.

God praksis i navngivningen

  • Man har kun en klasse i hver Tab.
  • Constructoren skal have samme navn som class-definitionen.
  • Tab’en kan have et andet navn – men det giver mening at det er det samme.
  • Genanvend gerne navngivning af egenskaber (x og y er gode navne).
  • Genanvend gerne navne på metoder (draw og update er sigende navne).

Hvordan omtaler man (i sin dokumentation) objekter?

For at kunne arbejde med objekter, så skal man have oprettet en klasse (i processing og c-lignende sprog hedder det en class). Som praksis er, så laver man én fil til hver klasse.

Klassen beskriver hvordan objekter oprettet af denne klasse opfører sig, og hvad de kan (man kan se det som det der er typen ved variable).

Når man opretter et objekt ud fra en klasse, så siger man at man opretter en instans af klassen. Det sker fx. ved følgende linje:

Button btn1 = new Button(100, 200, 80, 30, "Klik Her");

I denne kodelinje er Button typen, eller nærmere betegnet klassen som objektet btn1 laves ud fra. Efter lighedstegnet oprettes instansen ud fra klassens Constructor, der er den funktion som hedder det samme som klassen inde i klassen - man kan oprette flere forskellige constructors, som har forskelligt antal parametre. Det kunne fx. være en constructor til at oprette klappen med farver som vist i den følgende kode:

Button btn4 = new Button(570, 200, 100, 40, "Retur", #0000FF, #FF00FF);

Den ekstra contructor-kode kunne så se ud som følger:

  Button(float xPos, float yPos, float wid, float hgh, String tx, color cl, color hv) {
    x = xPos;
    y = yPos;
    w = wid;
    h = hgh;
    txt = tx;
    r = 0;
    c = cl;
    textColor = 255;
    hover = hv;
    txtSz = 25;
    frame = 0;
    frameColor = 0;
  }

De egenskaber (på engelsk properties) som et objekt har ligger defineret inde i klassen, og er lokale variabler i klassen. Derfor har hver instans af objektet sine egne egenskaber. Typisk vil man definere objektets egenskaber ved instansieringen af objektet, eller man kan rette på dem med set-metoder.c

Klassens metoder er de funktioner der er defineret inde i klassen, ud over constructoren. Det er disse metoder der gør at objektet oprettet ud fra klassen kan noget.

Et simpelt eksempel på et kald til en metode show() kunne være som følger:

  btn1.show();

Det der sker er at det aktuelle objekt btn1 får kaldt show() metoden inde i klassen Button, hvorved knappen vises ud fra de egenskaber som btn1 har.

En anden typisk metode som nok ikke er relevant for knapper, men som vil være typisk for spil-objekter er en move() metode. Det der typisk vil ske i sådan en metode er at objektets position (x- og y-egenskaber) bliver tilskrevet en værdi (fx. en hastighed) så objektet flytter sig til nærste gang det tegnes på skærmen.

Eksemplerne der er vist

Eksempler til denne side ligger i Denne ZIP-fil.

Demo eksempel med forskellige knapper på forskellige skærme

Det første eksempel i ZIP-filen er objectDemo, der kun indeholder en klasse til en knap (Button).

Eksemplet viser så hvordan man kan håndtere 6 forskellige knapper ind på 3 forskellige skærme ved at bruge en state-variabel, som indikerer hvilken skærm der vises.

Visningen sker i eksempelkodens draw() funktion, der kalder knappernes draw metoder. Denne kode ser ud som følger:

void draw() {
  background(128);  // Slet baggrund
  textSize(60);
  text("objekt Demo", 350, 100); 
  // Optegning af 3 skærme, alt efter hvad state står på
  switch(state) {
  case 0:
    btn1.draw();  // tegn knap 1 og 2
    btn2.draw();
    break;
  case 1:
    btn3.draw();  // Tegn knap 3 og 6
    btn6.draw();
    text(counter2, 60, 340);  // Vis tæller 2
    break;
  case 2:
    btn4.draw();  // Tegn knap 4 og 5
    btn5.draw();
    if (btn5.clicked()) {  // Tæl op i tæller 1 så længe knappen holdes 
      counter++;
    }
    text(counter, 600, 340);  // Vis tæller 1
  }
}

I state 0 har skærmen følgende udseende:

Skærmen på demoprogrammet med objekter i state 0

I state 1 har skærmen følgende udseende:

Skærmen på demoprogrammet med objekter i state 1

I state 2 har skærmen følgende udseende:

Skærmen på demoprogrammet med objekter i state 2

Eksempel med snefung der oprettes og nedlægges dynamisk

Det andet eksempel i objekter1 anvender to forskellige klasser - en Button klasse, der er mere simpel end det forrige, så den kun laver en knap på skærmen.

Det der så illustreres i eksempelkoden med den anden klasse Snowflake er hvordan man kan håndtere mange objekter i en datastruktur af typen ArrayList. Her oprettes og nedlægges objekter dynamisk, efterhånden som de opstår (dannes med random intervaller) og efterhånden skal nedlægges igen (når de er gledet ud af skærmen).

Koden til Snowflake-klassen ser ud som følger:

class Snowflake {  // Klasse der definerer et snefnug
  // Fnuggets egenskaber - position og radius samt y-hastighed
  float x, y, r;
  float v;
  
  // Construktoren, der opretter et nyt objekt
  Snowflake(float x, float v) {
    r = v * 1.5;  // Radius proportional med hastighed
    y = -r;  // Starter lige uden for skærmen
    this.x = x;
    this.v = v;
  }
  
  // Metode der tegner snefnugget
  void draw() {
    circle(x, y, r * 2);
  }
  
  // Metode der opdaterer snefnugget (flytter positionen ud fra hastigheden)
  void update() {
    y += v;
  }
  
  boolean checkBelow(float ground) {
    if (y > ground) {
      return true;
    }
    return false;
  }
}

Den måde ArrayList'en oprettes på er ved følgende kode:

ArrayList<Snowflake> flakes = new ArrayList<Snowflake>();

Det der sker i oprettelsen af listen er, at der laves en plads til at der kan fyldes Snowflake objekter i listen, men fra starten er den tom.

Når man så vil have et nyt objekt ind i listen, så gøres det som følger:

      flakes.add(new Snowflake(random(10, width-10), random(3, 5)));

Når man opretter et snefnug ved at kalde constructoren i klassen, så placeres snefnugget altid i toppen af skærmen, og ud fra første parameter placeres det inden for bredden af skærmen (i tilfældet her i en tilfældig position i x-retningen). Den anden parameter man angiver sætter størrelsen på fnugget (den sættes tilfældigt mellem 3 og 5), og størrelsen angiver så samtidigt hastigheden for snefnugget i y-retningen.

Det at kunne fjerne igen fra listen kræver lidt omtanke. For det første, så skal man løbe listen igennem, men der er det problem, at når man fjerne et element fra en liste, så vil størrelsen af listen ændre sig, og når man så kigger på næste element, efter man har fjernet et element, så vil man komme til at springe over et element, som ikke bliver tjekket. Begge problemer løses ved at starte bagfra - altså det sidste element i listen, og så løbe igennem til det forreste element. Det gør at man hele tiden har styr på hvilket element man er i gang med, og da man er startet bagfra, så gør det ikke noget at størrelsen ændrer sig.

Det kan ses i følgende kode:

  for (int i = flakes.size() - 1; i >= 0; i--) {
    if (flakes.get(i).checkBelow(height)) {
      flakes.remove(i);
    }
  }

I den viste kode anvendes en test ved hjælp af metoden checkBelow(). Det gøres faktisk med de to forskellige metoder get() og checkBelow(), der kaldes med koden flakes.get(i).checkBelow(height). Først hentes elementet nr. i med get-metoden (get metoden er indbygget i ArrayList, og er derfor ikke defineret i klassen). Når man har hentet det aktuelle element anvendes checkBelow-metoden (denne metode er defineret i klassen) på det aktuelle element ud fra parameteren height, der er højden på skærmen. Metoden returnerer så sandt, hvis elementet er ude af skærmen. Hvis elementet skal fjernes, så anvendes remove-metoden, der er indbygget i ArrayList, og metoden fjerner objektet fra ArrayList'en og sletter det i hukommelsen, så det ikke ender med at hukommelsen fyldes op med ikke aktive elementer.

Selve håndteringen og visningen af snefnuggene sker i følgende kode:

  println(flakes.size());
  for (Snowflake f : flakes) {
    f.draw();  // Tegn snefnuggene
    if (running) {
      f.update();  // Flyt snefnuggene
    }
  }

Her løbes alle snefnuggene i ArrayListen flakes igennem, og de tegnes på skærmen med metoden draw(). Ved hjælp af knappen i programmet (ikke vist i denne kode) kan man sætte variablen running true / false, og hvis den er true, så opdateres snefnuggene, hvilket betyder at de flytter sig nedad på skærmen.

Dette giver et program med følgende udseende:

Skærmen med snefnug på vej med over skærmen og en knap, som kan aktivere deres fald

Klasser som biblioteker

Det viser sig at det ikke er helt enkelt at konvertere en klasse til et rigtigt bibliotek - det er i skrivende stund ikke lykkedes for forfatteren.

Den simple måde at genbruge på er at tage fx. Button.pde og kopiere ind i et nyt projekt, hvor man så kan bruge klassen, uden at gøre andet - det er også derfor at det er en god ide at holde hver klasse for sig i sin egen fil, og at navngive filerne med samme navn som klassen.

De biblioteker man kan tilføje til processing ligger på github[2] og er sandsynligvis igennem en længere godkendelsesproces.

Hvis man vil videre med det, så ser det ud til at man skal kunne kode java (der ligner processing-sproget i sin grundsyntaks). Et sted at starte kunne være i en video på youTube, der handler om hvordan man laver et processing (java) bibliotek.

Koden

Den samlede kode fra eksemplerne ligger i Denne ZIP-fil. Det kan være en god ting at have koden downloadet og Processing[1] installeret, når man læser siden.

Referencer

Spiludvikling - Processing
Bruger-input mousePressed() - mouseReleased() - mouseClicked() - mouseMoved() - mouseDragged() - mouseX og mouseY - mouseButton - mousePressed - keyPressed() - keyReleased() - keyTyped() - key - keyCode - keyPressed - Flere taster
Data-model Objekt Placering - Objekt Hastighed - Spilleplade - Flere Objekter - Array i Spil - 2-dimensionelt Array - Spil Hop - Highscore - Start-skærm
Visning Rektangel - Cirkel - Spilleplade - Grafikobjekter - Farver i Spil - Kollision
Spil-tid frameRate - Objekter i Bevægelse - Spilletid - Spil Opdatering - Udvikling i Tid
Objekter Objekter i Bevægelse