Mit rumskib styrter ned

Emner på denne side: Computer, Programmering, Spil

Hvis du ikke har været med fra starten, så start med at læse, at jeg har bygget et rumskib .

Sidst fik jeg bygget et rumskib, jeg kunne flyve rundt på skærmen på en meget "space-agtig" måde, og i mit lille appendiks fik jeg også tilføjet et par planeter, og en asteroide der jagede spillerens skib.

Planeterne har jeg beholdt, men asteroiden må vende tilbage i et senere kapitel.

Planeterne var ligetil - det var blot at smække nogle sprites på scenen, præcis som rumskibet, og de skulle ikke engang bevæge på sig. Problemet var dog, at de eksisterede i en meget luftig form - rumskibet kunne flyve lige igennem dem.

Vi kan ikke komme udenom det! Vi vil se rumskibet springe i luften, når det støder ind i noget, og det betyder at jeg skal til at lege med animationer.

Animationer

PIXI har heldigvis et objekt kaldet en AnimatedSprite som kan bruges til dette formål, og det fungerer stort set som en helt almindelig sprite, bortset fra at man i stedet for at initiere den med et enkelt billede, initierer den med et array af billeder, der så udgør de enkelte billeder i animationen. Så jeg drog imod Internettet og fandt en eksplosion udgjort af 47 billeder, eller faktisk udgjort af et enkelt billede i 47 segmenter.

Det viser sig nemlig, at man typisk samler alle billeder i animationen i et enkelt billede, og så har PIXI et lille JSON-format, der beskriver hvor på dette store billede, man kan finde de enkelte animationsbilleder. Og selv om man kunne skabe disse filer i hånden, så findes der faktisk programmer, der kan skabe disse filer. Jeg valgte et kaldet Free Texture Packer , primært fordi det var gratis. Det kan lige præcis det, det skal kunne - hverken mere eller mindre.

Et uddrag af den resulterende JSON-fil ser ud som følger:

 {
  "frames": {
    "Explosion/00": {
      "frame": {
        "x": 1826,
        "y": 853,
        "w": 155,
        "h": 156
      },
      "rotated": true,
      "trimmed": true,
      "spriteSourceSize": {
        "x": 62,
        "y": 24,
        "w": 155,
        "h": 156
      },
      "sourceSize": {
        "w": 256,
        "h": 248
      },
      "pivot": {
        "x": 0.5,
        "y": 0.5
      }
    },
	"Explosion/01": {
      "frame": {
        "x": 1669,
        "y": 853,
        "w": 157,
        "h": 157
      },
      "rotated": false,
      "trimmed": true,
      "spriteSourceSize": {
        "x": 61,
        "y": 24,
        "w": 157,
        "h": 157
      },
      "sourceSize": {
        "w": 256,
        "h": 248
      },
      "pivot": {
        "x": 0.5,
        "y": 0.5
      }
    }
  },
  "meta": {
    "app": "http://free-tex-packer.com",
    "version": "0.6.7",
    "image": "Explosion.png",
    "format": "RGBA8888",
    "size": {
      "w": 2008,
      "h": 1558
    },
    "scale": 1
  }
}  

Det er egentligt rørende simpelt. Det fortæller hvad grundfilen hedder, hvor stor den er og hvor de enkelte billedsegmenter ligger på det samlede billede.

I koden kan man så indlæse billederne som følger:

 PIXI.Assets.load('Explosion.json');  

Dette sørger for at billederne bliver refererbare med de navne der står i JSON-filen, dvs. Explosion/00, Explosion/01 osv. op til Explosion/47. Det næste er så, at jeg skal konvertere billederne til individuelle "textures" og det kan gøres meget let, for load-funktionen returnerer et promise , så ved at udvide koden som følger, får jeg skabt et array med hele min eksplosion som 47 texture-objekter:

 var explosionTextures = [];
PIXI.Assets.load('Explosion.json')
    .then(() => {
        // Create textures for each explosion
        for (i = 0; i < 47; i++) {
            const texture = PIXI.Texture.from('Explosion/'+String(i).padStart(2,'0'));
            explosionTextures.push(texture);
        }
    }  

Og så kan jeg skabe min animerede sprite:

 var explosion_sprite = new PIXI.AnimatedSprite(explosionTextures);  

Kollision

Nu skal vi så bare finde ud af, hvornår vores skib brager ind i en planet, og det handler jo om at detektere kollisioner. PIXI har ikke noget indbygget kollisions-system, men mon ikke vi klarer den alligevel? En måde at lave simpel kollisionsdetektion er vha. såkaldte bounding boxes, hvor man blot beregner en firkant, som er stor nok til at indeholde ens objekt, og så hele tiden checker om de to firkanter overlapper, hvilket er simpelt nok.

Det kan tit klare det godt nok, så man slipper for de markant tungere beregninger af, om objekterne pixel for pixel rører ved hinanden, men lige præcis med planeterne her, er det ikke velegnet, da hjørnerne på bounding boxen ligger så langt væk fra planetens overflade, at man uden tvivl ville bemærke, at der ikke var en kollision.

Til gengæld er der jo den rare egenskab ved cirkler, at de har en radius som fortæller, hvor langt der er fra deres centrum til kanten af dem, så det bliver basis for min kollisionsdetektion. Jeg sammenligner blot rumskibets afstand til centrum af alle planeter, og hvis afstanden bliver lig med eller mindre end planetens radius (+ en sjat, da skibet jo også fylder lidt), så ved jeg, at de er kollideret.

Så er det blot at lime det hele sammen, og det gør jeg ved at smide alle mine planet-sprites ind i et array kaldet collision_objects , og så checke hver frame, om der er en kollision. Hvis der er dette, så skjuler jeg rumskibet, flytter eksplosionen hen, hvor rumskibet var, og viser, og afspiller den:

 function() {
    collision_objects.forEach(function(collision_object) {
    var dist = distance(Spaceship.sprite.x, Spaceship.sprite.y, collision_object.sprite.x, collision_object.sprite.y);
    // Check om vi kolliderer.
    if (dist < collision_object.diameter/2+25) {
        // Skjul rumskibet.
        Spaceship.sprite.visible = false;
        // Flyt eksplosionen hen hvor rumskibet var.
        Explosion.sprite.x = Spaceship.sprite.x;
        Explosion.sprite.y = Spaceship.sprite.y;
        // Vis eksplosionen og afspil animationen fra billed 0.
        Explosion.sprite.visible = true;
        Explosion.sprite.gotoAndPlay(0);
    }
};

function distance(x1, y1, x2, y2) {
    return Math.sqrt(Math.pow(Math.abs(x2-x1),2)+Math.pow(Math.abs(y2-y1),2));
}  

Og kapow: Så kan vi smadre vores rumskib ind i en planet!

Tyngdekraft

Nu kan vi stryge ind og ud imellem planeterne, og hvis vi ikke passer på, styrter vi ned, og springer i luften. Men det virker lidt for let, så for at øge sværhedsgraden vil jeg introducere tyngdekraft, således at rumskibet bliver trukket imod planeterne. Det er noget med, at samtlige planeter skal trække lidt i rumskibet og hvad lidt er, må afhænge af, hvor tæt rumskibet er på planeten, dvs. jo tættere skibet er på, jo mere skal planeten trække.

Jeg tildeler hver planet en gravity -værdi, som er planetens tiltrækningskraft i pixels/s/s og så definerer jeg, at hver gang vi fjerner os, hvad der modsvarer en gang planetens diameter fra dens centrum, så halverer vi kraften.

 var dist = distance(Spaceship.sprite.x, Spaceship.sprite.y, Planet.sprite.x, Planet.sprite.y);
var force = Planet.gravity / (dist/Planet.diameter);  

Force er altså den værdi vi skal trække i rumskibet med (i pixels/s/s), så for at gøre dette, så skal vi først finde retningen fra rumskibet til planeten, hvor man kan bruge funktionen atan2() , der returnerer vinklen i radianer på den vektor man giver, så hvis man blot giver vektoren fra rumskibet til planeten, så får man retningen i radianer. Herfra kan vi så blot lave den sædvanlige sinus/cosinus beregning og gange kraften på - præcis som når vi bevæger rumskibet.

 // Retningen fra rumskibet til planeten
var direction = Math.PI-Math.atan2(Spaceship.sprite.y-Planet.sprite.y, Spaceship.sprite.x-Planet.sprite.x);

// Påvirk rumskibets bevægelses-vektor med en passende kraft i denne retning.
Spaceship.velocity_x += Math.cos(direction) * force / 60;
Spaceship.velocity_y -= Math.sin(direction) * force / 60;  

Og så er det ellers med at være klar på tasterne, for hvis man ikke navigerer, så styrter rumskibet ned!

Scroll

Når jeg er nået hertil, så er jeg også begyndt at blive yderst irriteret over skærmkanten. Hvis rumskibet flyver ud af skærmen, så har man højst sandsynligt tabt det for evigt, med mindre man havde en meget god fornemmelse af, hvad retning det pegede, og hvad hastighed det havde.

Jeg vil derfor have, at skærmen følger med rumskibet, når det nærmer sig kanten af denne.

Hele PIXI er opbygget i et hierarki. Du starter med en stage , hvor du kan tilføje dine sprites. Men du kan også tilføje sprites til andre sprites, og du har også et container -objekt, der kan tilføjes til din scene og igen indeholde yderligere sprites. Alle positioner fortolkes relativt til det objekt elementerne er tilknyttet, så i princippet burde jeg kunne tilføje alle mine spil-objekter til en container fremfor direkte til scenen, og så blot flytte denne container rundt således at rumskibet forbliver på skærmen.

Dvs. jeg blot bygger hele min bane i min container , og så sørger jeg for at rykke denne container rundt, således at rumskibet altid er synligt.

Det er meget let at lave containeren. Det er blot at smide den ind imellem scenen og objekterne, dvs.:

 var sprite = PIXI.Sprite.from('spaceship.png');
game.stage.addChild(sprite);  

bliver til...

 var sprite = PIXI.Sprite.from('spaceship.png');
var container = PIXI.Container();
container.addChild(sprite);
game.stage.addChild(container);  

Min logik bliver, at hvis rumskibet kommer indenfor 400px af skærmkanten, så begynder jeg at scrolle. Men som jeg skrev før, så gives alle koordinater i forhold til det objekt der ligger over dette, hvilket betyder at mit rumskibs koordinat er dets placering i forhold til containeren og ikke i forhold til skærmen.

Heldigvis har alle sprites en funktion kaldet getGlobalPosition(), der meget bekvemt returnerer deres globale position, dvs. positionen i forhold til skærmen. Jeg skal derfor sikre mig, at hvis mit rumskib f.eks. kun er 300px fra skærmkanten, så skal jeg flytte min container 100px for at få rumskibet tilbage til de 400px som jeg havde sat mig for. I kode bliver dette til:

 var scroll_position_x = 0; // Hvor langt er skærmen scrollet på x-aksen?
var scroll_position_y = 0; // Hvor langt er skærmen scrollet på y-aksen?

// Hent rumskibets globale position, der også modsvarer positionen på skærmen.
var global_position = Spaceship.sprite.getGlobalPosition();
        
if (global_position.x < 400) this.scroll_position_x -="" 400-global_position.x; // Vi skal scrolle til venstre
if (global_position.x>$(window).innerWidth()-400) scroll_position_x += 400-($(window).innerWidth()-global_position.x); // Vi skal scrolle til højre

if (global_position.y < 400) this.scroll_position_y -="" 400-global_position.y; // Vi skal scrolle ned
if (global_position.y>$(window).innerHeight()-400) scroll_position_y += 400-($(window).innerHeight()-global_position.y); // Vi skal scrolle op

// Vi flytter ikke skærmen, men vi flytter på containeren, der ligger "bag" skærmen. 
// Derfor bruger vi negative værdier.
container.setTransform(-this.scroll_position_x, -this.scroll_position_y);  

Og vupti. Så kan man ikke længere flyve ud af skærmen.

Version 2

Og det bliver så langt vi kommer for nuværende. Du kan prøve version 2 her (hvis du sidder ved en computer og har et tastatur) , og hvis du ønsker hele kildeteksten, er du velkommen til downloade den her .

Du kan nu læse næste kapitel i Lir på rumskibet