Sunt un dezvoltator și un dezvoltator de backend, iar expertiza mea în dezvoltarea frontend-ului este relativ slabă. Acum ceva vreme am vrut să mă distrez și să fac un joc într-un browser; Am ales Phaser 3 ca cadru (pare destul de popular în zilele noastre) și TypeScript ca limbaj (pentru că prefer tastarea statică în locul dinamicii). S-a dovedit că trebuie să faci niște lucruri plictisitoare pentru ca totul să funcționeze, așa că am scris acest tutorial pentru a-i ajuta pe ceilalți oameni ca mine să înceapă mai repede.

Pregătirea mediului

IDE

Alegeți-vă mediul de dezvoltare. Puteți folosi oricând Notepad simplu, dacă doriți, dar aș sugera să folosiți ceva mai util. În ceea ce mă privește, prefer să dezvolt proiecte pentru animale de companie în Emacs, prin urmare am instalat maree și a urmat instrucțiunile de configurare.

Nodul

Dacă ne-am dezvolta pe JavaScript, am fi perfect să începem codificarea fără toți acești pași de pregătire. Cu toate acestea, deoarece vrem să folosim TypeScript, trebuie să configurăm infrastructura pentru a face viitoarea dezvoltare cât mai rapidă posibil. Astfel, trebuie să instalăm nod și npm.

În timp ce scriu acest tutorial, îl folosesc nodul 10.13.0 și npm 6.4.1. Vă rugăm să rețineți că versiunile din frontend world se actualizează extrem de rapid, deci pur și simplu luați cele mai recente versiuni stabile. Recomand cu tărie utilizarea nvm în loc să instaleze manual nodul și npm; vă va economisi mult timp și nervi.

Configurarea proiectului

Structura proiectului

Vom folosi npm pentru construirea proiectului, așa că pentru a începe proiectul mergeți într-un folder gol și rulați npm init. npm vă va pune mai multe întrebări despre proprietățile proiectului dvs. și apoi va crea un package.json fişier. Va arăta cam așa:

{
  "name": "Starfall",
  "version": "0.1.0",
  "description": "Starfall game (Phaser 3 + TypeScript)",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "Mariya Davydova",
  "license": "MIT"
}

Pachete

Instalați pachetele de care avem nevoie cu următoarea comandă:

npm install -D typescript webpack webpack-cli ts-loader phaser live-server

-D opțiune (aka --save-dev) face ca npm să adauge aceste pachete la lista de dependențe din package.json automat:

"devDependencies": {
   "live-server": "^1.2.1",
   "phaser": "^3.15.1",
   "ts-loader": "^5.3.0",
   "typescript": "^3.1.6",
   "webpack": "^4.26.0",
   "webpack-cli": "^3.1.2"
 }

Webpack

Webpack va rula compilatorul TypeScript și va colecta grămada de fișiere JS rezultate, precum și biblioteci într-un JS micșorat, astfel încât să îl putem include în pagina noastră.

Adăuga webpack.config.js lângă tine project.json:

const path = require('path');
module.exports = {
  entry: './src/app.ts',
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [ '.ts', '.tsx', '.js' ]
  },
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development'
};

Aici vedem că webpack trebuie să pornească de la surse src/app.ts(pe care îl vom adăuga foarte curând) și vom colecta totul dist/app.js fişier.

TypeScript

De asemenea, avem nevoie de un mic fișier de configurare pentru compilatorul TypeScript (tsconfig.json) în care explicăm ce versiune JS dorim să fie compilate sursele și unde să găsim aceste surse:

{
  "compilerOptions": {
    "target": "es5"
  },
  "include": [
    "src/*"
  ]
}

Definiții TypeScript

TypeScript este un limbaj tipizat static. Prin urmare, necesită definiții de tip pentru compilare. La momentul redactării acestui tutorial, definițiile pentru Phaser 3 nu erau încă disponibile ca pachet npm, deci este posibil să fie necesar să descărcați-le din depozitul oficial și introduceți fișierul în src subdirectorul proiectului dumneavoastră.

Scripturi

Aproape am terminat proiectul înființat. În acest moment ar fi trebuit să creați package.json, webpack.config.js, și tsconfig.json, si adaugat src/phaser.d.ts. Ultimul lucru pe care trebuie să-l facem înainte de a începe să scriem cod este să explicăm ce anume are npm cu proiectul. Actualizăm scripts secțiunea din package.json după cum urmează:

"scripts": {
  "build": "webpack",
  "start": "webpack --watch & live-server --port=8085"
}

Când executați npm build, app.js fișierul va fi construit în conformitate cu configurația webpack. Și când alergi npm start, nu va trebui să vă deranjați despre procesul de construire: de îndată ce salvați orice sursă, webpack va reconstrui aplicația și live-server îl va reîncărca în browserul dvs. implicit. Aplicația va fi găzduită la http://127.0.0.1:8085/.

Noțiuni de bază

Acum, că am creat infrastructura (partea pe care eu personal o urăsc când încep un proiect), putem începe în cele din urmă codarea. În acest pas vom face un lucru simplu: desenăm un dreptunghi albastru închis în fereastra browserului nostru. Utilizarea unui cadru de dezvoltare a jocurilor mari pentru acest lucru este un pic de … hmmm … exagerare. Totuși, vom avea nevoie de el la următorii pași.

Permiteți-mi să explic pe scurt principalele concepte ale Phaser 3. Jocul este o instanță a Phaser.Game clasa (sau descendentul acesteia). Fiecare joc conține una sau mai multe instanțe de Phaser.Scene urmasi. Fiecare scenă conține mai multe obiecte, fie statice, fie dinamice, și reprezintă o parte logică a jocului. De exemplu, jocul nostru trivial va avea trei scene: ecranul de bun venit, jocul în sine și ecranul scorului.

Să începem codarea.

Mai întâi, creați un container HTML minimalist pentru joc. A face o index.html care conține următorul cod:

<!DOCTYPE html>
<html>
  <head>
    <title>Starfall</title>
    <script src="https://www.freecodecamp.org/news/how-to-build-a-simple-game-in-the-browser-with-phaser-3-and-typescript-bdc94719135/dist/app.js"></script>
  </head>
  <body>
    <div id="game"></div>
  </body>
</html>

Există doar două părți esențiale aici: prima este a script intrare care spune că vom folosi fișierul nostru construit aici, iar al doilea este un div intrare care va fi containerul jocului.

Acum creați un fișier src/app.ts cu următorul cod:

import "phaser";
const config: GameConfig = {
  title: "Starfall",
  width: 800,
  height: 600,
  parent: "game"
  backgroundColor: "#18216D"
};
export class StarfallGame extends Phaser.Game {
  constructor(config: GameConfig) {
    super(config);
  }
}
window.onload = () => {
  var game = new StarfallGame(config);
};

Acest cod se explică de la sine. GameConfig are o mulțime de proprietăți diverse, le puteți verifica Aici .

Și acum poți fugi în sfârșit npm start. Dacă totul a fost făcut corect la pașii anteriori și anteriori, ar trebui să vedeți ceva atât de simplu în browser:

Cum sa construiesti un joc simplu in browser cu Phaser
Da, acesta este un ecran albastru.

Făcând să cadă stelele

Am creat o aplicație elementară. Acum este timpul să adăugați o scenă în care se va întâmpla ceva. Jocul nostru va fi simplu: stelele vor cădea la pământ, iar scopul va fi de a prinde cât mai multe.

Pentru a atinge acest obiectiv, creați un fișier nou, gameScene.tsși adăugați următorul cod:

import "phaser";
export class GameScene extends Phaser.Scene {
constructor() {
    super({
      key: "GameScene"
    });
  }
init(params): void {
    // TODO
  }
preload(): void {
    // TODO
  }
  
  create(): void {
    // TODO
  }
update(time): void {
    // TODO
  }
};

Constructor conține aici o cheie sub care alte scene pot numi această scenă.

Vedeți aici butoane pentru patru metode. Permiteți-mi să explic pe scurt diferența dintre atunci:

  • init([params]) se numește când începe scena; această funcție poate accepta parametri, care sunt trecuți din alte scene sau jocuri prin apelare scene.start(key, [params])
  • preload() este apelat înainte de crearea obiectelor scene și conține materiale de încărcare; aceste active sunt stocate în cache, deci atunci când scena este repornită, acestea nu sunt reîncărcate
  • create() se numește atunci când activele sunt încărcate și conține de obicei crearea principalelor obiecte de joc (fundal, jucător, obstacole, dușmani etc.)
  • update([time]) se numește fiecare bifă și conține partea dinamică a scenei – tot ce mișcă, clipește etc.

Pentru a fi siguri că nu o uităm mai târziu, să adăugăm rapid următoarele rânduri în game.ts:

import "phaser";
import { GameScene } from "./gameScene";
const config: GameConfig = {
  title: "Starfall",
  width: 800,
  height: 600,
  parent: "game",
  scene: [GameScene],
  physics: {
    default: "arcade",
    arcade: {
      debug: false
    }
  },
  backgroundColor: "#000033"
};
...

Jocul nostru știe acum despre scena jocului. Dacă configurația jocului conține o listă de scene, atunci prima este pornită când jocul este început, iar toate celelalte sunt create, dar nu pornite până când nu sunt chemate explicit.

De asemenea, am adăugat fizică arcade aici. Este necesar ca stelele noastre să cadă.

Acum putem pune carne pe oasele scenei noastre de joc.

Mai întâi, declarăm câteva proprietăți și obiecte de care vom avea nevoie:

export class GameScene extends Phaser.Scene {
  delta: number;
  lastStarTime: number;
  starsCaught: number;
  starsFallen: number;
  sand: Phaser.Physics.Arcade.StaticGroup;
  info: Phaser.GameObjects.Text;
...

Apoi, inițializăm numerele:

init(/*params: any*/): void {
    this.delta = 1000;
    this.lastStarTime = 0;
    this.starsCaught = 0;
    this.starsFallen = 0;
  }

Acum, încărcăm câteva imagini:

preload(): void {
    this.load.setBaseURL(
      "https://raw.githubusercontent.com/mariyadavydova/" +
      "starfall-phaser3-typescript/master/");
    this.load.image("star", "assets/star.png");
    this.load.image("sand", "assets/sand.jpg");
  }

După aceea, ne putem pregăti componentele statice. Vom crea terenul, unde vor cădea stelele, iar textul ne va informa despre scorul actual:

create(): void {
    this.sand = this.physics.add.staticGroup({
      key: 'sand',
      frameQuantity: 20
    });
    Phaser.Actions.PlaceOnLine(this.sand.getChildren(),
      new Phaser.Geom.Line(20, 580, 820, 580));
    this.sand.refresh();
this.info = this.add.text(10, 10, '',
      { font: '24px Arial Bold', fill: '#FBFBAC' });
  }

Un grup din Phaser 3 este o modalitate de a crea o grămadă de obiecte pe care doriți să le controlați împreună. Există două tipuri de obiecte: statice și dinamice. După cum ați putea ghici, obiectele statice nu se mișcă (sol, pereți, diverse obstacole), în timp ce cele dinamice fac treaba (Mario, nave, rachete).

Creăm un grup static de piese de pământ. Aceste piese sunt plasate de-a lungul liniei. Vă rugăm să rețineți că linia este împărțită în 20 de secțiuni egale (nu 19 așa cum v-ați fi așteptat), iar dale de sol sunt plasate pe fiecare secțiune la capătul din stânga cu centrul de dale situat în acel punct (sper că acest lucru explică cele numere). De asemenea, trebuie să sunăm refresh() pentru a actualiza caseta de limitare a grupului (în caz contrar, coliziunile vor fi verificate în raport cu locația implicită, care este colțul din stânga sus al scenei).

Dacă vă uitați la aplicația dvs. acum în browser, ar trebui să vedeți așa ceva:

1612182849 184 Cum sa construiesti un joc simplu in browser cu Phaser
Evoluția ecranului albastru

Am ajuns în cele din urmă la cea mai dinamică parte a acestei scene – update() funcție, unde cad stelele. Această funcție este numită undeva la 60 ms. Vrem să emitem o nouă stea căzătoare în fiecare secundă. Nu vom folosi un grup dinamic pentru aceasta, deoarece ciclul de viață al fiecărei stele va fi scurt: va fi distrus fie prin clic de utilizator, fie prin ciocnirea cu solul. Prin urmare, în interiorul emitStar() funcția creăm o stea nouă și adăugăm procesarea a două evenimente: onClick() și onCollision().

update(time: number): void {
    var diff: number = time - this.lastStarTime;
    if (diff > this.delta) {
      this.lastStarTime = time;
      if (this.delta > 500) {
        this.delta -= 20;
      }
      this.emitStar();
    }
    this.info.text =
      this.starsCaught + " caught - " +
      this.starsFallen + " fallen (max 3)";
  }
private onClick(star: Phaser.Physics.Arcade.Image): () => void {
    return function () {
      star.setTint(0x00ff00);
      star.setVelocity(0, 0);
      this.starsCaught += 1;
      this.time.delayedCall(100, function (star) {
        star.destroy();
      }, [star], this);
    }
  }
private onFall(star: Phaser.Physics.Arcade.Image): () => void {
    return function () {
      star.setTint(0xff0000);
      this.starsFallen += 1;
      this.time.delayedCall(100, function (star) {
        star.destroy();
      }, [star], this);
    }
  }
private emitStar(): void {
    var star: Phaser.Physics.Arcade.Image;
    var x = Phaser.Math.Between(25, 775);
    var y = 26;
    star = this.physics.add.image(x, y, "star");
star.setDisplaySize(50, 50);
    star.setVelocity(0, 200);
    star.setInteractive();
star.on('pointerdown', this.onClick(star), this);
    this.physics.add.collider(star, this.sand, 
      this.onFall(star), null, this);
  }

În sfârșit, avem un joc! Nu are încă o condiție de câștig. O vom adăuga în ultima parte a tutorialului nostru.

1612182849 340 Cum sa construiesti un joc simplu in browser cu Phaser
Mă pricep la prinderea stelelor …

Înfășurând totul

De obicei, un joc constă din mai multe scene. Chiar dacă jocul este simplu, aveți nevoie de o scenă de deschidere (care să conțină cel puțin butonul „Joacă!”) Și de una de închidere (care să arate rezultatul sesiunii dvs. de joc, cum ar fi scorul sau nivelul maxim atins). Să adăugăm aceste scene la aplicația noastră.

În cazul nostru, vor fi destul de asemănătoare, deoarece nu vreau să acord prea multă atenție designului grafic al jocului. La urma urmei, acesta este un tutorial de programare.

Scena de întâmpinare va avea următorul cod în welcomeScene.ts. Rețineți că atunci când un utilizator face clic undeva pe această scenă, va apărea o scenă de joc.

import "phaser";
export class WelcomeScene extends Phaser.Scene {
  title: Phaser.GameObjects.Text;
  hint: Phaser.GameObjects.Text;
constructor() {
    super({
      key: "WelcomeScene"
    });
  }
create(): void {
    var titleText: string = "Starfall";
    this.title = this.add.text(150, 200, titleText,
      { font: '128px Arial Bold', fill: '#FBFBAC' });
var hintText: string = "Click to start";
    this.hint = this.add.text(300, 350, hintText,
      { font: '24px Arial Bold', fill: '#FBFBAC' });
this.input.on('pointerdown', function (/*pointer*/) {
      this.scene.start("GameScene");
    }, this);
  }
};

Scena de partitura va arăta aproape la fel, ducând la scena de bun venit pe clic (scoreScene.ts).

import "phaser";
export class ScoreScene extends Phaser.Scene {
  score: number;
  result: Phaser.GameObjects.Text;
  hint: Phaser.GameObjects.Text;
constructor() {
    super({
      key: "ScoreScene"
    });
  }
init(params: any): void {
    this.score = params.starsCaught;
  }
create(): void {
    var resultText: string = 'Your score is ' + this.score + '!';
    this.result = this.add.text(200, 250, resultText,
      { font: '48px Arial Bold', fill: '#FBFBAC' });
var hintText: string = "Click to restart";
    this.hint = this.add.text(300, 350, hintText,
      { font: '24px Arial Bold', fill: '#FBFBAC' });
this.input.on('pointerdown', function (/*pointer*/) {
      this.scene.start("WelcomeScene");
    }, this);
  }
};

Trebuie să actualizăm fișierul principal al aplicației noastre acum: adăugați aceste scene și creați WelcomeScene pentru a fi primul din listă:

import "phaser";
import { WelcomeScene } from "./welcomeScene";
import { GameScene } from "./gameScene";
import { ScoreScene } from "./scoreScene";
const config: GameConfig = {
  ...
  scene: [WelcomeScene, GameScene, ScoreScene],
  ...

Ai observat ce lipsește? Corect, nu numim ScoreScene de oriunde încă! Să o numim atunci când jucătorul a ratat a treia stea:

private onFall(star: Phaser.Physics.Arcade.Image): () => void {
    return function () {
      star.setTint(0xff0000);
      this.starsFallen += 1;
      this.time.delayedCall(100, function (star) {
        star.destroy();
        if (this.starsFallen > 2) {
          this.scene.start("ScoreScene", 
            { starsCaught: this.starsCaught });
        }
      }, [star], this);
    }
  }

În cele din urmă, jocul nostru Starfall arată ca un joc real – începe, se termină și chiar are un scop de arhivat (câte stele poți prinde?).

Sper că acest tutorial vă va fi la fel de util ca și pentru mine când l-am scris 🙂 Orice feedback este foarte apreciat!

Codul sursă pentru acest tutorial poate fi găsit Aici.