Vytvoření hry v Three.js

Napsal Lewis Cianci✏️

Před nedávnem bylo jediným způsobem, jak vytvářet a nasazovat hry, vybrat si herní engine jako Unity nebo Unreal, naučit se jazyk a poté hru zabalit a nasadit na platformu, kterou si zvolíte.

Myšlenka pokusit se doručit hru uživateli prostřednictvím jeho prohlížeče by se zdála jako nemožný úkol.

Naštěstí díky pokroku v technologiích prohlížečů a hardwarové akceleraci, která je dostupná ve všech populárních prohlížečích, zlepšení výkonu JavaScriptu a trvalému nárůstu dostupného výpočetního výkonu je vytváření interaktivních herních zážitků pro prohlížeče stále běžnější.

V tomto článku se podíváme na to, jak vytvořit hru pomocí Three.js. Můžete to sledovat zde a také sledovat video tutoriál:

https://youtu.be/2IWjCvTCeNE/?yt-tutorial

Nejprve si ale proberme, co je Three.js a proč je dobrou volbou pro vývoj her.

Co je Three.js?

Popis projektu Three.js na GitHubu výstižně popisuje Three.js jako „…snadno použitelná, lehká, univerzální 3D knihovna pro různé prohlížeče.“

Díky Three.js je pro nás jako vývojáře poměrně jednoduché kreslit 3D objekty a modely na obrazovku. Bez něj bychom potřebovali přímé rozhraní s WebGL, což, i když to není nemožné, může i ten nejmenší projekt vývoje her zabrat neuvěřitelně mnoho času.

Tradičně se „herní engine“ skládá z několika částí. Například Unity a Unreal poskytují způsob, jak vykreslit objekty na obrazovku, ale také řadu dalších funkcí, jako je síť, fyzika a tak dále.

Three.js je však ve svém přístupu omezenější a nezahrnuje věci jako fyziku nebo vytváření sítí. Tento jednodušší přístup však znamená, že je snazší se naučit a lépe optimalizovat to, co umí nejlépe:kreslit objekty na obrazovku.

Má také skvělou sadu vzorků, které můžeme použít k pochopení toho, jak kreslit různé objekty na obrazovku. Konečně poskytuje snadný a nativní způsob, jak načíst naše modely do naší scény.

Three.js může být atraktivní volbou jako engine pro vývoj her, pokud nechcete, aby si vaši uživatelé museli stahovat aplikaci z obchodu s aplikacemi nebo mít nějaké nastavení, aby mohli hrát vaši hru. Pokud vaše hra funguje v prohlížeči, pak máte nejnižší překážku vstupu, což může být jedině dobře.

Použití Three.js k vytvoření naší hry

Dnes se podíváme na prohlídku Three.js a vytvoříme hru, která používá shadery, modely, animace a herní logiku. To, co vytvoříme, bude vypadat takto:

https://youtu.be/XGIThz9m3aQ

Koncept je jednoduchý. Máme pod kontrolou raketovou loď, která se řítí přes planetu, a naším cílem je posbírat energetické krystaly. Potřebujeme také řídit zdraví naší lodi tím, že sbíráme zesílení štítů a snažíme se naši loď příliš nepoškodit nárazy do kamenů ve scéně.

Na konci našeho běhu se raketová loď vrátí k mateřské lodi na obloze a pokud uživatel klikne na DALŠÍ ÚROVEŇ , dostanou další cestu, tentokrát s delší dráhou, kterou může raketa projít.

Jak uživatel hraje, rychlost raketové lodi se zvyšuje, takže musí pracovat rychleji, aby se vyhýbal kamenům a sbíral energetické krystaly.

Abychom vytvořili takovou hru, musíme odpovědět na následující otázky:

  • Jak můžeme raketovou loď přesouvat neustále vpřed přes vodní plochu?
  • Jak můžeme detekovat srážky mezi raketovou lodí a objekty?
  • Jak můžeme vytvořit uživatelské rozhraní, které funguje na stolních i mobilních zařízeních?

V době, kdy jsme vytvořili tuto hru, tyto výzvy překonáme.

Než však začneme kódovat, musíme si zopakovat nějakou krátkou teorii, konkrétně týkající se toho, jak vytvoříme pocit pohybu ve hře.

Vytváření pocitu pohybu

Představte si na okamžik, že v reálném životě řídíte vrtulník a sledujete předmět na zemi. Objekt pokračuje rychlostí, která se postupně zvyšuje. Abyste mohli držet krok, musíte postupně zvyšovat rychlost vrtulníku, ve kterém se nacházíte.

Pokud by neexistovala žádná omezení rychlosti vrtulníku nebo předmětu na zemi, pokračovalo by to tak dlouho, dokud byste chtěli držet krok s předmětem na zemi.

Při vytváření hry, která sleduje objekt, jak to děláme v tomto příkladu, může být lákavé použít stejnou logiku. To znamená, že pohyb objektu ve světovém prostoru, jak se zrychluje, a aktualizace rychlosti kamery, která je za vámi. To však představuje okamžitý problém.

V podstatě každý, kdo hraje tuto hru, ji bude hrát na svých telefonech nebo stolních počítačích. Jedná se o zařízení, která mají omezené zdroje. Pokud se pokusíme generovat možná neomezené množství objektů, když se kamera pohybuje, a poté s kamerou pohneme, nakonec využijeme všechny dostupné zdroje a karta prohlížeče přestane reagovat nebo se zhroutí.

Jsme také povinni vytvořit rovinu (plochý 2D objekt), která představuje oceán. Když to uděláme, musíme zadat rozměry oceánu.

Nemůžeme však vytvořit rovinu, která má nekonečnou velikost, ani nemůžeme vytvořit gigantickou rovinu a jen doufat, že uživatel nikdy nepostoupí přes naši úroveň dostatečně daleko, aby mohl z letadla vystoupit.

To je špatný design a doufat, že lidé nehrají naši hru dostatečně na to, aby zažili chyby, se zdá být neintuitivní.

Nekonečný pohyb v konečných mezích

Místo toho, abychom kamerou pohybovali donekonečna jedním směrem, místo toho ji udržujeme nehybnou a pohybujeme kolem ní prostředím. To má několik výhod.

Jedním z nich je, že vždy víme, kde je naše raketová loď, protože umístění rakety se nepohybuje do dálky; pohybuje se pouze ze strany na stranu. Díky tomu můžeme snadno zjistit, zda se za kamerou nacházejí objekty a lze je odstranit ze scény, aby se uvolnily zdroje.

Další výhodou je, že si můžeme vybrat bod ve vzdálenosti, ve kterém budeme vytvářet objekty. To znamená, že jak se předměty přibližují k hráči, budou se neustále vytvářet nové předměty nebo předměty v dálce mimo zorné pole hráčů.

Když zmizí z dohledu, ať už tím, že se s nimi hráč střetne, nebo když za ním přejdou, jsou tyto předměty odstraněny ze scény, aby se snížilo využití paměti.

K vytvoření tohoto efektu budeme muset udělat dvě věci:Nejprve musíme procedurálně posunout každou položku podél osy hloubky, abychom přesunuli objekty směrem ke kameře. Za druhé, musíme poskytnout naší vodní hladině hodnotu, kterou lze kompenzovat, a tuto odchylku v průběhu času zvyšovat.

To způsobí, že se hladina vody pohybuje stále rychleji.

Nyní, když jsme vyřešili, jak posuneme raketu kupředu scénou, přejděme k nastavení našeho projektu.

Konfigurace herního projektu

Začněme vytvářet naši hru! První věc, kterou musíme udělat, je nastavit naše prostředí pro sestavení. Pro tento příklad jsem zvolil použití Typescript a Webpack. Tento článek není o výhodách těchto technologií, takže se zde o nich nebudu příliš podrobně věnovat, s výjimkou rychlého shrnutí.

Používání Webpacku znamená, že když vyvíjíme náš projekt a jak ukládáme naše soubory, Webpack uvidí, že se naše soubory změnily, a automaticky znovu načte náš prohlížeč s našimi uloženými změnami.

To znamená, že při každé změně nemusíte prohlížeč ručně obnovovat, což ušetří spoustu času. Znamená to také, že můžeme používat pluginy, jako je tři minifikátory, což snižuje velikost našeho balíčku při jeho nasazení.

Použití TypeScript v našem příkladu znamená, že náš projekt bude mít bezpečnost typu. Považuji to za užitečné zejména při práci s některými interními typy Three.js, jako je Vector3 s a Quaternions . Vědět, že proměnné přiřazuji správný typ hodnoty, je velmi cenné.

Pro naše uživatelské rozhraní také použijeme Materialise CSS. U několika tlačítek a karet, které použijeme jako uživatelské rozhraní, tento rámec CSS výrazně pomůže.

Chcete-li začít pracovat na našem projektu, vytvořte novou složku. Ve složce vytvořte package.json a vložte následující obsah:

{
  "dependencies": {
    "materialize-css": "^1.0.0",
    "nipplejs": "^0.9.0",
    "three": "^0.135.0"
  },
  "devDependencies": {
    "@types/three": "^0.135.0",
    "@yushijinhun/three-minifier-webpack": "^0.3.0",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^9.1.0",
    "html-webpack-plugin": "^5.5.0",
    "raw-loader": "^4.0.2",
    "ts-loader": "^9.2.5",
    "typescript": "^4.5.4",
    "webpack": "^5.51.1",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.0.0",
    "webpack-glsl-loader": "git+https://github.com/grieve/webpack-glsl-loader.git",
    "webpack-merge": "^5.8.0"
  },
  "scripts": {
    "dev": "webpack serve --config ./webpack.dev.js",
    "build": "webpack --config ./webpack.production.js"
  }
}

Poté v příkazovém okně zadejte npm i k instalaci balíčků do vašeho nového projektu.

Přidávání souborů Webpack

Nyní potřebujeme vytvořit tři soubory, základní konfigurační soubor Webpack, následovaný vývojovou a produkční konfigurací pro náš projekt.

Vytvořte [webpack.common.js] soubor ve složce projektu a vložte jej do následující konfigurace:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
    plugins: [
        // Automatically creat an index.html with the right bundle name and references to our javascript.
        new HtmlWebpackPlugin({
            template: 'html/index.html'
        }),
        // Copy game assets from our static directory, to the webpack output
        new CopyPlugin({
            patterns: [
                {from: 'static', to: 'static'}
            ]
        }),
    ],
    // Entrypoint for our game
    entry: './game.ts',
    module: {
        rules: [
            {
                // Load our GLSL shaders in as text
                test: /.(glsl|vs|fs|vert|frag)$/, exclude: /node_modules/, use: ['raw-loader']
            },
            {
                // Process our typescript and use ts-loader to transpile it to Javascript
                test: /.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/,
            }

        ],
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },

}

Poté vytvořte [webpack.dev.js] soubor a vložte tyto podrobnosti. Tím se nakonfiguruje funkce rychlého načtení vývojového serveru Webpack:

const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const path = require('path');
module.exports = merge(common, {
    mode: 'development', // Don't minify the source
    devtool: 'eval-source-map', // Source map for easier development
    devServer: {
        static: {
            directory: path.join(__dirname, './dist'), // Serve static files from here
        },
        hot: true, // Reload our page when the code changes
    },
})

Nakonec vytvořte [webpack.production.js] soubor a vložte tyto podrobnosti:

const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const path = require('path');
const ThreeMinifierPlugin = require("@yushijinhun/three-minifier-webpack");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const threeMinifier = new ThreeMinifierPlugin();

module.exports = merge(common, {
    plugins: [
        threeMinifier, // Minifies our three.js code
        new CleanWebpackPlugin() // Cleans our 'dist' folder between builds
    ],
    resolve: {
        plugins: [
            threeMinifier.resolver,
        ]
    },
    mode: 'production', // Minify our output
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[fullhash:8].js', // Our output will have a unique hash, which will force our clients to download updates if they become available later
        sourceMapFilename: '[name].[fullhash:8].map',
        chunkFilename: '[id].[fullhash:8].js'
    },
    optimization: {
        splitChunks: {
            chunks: 'all', // Split our code into smaller chunks to assist caching for our clients
        },
    },
})

Konfigurace prostředí TypeScript

Další věc, kterou musíme udělat, je nakonfigurovat prostředí TypeScript tak, aby nám umožnilo používat importy ze souborů JavaScript. Chcete-li to provést, vytvořte [tsconfig.json] soubor a vložte následující podrobnosti:

{
    "compilerOptions": {
        "moduleResolution": "node",
        "strict": true,
        "allowJs": true,
        "checkJs": false,
        "target": "es2017",
      "module": "commonjs"

    },
    "include": ["**/*.ts"]
}

Naše prostředí pro sestavení je nyní nakonfigurováno. Nyní je čas pustit se do práce na vytvoření krásné a uvěřitelné scény, kterou mohou naši hráči procházet.

Nastavení herní scény

Naše scéna se skládá z následujících prvků:

  1. Scéna samotná (to je to, k čemu přidáváme naše objekty, abychom vytvořili herní svět)
  2. Obloha
  3. Voda
  4. Objekty na pozadí (kameny, které leží na obou stranách herní plochy uživatele)
  5. Raketová loď
  6. Řádky, které obsahují krystaly, kameny a předměty štítu (označované jako „řádky výzev“)

Většinu naší práce budeme provádět v souboru s názvem game.ts , ale také rozdělíme části naší hry do samostatných souborů, abychom neskončili s neuvěřitelně dlouhým souborem. Můžeme pokračovat a vytvořit game.ts soubor nyní.

Protože se zabýváme poměrně složitým tématem, uvedu také odkazy, kde se tento kód v rámci projektu nachází na GitHubu. To by vám snad mělo pomoci udržet se v orientaci a neztratit se ve větším projektu.

Vytvoření Scene

První věc, kterou musíme udělat, je vytvořit Scene takže Three.js má co vykreslovat. V rámci našeho game.ts , přidáme následující řádky, abychom vytvořili náš Scene a umístěte PerspectiveCamera ve scéně, abychom viděli, co se děje.

Nakonec vytvoříme referenci pro náš renderer, který přiřadíme později:

export const scene = new Scene()
export const camera = new PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    2000
)

// Our three renderer
let renderer: WebGLRenderer;

Vytvoření inicializační funkce

Abychom připravili naši scénu, musíme provést některé úkoly, jako je vytvoření nového WebGLRenderer a nastavení velikosti plátna, na které chceme kreslit.

Chcete-li to provést, vytvořte init a umístěte ji do našeho game.ts také. Toto init funkce provede úvodní nastavení pro naši scénu a spustí se pouze jednou (při prvním načtení hry):

/// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L275-L279)
async function init() {
    renderer = new WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
}

Pro naši scénu budeme také muset využít vykreslovací a animační smyčku. Budeme potřebovat smyčku animace, abychom mohli přesouvat objekty na obrazovce, jak potřebujeme, a budeme potřebovat smyčku vykreslování, abychom mohli na obrazovku vykreslit nové snímky.

Pojďme do toho a vytvořte render nyní funguje v našem game.ts . Na začátku bude tato funkce vypadat docela holá, protože jednoduše vyžaduje snímek animace a poté vykresluje scénu.

Existuje několik důvodů, proč požadujeme snímek animace, ale jedním z hlavních je, že naše hra se pozastaví, pokud uživatel změní karty, což zlepší výkon a sníží možné plýtvání zdroji na zařízení:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L157)
const animate = () => {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}

Takže teď máme prázdnou scénu s kamerou, ale nic jiného. Přidejme trochu vody do naší scény.

Vytvoření vody pro Scene

Naštěstí Three.js obsahuje příklad vodního objektu, který můžeme použít v naší scéně. Obsahuje odrazy v reálném čase a vypadá docela dobře; můžete se na to podívat zde.

Naštěstí pro nás tato voda splní většinu toho, co chceme v rámci naší scény dělat. Jediné, co musíme udělat, je mírně změnit shader pro vodu, abychom jej mohli aktualizovat z naší vykreslovací smyčky.

Děláme to proto, že pokud budeme s postupem času kompenzovat texturu vody stále více, pak nám to dá pocit rychlosti.

Abych to demonstroval, toto je úvodní scéna naší hry, ale každý snímek zvyšuji offset. Jak se posun zvyšuje, zdá se, že rychlost oceánu pod námi roste (i když raketa ve skutečnosti stojí).

Vodní objekt lze nalézt zde na GitHubu Three.js. Jediné, co budeme muset udělat, je provést malou změnu, aby bylo možné tento offset ovládat z naší vykreslovací smyčky (abychom jej mohli časem aktualizovat).

První věc, kterou uděláme, je získat kopii ukázky Water.js v úložišti Three.js. Tento soubor umístíme v rámci našeho projektu na objects/water.js . Pokud otevřeme water.js přibližně v polovině, začneme vidět něco, co vypadá takto:

Toto jsou shadery pro náš oceánský materiál. Samotné shadery jsou mimo rozsah tohoto článku, ale v zásadě jsou to pokyny, které naše hra poskytne počítačům našich uživatelů, jak nakreslit tento konkrétní objekt.

Máme zde také náš shader kód, který je napsán v OpenGraph Shader Language (GLSL), začleněný do souboru, který je jinak JavaScript.

Na tom není nic špatného, ​​ale pokud přesuneme tento kód shaderu do souboru samostatně, můžeme nainstalovat podporu GLSL do našeho zvoleného IDE a získáme věci, jako je barvení syntaxe a ověřování, což nám pomáhá přizpůsobit naši GLSL .

Chcete-li rozdělit GLSL na samostatné soubory, vytvořte shader adresář v našem aktuálním objects adresáře, vyberte obsah našeho vertexShader a naše fragmentShader a přesuňte je do waterFragmentShader.glsl a waterVertexShader.glsl soubory.

V horní části našeho [waterFragmentShader.glsl] soubor, máme getNoise funkce. Ve výchozím nastavení to vypadá takto:

vec4 getNoise( vec2 uv ) {
  vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0);
  vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 );
  vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 );
  vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 );
  vec4 noise = texture2D( normalSampler, uv0 ) +
   texture2D( normalSampler, uv1 ) +
   texture2D( normalSampler, uv2 ) +
   texture2D( normalSampler, uv3 );
  return noise * 0.5 - 1.0;
}

Aby byl tento offset nastavitelný z našeho kódu hry, chceme do našeho souboru GLSL přidat parametr, který nám umožní jej během provádění upravovat. K tomu musíme tuto funkci nahradit následující funkcí:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/objects/shaders/waterFragmentShader.glsl#L15-L32)

uniform float speed;

vec4 getNoise(vec2 uv) {
    float offset;
    if (speed == 0.0){
        offset = time / 10.0;
    }
    else {
        offset = speed;
    }
    vec2 uv3 = uv / vec2(50.0, 50.0) - vec2(speed / 1000.0, offset);
    vec2 uv0 = vec2(0, 0);
    vec2 uv1 = vec2(0, 0);
    vec2 uv2 = vec2(0, 0);
    vec4 noise = texture2D(normalSampler, uv0) +
    texture2D(normalSampler, uv1) +
    texture2D(normalSampler, uv2) +
    texture2D(normalSampler, uv3);
    return noise * 0.5 - 1.0;
}

Všimněte si, že jsme do tohoto souboru GLSL zahrnuli novou proměnnou:speed variabilní. Toto je proměnná, kterou aktualizujeme, abychom získali pocit rychlosti.

V rámci našeho game.ts , nyní musíme nakonfigurovat nastavení vody. Nahoře v našem souboru přidejte následující proměnné:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L81-L98)

const waterGeometry = new PlaneGeometry(10000, 10000);

const water = new Water(
    waterGeometry,
    {
        textureWidth: 512,
        textureHeight: 512,
        waterNormals: new TextureLoader().load('static/normals/waternormals.jpeg', function (texture) {
            texture.wrapS = texture.wrapT = MirroredRepeatWrapping;
        }),
        sunDirection: new Vector3(),
        sunColor: 0xffffff,
        waterColor: 0x001e0f,
        distortionScale: 3.7,
        fog: scene.fog !== undefined
    }
);

Poté v rámci našeho init funkce, musíme nakonfigurovat rotaci a polohu naší vodní roviny takto:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L364-L368)
// Water
water.rotation.x = -Math.PI / 2;
water.rotation.z = 0;
scene.add(water);

To zajistí správnou rotaci pro oceán.

Vytváření oblohy

Three.js přichází s poměrně přesvědčivou oblohou, kterou můžeme v rámci našeho projektu používat zdarma. Příklad toho můžete vidět na ukázkové stránce Three.js zde.

Je docela snadné přidat do našeho projektu oblohu; jednoduše potřebujeme přidat oblohu do scény, nastavit velikost skyboxu a poté nastavit některé parametry, které řídí, jak naše obloha vypadá.

V rámci našeho init funkce, kterou jsme deklarovali, přidáme oblohu do naší scény a nakonfigurujeme vizuály pro oblohu:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L369-L398)
const sky = new Sky();
sky.scale.setScalar(10000); // Specify the dimensions of the skybox
scene.add(sky); // Add the sky to our scene

// Set up variables to control the look of the sky
const skyUniforms = sky.material.uniforms;
skyUniforms['turbidity'].value = 10;
skyUniforms['rayleigh'].value = 2;
skyUniforms['mieCoefficient'].value = 0.005;
skyUniforms['mieDirectionalG'].value = 0.8;

const parameters = {
    elevation: 3,
    azimuth: 115
};

const pmremGenerator = new PMREMGenerator(renderer);

const phi = MathUtils.degToRad(90 - parameters.elevation);
const theta = MathUtils.degToRad(parameters.azimuth);

sun.setFromSphericalCoords(1, phi, theta);

sky.material.uniforms['sunPosition'].value.copy(sun);
(water.material as ShaderMaterial).uniforms['sunDirection'].value.copy(sun).normalize();
scene.environment = pmremGenerator.fromScene(sky as any).texture;

(water.material as ShaderMaterial).uniforms['speed'].value = 0.0;

Finální Scene příprava

Poslední věc, kterou musíme udělat s naší počáteční inicializací scény, je přidat nějaké osvětlení a přidat náš model rakety a model naší mateřské lodi:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L410-L420)
// Set the appropriate scale for our rocket
rocketModel.scale.set(0.3, 0.3, 0.3);
scene.add(rocketModel);
scene.add(mothershipModel);

// Set the scale and location for our mothership (above the player)
mothershipModel.position.y = 200;
mothershipModel.position.z = 100;
mothershipModel.scale.set(15,15,15);
sceneConfiguration.ready = true;

Nyní máme naši scénu s pěkně vypadající vodou a raketou. Chybí nám ale cokoliv, co by z toho mohlo udělat hru. Abychom to vyřešili, musíme sestavit některé základní parametry pro ovládání hry a umožnění hráči pohybovat se směrem k určitým cílům.

V horní části našeho game.ts soubor, přidáme následující sceneConfiguration proměnná, která nám pomáhá sledovat objekty v naší scéně:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L110-L143)
export const sceneConfiguration = {
    /// Whether the scene is ready (i.e.: All models have been loaded and can be used)
    ready: false,
    /// Whether the camera is moving from the beginning circular pattern to behind the ship
    cameraMovingToStartPosition: false,
    /// Whether the rocket is moving forward
    rocketMoving: false,
    // backgroundMoving: false,
    /// Collected game data
    data: {
        /// How many crystals the player has collected on this run
        crystalsCollected: 0,
        /// How many shields the player has collected on this run (can be as low as -5 if player hits rocks)
        shieldsCollected: 0,
    },
    /// The length of the current level, increases as levels go up
    courseLength: 500,
    /// How far the player is through the current level, initialises to zero.
    courseProgress: 0,
    /// Whether the level has finished
    levelOver: false,
    /// The current level, initialises to one.
    level: 1,
    /// Gives the completion amount of the course thus far, from 0.0 to 1.0.
    coursePercentComplete: () => (sceneConfiguration.courseProgress / sceneConfiguration.courseLength),
    /// Whether the start animation is playing (the circular camera movement while looking at the ship)
    cameraStartAnimationPlaying: false,
    /// How many 'background bits' are in the scene (the cliffs)
    backgroundBitCount: 0,
    /// How many 'challenge rows' are in the scene (the rows that have rocks, shields, or crystals in them).
    challengeRowCount: 0,
    /// The current speed of the ship
    speed: 0.0
}

Nyní musíme provést inicializaci pro aktuální úroveň, na které se hráč nachází. Tato funkce nastavení scény je důležitá, protože bude vyvolána pokaždé, když uživatel začne novou úroveň.

Takže musíme nastavit umístění naší rakety zpět na začátek a vyčistit všechna stará aktiva, která se používala. Vložil jsem několik komentářů, abyste viděli, co každý řádek dělá:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L519-L591)
export const sceneSetup = (level: number) => {
    // Remove all references to old "challenge rows" and background bits
    sceneConfiguration.challengeRowCount = 0;
    sceneConfiguration.backgroundBitCount = 0;

    // Reset the camera position back to slightly infront of the ship, for the start-up animation
    camera.position.z = 50;
    camera.position.y = 12;
    camera.position.x = 15;
    camera.rotation.y = 2.5;

    // Add the starter bay to the scene (the sandy shore with the rocks around it)
    scene.add(starterBay);

    // Set the starter bay position to be close to the ship
    starterBay.position.copy(new Vector3(10, 0, 120));

    // Rotate the rocket model back to the correct orientation to play the level
    rocketModel.rotation.x = Math.PI;
    rocketModel.rotation.z = Math.PI;

    // Set the location of the rocket model to be within the starter bay
    rocketModel.position.z = 70;
    rocketModel.position.y = 10;
    rocketModel.position.x = 0;

    // Remove any existing challenge rows from the scene
    challengeRows.forEach(x => {
        scene.remove(x.rowParent);
    });

    // Remove any existing environment bits from the scene
    environmentBits.forEach(x => {
        scene.remove(x);
    })

    // Setting the length of these arrays to zero clears the array of any values
    environmentBits.length = 0;
    challengeRows.length = 0;

    // Render some challenge rows and background bits into the distance
    for (let i = 0; i < 60; i++) {
        // debugger;
        addChallengeRow(sceneConfiguration.challengeRowCount++);
        addBackgroundBit(sceneConfiguration.backgroundBitCount++);
    }

    //Set the variables back to their beginning state

    // Indicates that the animation where the camera flies from the current position isn't playing
    sceneConfiguration.cameraStartAnimationPlaying = false;
    // The level isn't over (we just started it)
    sceneConfiguration.levelOver = false;
    // The rocket isn't flying away back to the mothership
    rocketModel.userData.flyingAway = false;
    // Resets the current progress of the course to 0, as we haven't yet started the level we're on
    sceneConfiguration.courseProgress = 0;
    // Sets the length of the course based on our current level
    sceneConfiguration.courseLength = 1000 * level;

    // Reset how many things we've collected in this level to zero
    sceneConfiguration.data.shieldsCollected = 0;
    sceneConfiguration.data.crystalsCollected = 0;

    // Updates the UI to show how many things we've collected to zero.
    crystalUiElement.innerText = String(sceneConfiguration.data.crystalsCollected);
    shieldUiElement.innerText = String(sceneConfiguration.data.shieldsCollected);

    // Sets the current level ID in the UI
    document.getElementById('levelIndicator')!.innerText = `LEVEL ${sceneConfiguration.level}`;
    // Indicates that the scene setup has completed, and the scene is now ready
    sceneConfiguration.ready = true;
}

Přidání logiky hry

Očekáváme, že naši hru budou hrát dva typy zařízení:stolní počítače a mobilní telefony. Za tímto účelem musíme přizpůsobit dva typy vstupních možností:

  • Klávesnice (konkrétně levá a pravá klávesa na klávesnici)
  • Dotykové obrazovky (zobrazením joysticku na obrazovce můžete s plavidlem manévrovat zleva doprava)

Pojďme je nyní nakonfigurovat.

Vstup z klávesnice

V horní části našeho game.ts , přidáme následující proměnné, abychom mohli sledovat, zda byla na klávesnici stisknuta levá nebo pravá klávesa:

let leftPressed = false;
let rightPressed = false;

Poté v rámci našeho init funkce, zaregistrujeme keydown a keyup události pro volání onKeyDown a onKeyUp funkce, respektive:

document.addEventListener('keydown', onKeyDown, false);
document.addEventListener('keyup', onKeyUp, false);

Nakonec pro vstup z klávesnice zaregistrujeme, co dělat, když jsou stisknuty tyto klávesy:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L500-L517)
function onKeyDown(event: KeyboardEvent) {
    console.log('keypress');
    let keyCode = event.which;
    if (keyCode == 37) { // Left arrow key
        leftPressed = true;
    } else if (keyCode == 39) { // Right arrow key
        rightPressed = true;
    }
}

function onKeyUp(event: KeyboardEvent) {
    let keyCode = event.which;
    if (keyCode == 37) { // Left arrow key
        leftPressed = false;
    } else if (keyCode == 39) { // Right arrow key
        rightPressed = false;
    }
}

Vstup na dotykové obrazovce

Naši mobilní uživatelé nebudou mít klávesnici, na kterou by mohli zadávat svůj vstup, takže použijeme nippleJS k vytvoření joysticku na obrazovce a výstup z joysticku použijeme k ovlivnění pozice rakety na obrazovce.

V rámci našeho init zkontrolujeme, zda je zařízení dotykové, a to tak, že zkontrolujeme, zda má na obrazovce nenulový počet dotykových bodů. Pokud ano, vytvoříme joystick, ale také nastavíme pohyb rakety zpět na nulu, jakmile hráč uvolní ovládání joysticku:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L280-L296)
if (isTouchDevice()) {
    // Get the area within the UI to use as our joystick
    let touchZone = document.getElementById('joystick-zone');

    if (touchZone != null) {
        // Create a Joystick Manager
        joystickManager = joystick.create({zone: document.getElementById('joystick-zone')!,})
        // Register what to do when the joystick moves
        joystickManager.on("move", (event, data) => {
            positionOffset = data.vector.x;
        })
        // When the joystick isn't being interacted with anymore, stop moving the rocket
        joystickManager.on('end', (event, data) => {
            positionOffset = 0.0;
        })
    }
}

V rámci našeho animate funkce, sledujeme, co dělat, pokud je v daný okamžik stisknuto levé nebo pravé tlačítko nebo pokud je používán joystick. Pozici rakety také upneme na přijatelnou levou a pravou pozici, takže se raketa nemůže zcela pohybovat mimo obrazovku:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L159-L170)
// If the left arrow is pressed, move the rocket to the left
if (leftPressed) {
    rocketModel.position.x -= 0.5;
}
// If the right arrow is pressed, move the rocket to the right
if (rightPressed) {
    rocketModel.position.x += 0.5;
}
// If the joystick is in use, update the current location of the rocket accordingly
rocketModel.position.x += positionOffset;
// Clamp the final position of the rocket to an allowable region
rocketModel.position.x = clamp(rocketModel.position.x, -20, 25);

Pohyb objektů v naší scéně

Jak jsme již probrali, raketová loď zůstává v naší scéně nehybná a objekty se pohybují směrem k ní. Rychlost pohybu těchto objektů se postupně zvyšuje, jak uživatel pokračuje ve hře, což v průběhu času zvyšuje obtížnost úrovně.

Stále v rámci naší animační smyčky chceme tyto objekty postupně přesouvat směrem k přehrávači. Když objekty opustí pohled hráče, chceme je ze scény odstranit, abychom nezabírali zbytečné zdroje v počítači hráče.

V rámci naší vykreslovací smyčky můžeme tuto funkci nastavit takto:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L215-L252)
if (sceneConfiguration.rocketMoving) {
    // Detect if the rocket ship has collided with any of the objects within the scene
    detectCollisions();

    // Move the rocks towards the player
    for (let i = 0; i < environmentBits.length; i++) {
        let mesh = environmentBits[i];
        mesh.position.z += sceneConfiguration.speed;
    }

    // Move the challenge rows towards the player
    for (let i = 0; i < challengeRows.length; i++) {
        challengeRows[i].rowParent.position.z += sceneConfiguration.speed;
        // challengeRows[i].rowObjects.forEach(x => {
        //     x.position.z += speed;
        // })
    }

    // If the furtherest rock is less than a certain distance, create a new one on the horizon
    if ((!environmentBits.length || environmentBits[0].position.z > -1300) && !sceneConfiguration.levelOver) {
        addBackgroundBit(sceneConfiguration.backgroundBitCount++, true);
    }

    // If the furtherest challenge row is less than a certain distance, create a new one on the horizon
    if ((!challengeRows.length || challengeRows[0].rowParent.position.z > -1300) && !sceneConfiguration.levelOver) {
        addChallengeRow(sceneConfiguration.challengeRowCount++, true);
    }

    // If the starter bay hasn't already been removed from the scene, move it towards the player
    if (starterBay != null) {
        starterBay.position.z += sceneConfiguration.speed;
    }

    // If the starter bay is outside of the players' field of view, remove it from the scene
    if (starterBay.position.z > 200) {
        scene.remove(starterBay);
    }

Vidíme, že součástí tohoto volání je několik funkcí:

  • detectCollisions
  • addBackgroundBit
  • addChallengeRow

Pojďme prozkoumat, čeho tyto funkce v naší hře dosahují.

detectCollisions

Detekce kolizí je důležitou cestou naší hry. Bez toho nebudeme vědět, jestli naše raketová loď zasáhla některý z cílů nebo jestli narazila na kámen a měla by zpomalit. To je důvod, proč chceme v naší hře používat detekci kolizí.

Normálně bychom mohli použít fyzikální engine k detekci kolizí mezi objekty v naší scéně, ale Three.js žádný fyzikální engine nemá.

To však neznamená, že fyzikální motory pro Three.js neexistují. Určitě ano, ale pro naše potřeby nepotřebujeme přidávat fyzikální engine, abychom zkontrolovali, zda naše raketa nezasáhla jiný objekt.

V podstatě chceme odpovědět na otázku:„Protíná se můj model rakety v současnosti s nějakými jinými modely na obrazovce? Musíme také reagovat určitými způsoby v závislosti na tom, co bylo zasaženo.

Pokud například náš hráč neustále naráží raketou do kamenů, musíme úroveň ukončit, jakmile utrpíme určité poškození.

Abychom toho dosáhli, vytvořme funkci, která kontroluje průsečík naší rakety a objektů ve scéně. Podle toho, co hráč zasáhl, podle toho zareagujeme.

Tento kód vložíme do našeho game adresář v souboru s názvem collisionDetection.ts :

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game/collisionDetection.ts#L18-L87) 

export const detectCollisions = () => {
    // If the level is over, don't detect collisions
    if (sceneConfiguration.levelOver) return;
    // Using the dimensions of our rocket, create a box that is the width and height of our model
    // This box doesn't appear in the world, it's merely a set of coordinates that describe the box
    // in world space.
    const rocketBox = new Box3().setFromObject(rocketModel);
    // For every challange row that we have on the screen...
    challengeRows.forEach(x => {
        // ...update the global position matrix of the row, and its children.
        x.rowParent.updateMatrixWorld();
        // Next, for each object within each challenge row...
        x.rowParent.children.forEach(y => {
            y.children.forEach(z => {
                // ...create a box that is the width and height of the object
                const box = new Box3().setFromObject(z);
                // Check if the box with the obstacle overlaps (or intersects with) our rocket
                if (box.intersectsBox(rocketBox)) {
                    // If it does, get the center position of that box
                    let destructionPosition = box.getCenter(z.position);
                    // Queue up the destruction animation to play (the boxes flying out from the rocket)
                    playDestructionAnimation(destructionPosition);
                    // Remove the object that has been hit from the parent
                    // This removes the object from the scene
                    y.remove(z);
                    // Now, we check what it was that we hit, whether it was a rock, shield, or crystal
                    if (y.userData.objectType !== undefined) {
                        let type = y.userData.objectType as ObjectType;
                        switch (type) {
                            // If it was a rock...
                            case ObjectType.ROCK:
                                // ...remove one shield from the players' score
                                sceneConfiguration.data.shieldsCollected--;
                                // Update the UI with the new count of shields
                                shieldUiElement.innerText = String(sceneConfiguration.data.shieldsCollected);
                                // If the player has less than 0 shields...
                                if (sceneConfiguration.data.shieldsCollected <= 0) {
                                    // ...add the 'danger' CSS class to make the text red (if it's not already there)
                                    if (!shieldUiElement.classList.contains('danger')) {
                                        shieldUiElement.classList.add('danger');
                                    }
                                } else { //Otherwise, if it's more than 0 shields, remove the danger CSS class
                                    // so the text goes back to being white
                                    shieldUiElement.classList.remove('danger');
                                }

                                // If the ship has sustained too much damage, and has less than -5 shields...
                                if (sceneConfiguration.data.shieldsCollected <= -5) {
                                    // ...end the scene
                                    endLevel(true);
                                }
                                break;
                            // If it's a crystal...
                            case ObjectType.CRYSTAL:
                                // Update the UI with the new count of crystals, and increment the count of
                                // currently collected crystals
                                crystalUiElement.innerText = String(++sceneConfiguration.data.crystalsCollected);
                                break;
                            // If it's a shield...
                            case ObjectType.SHIELD_ITEM:
                                // Update the UI with the new count of shields, and increment the count of
                                // currently collected shields
                                shieldUiElement.innerText = String(++sceneConfiguration.data.shieldsCollected);
                                break;
                        }
                    }
                }
            });
        })
    });
}

Jediná další věc, kterou musíme udělat pro naši detekci kolize, je přidat krátkou animaci, která se přehraje, když se uživatel srazí s objektem. Tato funkce vezme místo, kde došlo ke kolizi, a vytvoří několik polí z tohoto výchozího bodu.

Hotový výsledek bude vypadat takto.

Abychom toho dosáhli, musíme vytvořit rámečky v kruhu kolem místa, kde ke srážce dojde, a animovat je směrem ven, aby to vypadalo, že explodují ze srážky. Chcete-li to provést, přidejte tuto funkci do našeho collisionDetection.ts soubor:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game/collisionDetection.ts#L89-L146)
const playDestructionAnimation = (spawnPosition: Vector3) => {

    // Create six boxes
    for (let i = 0; i < 6; i++) {
        // Our destruction 'bits' will be black, but have some transparency to them
        let destructionBit = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial({
            color: 'black',
            transparent: true,
            opacity: 0.4
        }));

        // Each destruction bit object within the scene will have a 'lifetime' property associated to it
        // This property is incremented every time a frame is drawn to the screen
        // Within our animate loop, we check if this is more than 500, and if it is, we remove the object
        destructionBit.userData.lifetime = 0;
        // Set the spawn position of the box
        destructionBit.position.set(spawnPosition.x, spawnPosition.y, spawnPosition.z);
        // Create an animation mixer for the object
        destructionBit.userData.mixer = new AnimationMixer(destructionBit);

        // Spawn the objects in a circle around the rocket
        let degrees = i / 45;

        // Work out where on the circle we should spawn this specific destruction bit
        let spawnX = Math.cos(radToDeg(degrees)) * 15;
        let spawnY = Math.sin(radToDeg(degrees)) * 15;

        // Create a VectorKeyFrameTrack that will animate this box from its starting position to the final
        // 'outward' position (so it looks like the boxes are exploding from the ship)
        let track = new VectorKeyframeTrack('.position', [0, 0.3], [
            rocketModel.position.x, // x 1
            rocketModel.position.y, // y 1
            rocketModel.position.z, // z 1
            rocketModel.position.x + spawnX, // x 2
            rocketModel.position.y, // y 2
            rocketModel.position.z + spawnY, // z 2
        ]);

        // Create an animation clip with our VectorKeyFrameTrack
        const animationClip = new AnimationClip('animateIn', 10, [track]);
        const animationAction = destructionBit.userData.mixer.clipAction(animationClip);

        // Only play the animation once
        animationAction.setLoop(LoopOnce, 1);

        // When complete, leave the objects in their final position (don't reset them to the starting position)
        animationAction.clampWhenFinished = true;
        // Play the animation
        animationAction.play();
        // Associate a Clock to the destruction bit. We use this within the render loop so ThreeJS knows how far
        // to move this object for this frame
        destructionBit.userData.clock = new Clock();
        // Add the destruction bit to the scene
        scene.add(destructionBit);

        // Add the destruction bit to an array, to keep track of them
        destructionBits.push(destructionBit);
    }

A to je naše detekce kolizí vyřešena, doplněná pěknou animací, když je objekt zničen.

addBackgroundBit

Jak naše scéna postupuje, chceme přidat nějaké útesy na obě strany hráče, aby se zdálo, že jejich pohyb je v určitém prostoru vhodně omezen. Operátor modulo používáme k procedurálnímu přidávání kamenů napravo nebo nalevo od uživatele:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game/objects.ts#L43-L60)
export const addBackgroundBit = (count: number, horizonSpawn: boolean = false) => {
    // If we're spawning on the horizon, always spawn at a position far away from the player
    // Otherwise, place the rocks at certain intervals into the distance-
    let zOffset = (horizonSpawn ? -1400 : -(60 * count));
    // Create a copy of our original rock model
    let thisRock = cliffsModel.clone();
    // Set the scale appropriately for the scene
    thisRock.scale.set(0.02, 0.02, 0.02);
    // If the row that we're adding is divisble by two, place the rock to the left of the user
    // otherwise, place it to the right of the user.
    thisRock.position.set(count % 2 == 0 ? 60 - Math.random() : -60 - Math.random(), 0, zOffset);
    // Rotate the rock to a better angle
    thisRock.rotation.set(MathUtils.degToRad(-90), 0, Math.random());
    // Finally, add the rock to the scene
    scene.add(thisRock);
    // Add the rock to the beginning of the environmentBits array to keep track of them (so we can clean up later)
    environmentBits.unshift(thisRock);// add to beginning of array
}

addChallengeRow

Jak se naše scéna vyvíjí, chceme do scény také přidat naše „řady výzev“. Jedná se o předměty, které obsahují kameny, krystaly nebo předměty štítu. Pokaždé, když je vytvořena jedna z těchto nových řad, náhodně přiřadíme ke každé řadě kameny, krystaly a štíty.

Takže ve výše uvedeném příkladu k buňkám 1, 2 a 4 není nic přidáno, zatímco k buňkám 3 a 5 je přidán krystal a položka štítu.

Abychom toho dosáhli, považujeme tyto řádky výzev za rozdělené do pěti různých buněk. V každé buňce vytvoříme určitou položku v závislosti na výstupu naší náhodné funkce, například:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game/objects.ts#L62-L92)
export const addChallengeRow = (count: number, horizonSpawn: boolean = false) => {
    // Work out how far away this challenge row should be
    let zOffset = (horizonSpawn ? -1400 : -(count * 60));
    // Create a Group for the objects. This will be the parent for these objects.
    let rowGroup = new Group();
    rowGroup.position.z = zOffset;
    for (let i = 0; i < 5; i++) {
        // Calculate a random number between 1 and 10
        const random = Math.random() * 10;
        // If it's less than 2, create a crystal
        if (random < 2) {
            let crystal = addCrystal(i);
            rowGroup.add(crystal);
        }
        // If it's less than 4, spawn a rock
        else if (random < 4) {
            let rock = addRock(i);
            rowGroup.add(rock);
        }
       // but if it's more than 9, spawn a shield
        else if (random > 9) {
            let shield = addShield(i);
            rowGroup.add(shield);
        }
    }
    // Add the row to the challengeRows array to keep track of it, and so we can clean them up later
    challengeRows.unshift({rowParent: rowGroup, index: sceneConfiguration.challengeRowCount++});
    // Finally add the row to the scene
    scene.add(rowGroup);
}

Funkci tvorby skály, krystalu a štítu lze zobrazit na kterémkoli z těchto odkazů.

Poslední doplňky k naší vykreslovací smyčce

Poslední věci, které musíme v rámci naší vykreslovací smyčky dokončit, jsou následující:

  • Přesuňte trosky ze shromážděných předmětů směrem k lodi
  • Pokud uživatel dokončí úroveň, zobrazte animaci „odlétání“ a shrnutí úrovně
  • Pokud raketa „odlétá“, nastavte kameru tak, aby se dívala na raketu, aby uživatel viděl, jak letí k mateřské lodi.

Ke konci naší vykreslovací funkce můžeme přidat následující kód, aby se tato funkce přizpůsobila:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L254-L270)

// Call the function to relocate the current bits on the screen and move them towards the rocket
// so it looks like the rocket is collecting them
moveCollectedBits();
// If the rockets progress equals the length of the course...
if (sceneConfiguration.courseProgress >= sceneConfiguration.courseLength) {
    // ...check that we haven't already started the level-end process
    if (!rocketModel.userData.flyingAway) {
        // ...and end the level
        endLevel(false);
    }
}
// If the level end-scene is playing...
if (rocketModel.userData.flyingAway) {
    // Rotate the camera to look at the rocket on it's return journey to the mothership
    camera.lookAt(rocketModel.position);
}

A tím je naše vykreslovací smyčka dokončena.

Vytvoření uživatelského rozhraní hry

Když lidé načtou naši hru, uvidí některá tlačítka, která jim umožní začít hrát.

Jsou to jen jednoduché HTML prvky, které programově zobrazujeme nebo skrýváme podle toho, co se ve hře děje. Ikona otázky dává hráči určitou představu o tom, o čem hra je, a obsahuje pokyny, jak hru hrát. Zahrnuje také (velmi důležité!) licence pro naše modely.

A stisknutím červeného tlačítka spustíte hru. Všimněte si, že když stiskneme červené tlačítko Play, kamera se pohne a otočí za raketu, čímž připraví hráče na začátek scény.

V rámci naší scény init zaregistrujeme událost, abychom to provedli do onClick ovladač tohoto tlačítka. Chcete-li vytvořit funkce rotace a pohybu, musíme provést následující:

  1. Získejte aktuální polohu a natočení kamery
  2. Získejte budoucí polohu a natočení tam, kde chceme kameru umístit
  3. Vytvořte KeyframeTrack zvládnout pohyby a rotace z obou herních pozic
  4. Přiřaďte tyto stopy k mixážnímu pultu a začněte je přehrávat

Za tímto účelem přidáme do našeho init následující kód funkce, třeba takto:

// Can be viewed [here](https://github.com/flutterfromscratch/threejs-rocket-game/blob/master/game.ts#L305-L421)

startGameButton.onclick = (event) => {
    // Indicate that the animation from the camera starting position to the rocket location is running
    sceneConfiguration.cameraStartAnimationPlaying = true;
    // Remove the red text on the shield item, if it existed from the last level
    shieldUiElement.classList.remove('danger');
    // Show the heads up display (that shows crystals collected, etc)
    document.getElementById('headsUpDisplay')!.classList.remove('hidden');

    // Create an animation mixer on the rocket model
    camera.userData.mixer = new AnimationMixer(camera);
    // Create an animation from the cameras' current position to behind the rocket
    let track = new VectorKeyframeTrack('.position', [0, 2], [
        camera.position.x, // x 1
        camera.position.y, // y 1
        camera.position.z, // z 1
        0, // x 2
        30, // y 2
        100, // z 2
    ], InterpolateSmooth);

    // Create a Quaternion rotation for the "forwards" position on the camera
    let identityRotation = new Quaternion().setFromAxisAngle(new Vector3(-1, 0, 0), .3);

    // Create an animation clip that begins with the cameras' current rotation, and ends on the camera being
    // rotated towards the game space
    let rotationClip = new QuaternionKeyframeTrack('.quaternion', [0, 2], [
        camera.quaternion.x, camera.quaternion.y, camera.quaternion.z, camera.quaternion.w,
        identityRotation.x, identityRotation.y, identityRotation.z, identityRotation.w
    ]);

    // Associate both KeyFrameTracks to an AnimationClip, so they both play at the same time
    const animationClip = new AnimationClip('animateIn', 4, [track, rotationClip]);
    const animationAction = camera.userData.mixer.clipAction(animationClip);
    animationAction.setLoop(LoopOnce, 1);
    animationAction.clampWhenFinished = true;

    camera.userData.clock = new Clock();
    camera.userData.mixer.addEventListener('finished', function () {
        // Make sure the camera is facing in the right direction
        camera.lookAt(new Vector3(0, -500, -1400));
        // Indicate that the rocket has begun moving
        sceneConfiguration.rocketMoving = true;
    });

    // Play the animation
    camera.userData.mixer.clipAction(animationClip).play();
    // Remove the "start panel" (containing the play buttons) from view
    startPanel.classList.add('hidden');
}

Musíme také zapojit naši logiku, co dělat, když naše úroveň skončí, a kód, jak to udělat, můžete vidět zde.

Závěr

Vytvořením hry v Three.js získáte přístup k neuvěřitelnému množství možných zákazníků. Vzhledem k tomu, že lidé mohou hrát hru ve svém prohlížeči, aniž by si museli nic stahovat nebo instalovat do svých zařízení, stává se to docela přitažlivým způsobem vývoje a distribuce vaší hry.

Jak jsme viděli, je velmi možné vytvořit poutavý a zábavný zážitek pro širokou škálu uživatelů. Takže jediná věc, kterou musíte vyřešit, je, co vytvoříte v Three.js?

Přidáváte nové knihovny JS pro zlepšení výkonu nebo vytváření nových funkcí? Co když dělají opak?

Není pochyb o tom, že frontendy jsou stále složitější. Při přidávání nových knihoven JavaScriptu a dalších závislostí do své aplikace budete potřebovat větší viditelnost, abyste zajistili, že vaši uživatelé nenarazí na neznámé problémy.

LogRocket je řešení pro monitorování frontendových aplikací, které vám umožní přehrát chyby JavaScriptu, jako by se staly ve vašem vlastním prohlížeči, takže můžete na chyby reagovat efektivněji.

https://logrocket.com/signup/

LogRocket funguje perfektně s jakoukoli aplikací, bez ohledu na rámec, a má pluginy pro přihlášení dalšího kontextu z Redux, Vuex a @ngrx/store. Namísto hádání, proč k problémům dochází, můžete agregovat a hlásit, v jakém stavu byla vaše aplikace, když k problému došlo. LogRocket také monitoruje výkon vaší aplikace a hlásí metriky, jako je zatížení procesoru klienta, využití paměti klienta a další.

Budujte sebevědomě — Začněte sledovat zdarma.