I denne opplæringen vil jeg gå gjennom hvordan du analyserer et fliser, fliser gjennom dem og finn kamper. Vi skal skape et spill hvor du må koble linjer sammen for å danne helt lukkede baner uten åpne ender. For å forenkle ting, vil vi bruke bitmasking som en del av vår algoritme ved å tilordne hver flis (pluss dens rotasjon) sitt eget bitmasknummer. Ikke bekymre deg hvis du ikke vet hva bitmasking er. Det er faktisk veldig enkelt!
Relaterte innleggJeg skal skape prosjektet i C # ved hjelp av Unity with the Futile-rammen, men koden vil gjelde for stort sett alle 2D-rammer med få endringer. Her er Github repo med hele Unity-prosjektet. Og under er en spillbar demonstrasjon av spillet som vi skal gjøre:
Da jeg begynte å lage Polymer, ønsket jeg å skape noe annet enn et match-3-spill. Mitt interne kallenavn for det var et "match-any" -spill. Match-3 puslespill er overalt. Selv om de sikkert kan være morsomme, kan en grunn til at de er så vanlige, være fordi algoritmen for å finne tre matcher er ganske enkel.
Jeg ønsket å kunne matche flere fliser som kunne veve inn og ut av rader og kolonner, og snakket seg over bordet. Ikke bare det, men jeg ville ikke ha et enkelt fargevalgspill. Jeg ønsket at kampene skulle baseres på bestemte sider av flisene (for eksempel kan en form kun koble til andre former på venstre og høyre side, men ikke topp og bunn.) Dette viste seg å være mye mer komplisert enn bare en normal match-3-algoritme.
Denne opplæringen deles opp i tre seksjoner: Tile, Matchgruppen og Spillebrettet. I denne opplæringen vil jeg prøve å unngå så mye Futile-spesifikk kode som mulig. Hvis du vil se Futile-spesifikke ting, se kilden. Også, jeg skal ikke vise alle metoder og variabler i dette innlegget. Bare de viktigste. Så hvis du tror noe mangler, se igjen på kildekoden.
Ordet "bitmask" refererer til måten du kan lagre en serie av ekte / falske verdier på i en enkelt numerisk variabel. Fordi tallene er representert av de og nullene når de representeres i binær, ved å endre nummeret kan du slå på eller av verdier ved å bytte om en bit er 1 eller 0.
For mer detaljer, se denne artikkelen på bitwise operatører og denne artikkelen på binære tall.
Vår første klasse heter LineTile. Før begynnelsen av klassen, la oss definere hver type fliser.
// De forskjellige flisetyper: offentlig enum LineTileType Nub, Line, Corner, Threeway, Cross, MAX
Slik ser brikkene ut:
Deretter, siden vi bare tillater rotasjoner på 90 grader, la oss lage en enum
for rotasjon.
// Jeg bruker dette i stedet for eksakte grader siden // fliser skal bare ha fire forskjellige rotasjoner: offentlig enum RotationType Rotation0, Rotation90, Rotation180, Rotation270, MAX
Neste er a struct
kalt TileIndex
, som i utgangspunktet er den samme som a Vektor2
, unntatt med ints i stedet for flyter. Det vil bli brukt til å holde oversikt over hvor en flis er i spillbrettet.
offentlig struktur TileIndex offentlig int xIndex; offentlig int yIndex; offentlig TileIndex (int xIndex, int yIndex) this.xIndex = xIndex; this.yIndex = yIndex;
Til slutt, la oss definere de tre typer sammenhenger mellom to fliser.
public enum TileConnectionType // En mismatch. Ugyldig, // Teglene kobles ikke direkte, // men ikke på grunn av en uendelig kant. ValidWithOpenSide, // Flisene kobles direkte. ValidWithSolidMatch
Deretter definerer du i løpet av klassen en bitmask til hver side av en generisk flis.
// Her er brikkene jeg tilordnet hver side av flisen: // ===== 1 ===== // | | // | | // 8 2 // | | // | | // ===== 4 ==== // // == 0001 i binær // 2 == 0010 i binær // 4 == 0100 i binær // 8 == 1000 i binær offentlig const int kBitmaskNone = 0; offentlig const int kBitmaskTop = 1; offentlig const int kBitmaskRight = 2; offentlig const int kBitmaskBottom = 4; offentlig const int kBitmaskLeft = 8;
Definer deretter instansvariablene som hver flis vil ha.
// Flisens representasjon av flisen: offentlig FSprite sprite; // Type av flis: offentlig LineTileType lineTileType get; privat sett; // Rotasjonen til flisen: offentlig RotasjonType rotationType get; privat sett; // Bitmask som representerer flisen med sin rotasjon: offentlig int bitmask get; privat sett; // Flisens plassering på brettet: offentlig TileIndex tileIndex = ny TileIndex ();
For konstruktøren, opprett sprite og sett den opp ved riktig rotasjon. Det er noe Futile-spesifikk kode her inne, men det skal være veldig lett å forstå.
Offentlig LineTile (LineTileType lineTileType, RotationType rotationType) this.lineTileType = lineTileType; this.rotationType = rotationType; // Sett opp sprite: switch (lineTileType) case LineTileType.Nub: sprite = new FSprite ("lineTileNub"); gå i stykker; tilfelle LineTileType.Line: sprite = nytt FSprite ("lineTileLine"); gå i stykker; case LineTileType.Corner: sprite = ny FSprite ("lineTileCorner"); gå i stykker; case LineTileType.Threeway: sprite = nytt FSprite ("lineTileThreeway"); gå i stykker; tilfelle LineTileType.Cross: sprite = nytt FSprite ("lineTileCross"); gå i stykker; standard: kaste nye FutileException ("ugyldig linje fliser type"); AddChild (sprite); // Sett opp spritrotasjon: bryter (rotationType) tilfelle RotationType.Rotation0: sprite.rotation = 0; gå i stykker; case RotationType.Rotation90: sprite.rotation = 90; gå i stykker; case RotationType.Rotation180: sprite.rotation = 180; gå i stykker; case RotationType.Rotation270: sprite.rotation = 270; gå i stykker; standard: kaste nye FutileException ("ugyldig rotasjonstype");
Nå, en av de viktigste delene. Vi tilordner hver flis, kombinert med rotasjonen, en bitmask som avgjøres av hvilken av sidene som er solid og som er åpne.
// Sett opp bitmask ved å gjøre bitvis ELLER med hver side som er inkludert i formen. / / For eksempel vil en flis som har alle fire sider solid (f.eks. Korsflisen) være // 1 | 2 | 4 | 8 = 15, som er det samme som 0001 | 0010 | 0100 | 1000 = 1111 i binær. hvis (lineTileType == LineTileType.Nub) hvis (rotationType == RotationType.Rotation0) bitmask = kBitmaskTop; hvis (rotationType == RotationType.Rotation90) bitmask = kBitmaskRight; hvis (rotationType == RotationType.Rotation180) bitmask = kBitmaskBottom; hvis (rotationType == RotationType.Rotation270) bitmask = kBitmaskLeft; hvis (lineTileType == LineTileType.Line) hvis (rotationType == RotationType.Rotation0 || rotationType == RotationType.Rotation180) bitmask = kBitmaskTop | kBitmaskBottom; hvis (rotationType == RotationType.Rotation90 || rotationType == RotationType.Rotation270) bitmask = kBitmaskRight | kBitmaskLeft; hvis (lineTileType == LineTileType.Corner) hvis (rotationType == RotationType.Rotation0) bitmask = kBitmaskTop | kBitmaskRight; hvis (rotationType == RotationType.Rotation90) bitmask = kBitmaskRight | kBitmaskBottom; hvis (rotationType == RotationType.Rotation180) bitmask = kBitmaskBottom | kBitmaskLeft; hvis (rotationType == RotationType.Rotation270) bitmask = kBitmaskLeft | kBitmaskTop; hvis (lineTileType == LineTileType.Threeway) hvis (rotationType == RotationType.Rotation0) bitmask = kBitmaskTop | kBitmaskRight | kBitmaskBottom; hvis (rotationType == RotationType.Rotation90) bitmask = kBitmaskRight | kBitmaskBottom | kBitmaskLeft; hvis (rotationType == RotationType.Rotation180) bitmask = kBitmaskBottom | kBitmaskLeft | kBitmaskTop; hvis (rotationType == RotationType.Rotation270) bitmask = kBitmaskLeft | kBitmaskTop | kBitmaskRight; hvis (lineTileType == LineTileType.Cross) bitmask = kBitmaskTop | kBitmaskRight | kBitmaskBottom | kBitmaskLeft;
Våre fliser er satt opp og vi er klare til å begynne å samsvare dem sammen!
Matchgrupper er nettopp det: grupper av fliser som samsvarer med (eller ikke). Du kan starte på en hvilken som helst flis i en kampgruppe og nå andre fliser gjennom sine tilkoblinger. Alle fliser er tilkoblet. Hver av de forskjellige farger indikerer en annen kampgruppe. Den eneste som er ferdig er den blå i midten - den har ingen ugyldige tilkoblinger.
Kampgruppen klassen er faktisk ekstremt enkel. Det er egentlig bare en samling fliser med noen få hjelpefunksjoner. Her er det:
offentlig klasse MatchGroup offentlig listefliser; offentlig bool isClosed = true; offentlig MatchGroup () fliser = ny liste (); Offentlig tomgang SetTileColor (Fargefarge) foreach (LineTile fliser i fliser) tile.sprite.color = color; offentlig tomrom Destroy () tiles.Clear ();
Dette er langt den mest kompliserte delen av denne prosessen. Vi må analysere hele styret, splitte det opp i sine individuelle kampgrupper, og deretter avgjøre hvilke som er helt lukket. Jeg skal ringe denne klassen BitmaskPuzzleGame
, siden det er hovedklassen som omfatter spilllogikken.
Før vi kommer inn i implementeringen skjønner vi, men la oss definere et par ting. Først er det enkelt enum
at pilene vil bli tildelt basert en hvilken retning de står overfor:
// For å hjelpe oss med å avgjøre hvilken pil som ble trykket: Offentlig Enum Retning Opp, Høyre, Ned, Venstre
Neste er a struct
som vil bli sendt fra en pil som blir presset, slik at vi kan bestemme hvor det er i brettet og hvilken retning den står overfor:
// Når en pil er trykket, vil den inneholde disse dataene for å finne ut hva som skal gjøres med brettet: offentlig struktur ArrowData offentlig retningsretning; offentlig int indeks; offentlig ArrowData (retningsretning, int indeks) this.direction = retning; this.index = index;
Neste, i klassen, definerer instansvariablene vi trenger:
// Inneholder alle kartets fliser: offentlig LineTile [] [] tileMap; // Inneholder alle gruppene av tilkoblede fliser: offentlig listematchGroups = ny liste (); // Når en rad / kolonne er flyttet, er denne satt til sann, slik at HandleUpdate kan oppdatere: private bool matchGroupsAreDirty = true; // Hvor mange fliser brettet brettet er: privat int tileMapWidth; // Hvor mange fliser høye brettet er: privat int tileMapHeight;
Her er en funksjon som tar en flis og returnerer alle sine omkringliggende fliser (de over, under, til venstre og til høyre for det):
// Hjelpermetode for å få alle flisene som er over / under / høyre / venstre for en bestemt flis: privat listeGetTilesSurroundingTile (LineTile fliser) Liste surroundingTiles = ny liste (); int xIndex = tile.tileIndex.xIndex; int yIndex = tile.tileIndex.yIndex; hvis (xIndex> 0) surroundingTiles.Add (tileMap [xIndex - 1] [yIndex]); hvis (xIndex < tileMapWidth - 1) surroundingTiles.Add(tileMap[xIndex + 1][yIndex]); if (yIndex > 0) surroundingTiles.Add (tileMap [xIndex] [yIndex - 1]); hvis (yIndex < tileMapHeight - 1) surroundingTiles.Add(tileMap[xIndex][yIndex + 1]); return surroundingTiles;
Nå to metoder som returnerer alle flisene i enten en kolonne eller rad, slik at vi kan skifte dem:
// Hjelpermetode for å få alle flisene i en bestemt kolonne: Private LineTile [] GetColumnTiles (int columnIndex) if (columnIndex < 0 || columnIndex >= tileMapWidth) kaste nye FutileException ("ugyldig kolonne:" + columnIndex); LineTile [] columnTiles = ny LineTile [tileMapHeight]; for (int j = 0; j < tileMapHeight; j++) columnTiles[j] = tileMap[columnIndex][j]; return columnTiles; // Helper method to get all the tiles in a specific row: private LineTile[] GetRowTiles(int rowIndex) if (rowIndex < 0 || rowIndex >= tileMapHeight) kaste nye FutileException ("ugyldig kolonne:" + rowIndex); LineTile [] rowTiles = ny LineTile [tileMapWidth]; for (int i = 0; i < tileMapWidth; i++) rowTiles[i] = tileMap[i][rowIndex]; return rowTiles;
Nå to funksjoner som faktisk skifter en kolonne eller rad av fliser i en bestemt retning. Når en flis skifter av en kant, løkker den rundt til den andre siden. For eksempel vil et høyre skift på en rad med Nub, Cross, Line resultere i en rad med Line, Nub, Cross.
// Skift fliser i en kolonne enten opp eller ned (med innpakning). privat tomrom ShiftColumnInDirection (int columnIndex, Direction dir) LineTile [] currentColumnArrangement = GetColumnTiles (columnIndex); int nextIndex; // Flytt flisene, slik at de er i de riktige stedene i flisekartet. hvis (dir == Direction.Up) for (int j = 0; j < tileMapHeight; j++) nextIndex = (j + 1) % tileMapHeight; tileMap[columnIndex][nextIndex] = currentColumnArrangement[j]; tileMap[columnIndex][nextIndex].tileIndex = new TileIndex(columnIndex, nextIndex); else if (dir == Direction.Down) for (int j = 0; j < tileMapHeight; j++) nextIndex = j - 1; if (nextIndex < 0) nextIndex += tileMapHeight; tileMap[columnIndex][nextIndex] = currentColumnArrangement[j]; tileMap[columnIndex][nextIndex].tileIndex = new TileIndex(columnIndex, nextIndex); else throw new FutileException("can't shift column in direction: " + dir.ToString()); // Once the tileMap array is set up, actually visually move the tiles to their correct spots. for (int j = 0; j < tileMapHeight; j++) tileMap[columnIndex][j].y = (j + 0.5f) * tileSize; matchGroupsAreDirty = true; // Shift the tiles in a row either right or left one (with wrapping). private void ShiftRowInDirection(int rowIndex, Direction dir) LineTile[] currentRowArrangement = GetRowTiles(rowIndex); int nextIndex; // Move the tiles so they are in the correct spots in the tileMap array. if (dir == Direction.Right) for (int i = 0; i < tileMapWidth; i++) nextIndex = (i + 1) % tileMapWidth; tileMap[nextIndex][rowIndex] = currentRowArrangement[i]; tileMap[nextIndex][rowIndex].tileIndex = new TileIndex(nextIndex, rowIndex); else if (dir == Direction.Left) for (int i = 0; i < tileMapWidth; i++) nextIndex = i - 1; if (nextIndex < 0) nextIndex += tileMapWidth; tileMap[nextIndex][rowIndex] = currentRowArrangement[i]; tileMap[nextIndex][rowIndex].tileIndex = new TileIndex(nextIndex, rowIndex); else throw new FutileException("can't shift row in direction: " + dir.ToString()); // Once the tileMap array is set up, actually visually move the tiles to their correct spots. for (int i = 0; i < tileMapWidth; i++) tileMap[i][rowIndex].x = (i + 0.5f) * tileSize; matchGroupsAreDirty = true;
Når vi klikker på en pil (det vil si når pilknappen slippes), må vi avgjøre hvilken rad eller kolonne som skal skiftes, og i hvilken retning.
// Når en pil trykkes og slippes, skift en kolonne opp / ned eller en rad til høyre / venstre. offentlig tomrom ArrowButtonReleased (FButton-knapp) ArrowData arrowData = (ArrowData) button.data; hvis (arrowData.direction == Direction.Up || arrowData.direction == Direction.Down) ShiftColumnInDirection (arrowData.index, arrowData.direction); ellers hvis (arrowData.direction == Direction.Right || arrowData.direction == Direction.Left) ShiftRowInDirection (arrowData.index, arrowData.direction);
De to neste metodene er de viktigste i spillet. Den første tar to fliser og bestemmer hvilken type tilkobling de har. Den baserer forbindelsen på først fliser input til metoden (kalles baseTile
). Dette er et viktig skillestykke. De baseTile
kunne ha en ValidWithOpenSide
forbindelse med otherTile
, men hvis du skriver dem i omvendt rekkefølge, kan den returnere Ugyldig
.
// Det er tre typer tilkoblinger to fliser kan ha: // 1. ValidWithSolidMatch-dette betyr at flisene er nøyaktig tilpasset med de faste sidene som er koblet til. // 2. ValidWithOpenSide-dette betyr at baseTile har en åpen side som berører den andre flisen, så det spiller ingen rolle hva den andre flisen er. // 3. Ugyldig-dette betyr at baseTiles solid side er i samsvar med den andre sideens side, noe som resulterer i en feilstilling. private TileConnectionType TileConnectionTypeBetweenTiles (LineTile baseTile, LineTile otherTile) int baseTileBitmaskSide = baseTile.bitmask; // Bitmask for den spesifikke baseTile-siden som berører den andre flisen. int otherTileBitmaskSide = otherTile.bitmask; // Bitmask for den spesifikke andre side siden som berører bunnflisen. // Avhengig av hvilken side av bunnflisen den andre flisen er på, bitvis og hver side sammen. med // den bitvise konstanten for den enkelte side. Hvis resultatet er 0, er siden åpen. Ellers, // siden er solid. hvis (otherTile.tileIndex.yIndex < baseTile.tileIndex.yIndex) baseTileBitmaskSide &= LineTile.kBitmaskBottom; otherTileBitmaskSide &= LineTile.kBitmaskTop; else if (otherTile.tileIndex.yIndex > baseTile.tileIndex.yIndex) baseTileBitmaskSide & = LineTile.kBitmaskTop; otherTileBitmaskSide & = LineTile.kBitmaskBottom; annet hvis (otherTile.tileIndex.xIndex < baseTile.tileIndex.xIndex) baseTileBitmaskSide &= LineTile.kBitmaskLeft; otherTileBitmaskSide &= LineTile.kBitmaskRight; else if (otherTile.tileIndex.xIndex > baseTile.tileIndex.xIndex) baseTileBitmaskSide & = LineTile.kBitmaskRight; otherTileBitmaskSide & = LineTile.kBitmaskLeft; hvis (baseTileBitmaskSide == 0) returnere TileConnectionType.ValidWithOpenSide; // baseTile side berører otherTile er åpen. ellers hvis (otherTileBitmaskSide! = 0) returnerer TileConnectionType.ValidWithSolidMatch; // baseTile side og otherTile side er solid og matchet. ellers returnerer TileConnectionType.Invalid; // baseTile side er solid, men otherTile side er åpen. Mismatch!
Endelig, UpdateMatches
. Dette er den viktigste metoden for alle. Dette er den som går gjennom styret, analyserer alle brikkene, bestemmer hvilken forbindelse som er koblet til hverandre, og hvilke kampgrupper er helt lukket. Alt er forklart i kommentarene.
// Gå gjennom brettet og analyser alle fliser, leter etter kamper: private void UpdateMatches () // Kampgrupper blir oppdatert slik at de ikke lenger er skitne: matchGroupsAreDirty = false; // Siden glidende kolonner og rader kan ødelegge alt, må vi kvitte seg med de gamle kampgruppene og begynne over. // Husk at det er sannsynligvis en måte å bruke algoritmen på, hvor vi ikke trenger å bli kvitt alle kampene og / / starte over hver gang (si, oppdater bare kampene som forstyrres av et skifte), men det kan komme senere hvis / / du må forbedre ytelsen. foreach (MatchGroup matchGroup i matchGroups) matchGroup.Destroy (); matchGroups.Clear (); // Vi begynner å analysere brettet fra bunnen til venstre. Nåværende basefliser vil være den / / som vi for øyeblikket starter fra og bygger kampgrupper av. LineTile currentBaseTile = fliserMap [0] [0]; ListetileSurrounders; // Variabel som vil lagre omkringliggende fliser av ulike grunnfliser. Liste checkedTiles = ny liste (); // Vi lagrer basisfliser her etter at de har blitt analysert, slik at vi ikke reanalyserer dem. MatchGroup currentMatchGroup; // Den kampgruppen vi analyserer som inkluderer den nåværende baseflisen. // Loop kontinuerlig gjennom brettet, lag kampgrupper til det ikke er flere fliser å lage kampgrupper fra. mens (currentBaseTile! = null) // Opprett en ny kampgruppe, legg til den nåværende bunnplaten som sin første flis. currentMatchGroup = ny MatchGroup (); currentMatchGroup.tiles.Add (currentBaseTile); // Gå gjennom flisene som starter på nåværende flis, analyser forbindelsene, finn en ny flis, flis og igjen, til du ikke finner flere mulige forbindelser med noen av flisene i kampgruppen bool stillWorkingOnMatchGroup = true; mens (stillWorkingOnMatchGroup) // Populere tileSurrounders-listen med alle fliser som omgir den nåværende basisflisen: tileSurrounders = GetTilesSurroundingTile (currentBaseTile); // Iterer gjennom alle de omkringliggende fliser og kontroller om deres faste sider er justert med grunnflisens faste sider: foreach (LineTile surroundingTile in tileSurrounders) TileConnectionType connectionType = TileConnectionTypeBetweenTiles (currentBaseTile, surroundingTile); // Hvis det er en solid kamp, legg til surrounderen til kampgruppen. // Hvis det er feil, er kampgruppen ikke en perfekt "lukket" kampgruppe. // Hvis det er en feilmatch på grunn av en åpen side av bunnflisen, betyr det egentlig ingen rolle // siden det ikke er en solid side som kuttes av (dette kalles TileConnectionType.ValidWithOpenSide). hvis (connectionType == TileConnectionType.ValidWithSolidMatch) currentMatchGroup.tiles.Add (surroundingTile); ellers hvis (TileConnectionTypeBetweenTiles (currentBaseTile, surroundingTile) == TileConnectionType.Invalid) currentMatchGroup.isClosed = false; // Hvis bunnplaten har en lukket / solid side som berører kanten av brettet, kan kampgruppen ikke lukkes. hvis ((currentBaseTile.bitmask & LineTile.kBitmaskTop)! = 0 && currentBaseTile.tileIndex.yIndex == tileMapHeight - 1) || ((currentBaseTile.bitmask & LineTile.kBitmaskRight)! = 0 && currentBaseTile.tileIndex.xIndex == tileMapWidth - 1) || ((currentBaseTile.bitmask & LineTile.kBitmaskBottom)! = 0 && currentBaseTile.tileIndex.yIndex == 0) || ((currentBaseTile.bitmask & LineTile.kBitmaskLeft)! = 0 && currentBaseTile.tileIndex.xIndex == 0)) currentMatchGroup.isClosed = false; // Legg vår basefliser til en matrise slik at vi ikke sjekker den igjen senere: hvis (! CheckedTiles.Contains (currentBaseTile)) checkedTiles.Add (currentBaseTile); // Finn en ny bunnflise som vi har lagt til i kampgruppen, men har ikke analysert ennå: for (int i = 0; i < currentMatchGroup.tiles.Count; i++) LineTile tile = currentMatchGroup.tiles[i]; // If the checkedTiles array has the tile in it already, check to see if we're on the last // tile in the match group. If we are, then there are no more base tile possibilities so we are // done with the match group. If checkedTiles DOESN'T have a tile in the array, it means // that tile is in the match group but hasn't been analyzed yet, so we need to set it as // the next base tile. if (checkedTiles.Contains(tile)) if (i == currentMatchGroup.tiles.Count - 1) stillWorkingOnMatchGroup = false; matchGroups.Add(currentMatchGroup); else currentBaseTile = tile; break; // We're done with a match group, so now we need to find a new un-analyzed tile that's // not in any match groups to start a new one from. So we'll set currentBaseTile to // null then see if we can find a new one: currentBaseTile = null; for (int i = 0; i < tileMapWidth; i++) for (int j = 0; j < tileMapHeight; j++) LineTile newTile = tileMap[i][j]; if (!TileIsAlreadyInMatchGroup(newTile)) currentBaseTile = newTile; break; if (currentBaseTile != null) break;
Alt vi har igjen er HandleUpdate
funksjon! Hver ramme, oppdater kampgruppene hvis de trenger oppdatering (dvs.. matchGroupsAreDirty == true
), og sett deres farger.
offentlig tomgang HandleUpdate () if (matchGroupsAreDirty) UpdateMatches ();
Slik ser algoritmen ut om hvert trinn ble animert:
Og det er det! Selv om noen av koden i dette er spesifikk for Futile, bør det være ganske klart hvordan å forlenge det til hvilket som helst annet språk eller en motor. Og for å gjenta, er det mange ikke-essensielle ting som mangler i dette innlegget. Vennligst se på kildekoden for å se hvordan alt fungerer sammen!