Creazione di un gioco in Three.js

Scritto da Lewis Cianci✏️

Fino a poco tempo fa, l'unico modo per creare e distribuire giochi era scegliere un motore di gioco come Unity o Unreal, imparare la lingua, quindi impacchettare il gioco e distribuirlo sulla piattaforma preferita.

L'idea di tentare di fornire un gioco a un utente tramite il proprio browser sarebbe sembrata un compito impossibile.

Fortunatamente, grazie ai progressi nelle tecnologie dei browser e all'accelerazione hardware disponibili in tutti i browser più diffusi, ai miglioramenti delle prestazioni JavaScript e al costante aumento della potenza di elaborazione disponibile, la creazione di esperienze di gioco interattive per i browser sta diventando sempre più comune.

In questo articolo, vedremo come creare un gioco utilizzando Three.js. Puoi seguire qui e guardare il video tutorial:

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

Ma prima, esaminiamo cos'è Three.js e perché è una buona scelta per lo sviluppo di giochi.

Che cos'è Three.js?

La descrizione del progetto di Three.js su GitHub descrive appropriatamente Three.js come "... una libreria 3D per uso generale, facile da usare, leggera, cross-browser".

Three.js rende relativamente semplice per noi sviluppatori disegnare oggetti e modelli 3D sullo schermo. Senza di esso, dovremmo interfacciarci direttamente con WebGL, il che, sebbene non impossibile, può far sì che anche il più piccolo progetto di sviluppo di giochi richieda un'incredibile quantità di tempo.

Tradizionalmente, un "motore di gioco" è composto da più parti. Ad esempio, Unity e Unreal forniscono un modo per eseguire il rendering di oggetti sullo schermo, ma anche una serie di altre funzionalità, come il networking, la fisica e così via.

Three.js, tuttavia, ha un approccio più limitato e non include cose come la fisica o il networking. Ma questo approccio più semplice significa che è più facile imparare e più ottimizzato per fare ciò che sa fare meglio:disegnare oggetti sullo schermo.

Ha anche un ottimo set di campioni che possiamo usare per capire come disegnare una varietà di oggetti sullo schermo. Infine, fornisce un modo semplice e nativo per caricare i nostri modelli nella nostra scena.

Three.js può essere un'opzione interessante come motore di sviluppo di giochi se non vuoi che i tuoi utenti debbano scaricare un'app tramite un app store o avere qualsiasi configurazione per giocare. Se il tuo gioco funziona nel browser, hai la barriera più bassa all'accesso, il che può essere solo positivo.

Utilizzo di Three.js per creare il nostro gioco

Oggi faremo un tour di Three.js creando un gioco che utilizza shader, modelli, animazioni e logica di gioco. Quello che creeremo sarà simile a questo:

https://youtu.be/XGIThzz9m3aQ

Il concetto è semplice. Abbiamo il controllo di una navicella spaziale, che sta attraversando un pianeta, e il nostro obiettivo è raccogliere cristalli di energia. Dobbiamo anche gestire la salute della nostra nave raccogliendo potenziamenti dello scudo e cercando di non danneggiare la nostra nave troppo gravemente colpendo le rocce sulla scena.

Alla fine della nostra corsa, la navicella spaziale ritorna alla nave madre nel cielo e se l'utente fa clic su AVANTI LIVELLO , fanno un altro tentativo, questa volta con un percorso più lungo attraverso il quale il razzo può viaggiare.

Man mano che l'utente gioca, la velocità del razzo aumenta, quindi deve lavorare più velocemente per schivare le rocce e raccogliere i cristalli di energia.

Per creare un gioco come questo, dobbiamo rispondere alle seguenti domande:

  • Come possiamo spostare una nave spaziale perennemente in avanti su una distesa d'acqua?
  • Come possiamo rilevare le collisioni tra il razzo e gli oggetti?
  • Come possiamo creare un'interfaccia utente che funzioni sia su desktop che su dispositivi mobili?

Quando avremo creato questo gioco, avremo superato queste sfide.

Prima di iniziare a programmare, però, dobbiamo rivedere una breve teoria, in particolare relativa a come creeremo il senso di movimento all'interno del gioco.

Creare il senso del movimento

Immagina per un momento di avere il controllo di un elicottero nella vita reale e di seguire un oggetto a terra. L'oggetto prosegue a una velocità che aumenta gradualmente. Per poter stare al passo, devi aumentare progressivamente la velocità dell'elicottero su cui ti trovi.

Se non ci fossero limiti alla velocità dell'elicottero o dell'oggetto a terra, questo continuerebbe per tutto il tempo in cui si desidera tenere il passo con l'oggetto a terra.

Quando si crea un gioco che segue un oggetto, come stiamo facendo in questo esempio, si può essere tentati di applicare la stessa logica. Cioè, per spostare l'oggetto nello spazio mondiale mentre accelera e aggiornare la velocità della telecamera che sta seguendo. Tuttavia, questo presenta un problema immediato.

Fondamentalmente, tutti coloro che giocano a questo gioco lo giocheranno sui propri telefoni o computer desktop. Questi sono dispositivi che hanno risorse limitate. Se tentiamo di generare una quantità possibilmente illimitata di oggetti mentre la telecamera si muove, e quindi spostiamo quella telecamera, alla fine esauriremo tutte le risorse disponibili e la scheda del browser non risponderà o andrà in crash.

Dobbiamo anche creare un piano (un oggetto 2D piatto) che rappresenti l'oceano. Quando lo facciamo, dobbiamo fornire le dimensioni dell'oceano.

Tuttavia, non possiamo creare un piano di dimensioni infinite, né possiamo creare un aereo gigantesco e sperare solo che l'utente non avanzi mai abbastanza nel nostro livello da poter navigare fuori dall'aereo.

È un design scadente e sperare che le persone non giochino abbastanza al nostro gioco per riscontrare bug sembra controintuitivo.

Movimento infinito entro limiti finiti

Invece di muovere la nostra fotocamera indefinitamente in una direzione, teniamo invece la fotocamera ferma e spostiamo l'ambiente circostante. Questo ha diversi vantaggi.

Uno è che sappiamo sempre dove si trova la nostra nave spaziale, poiché la posizione del razzo non si sposta in lontananza; si muove solo da un lato all'altro. Questo ci consente di capire facilmente se gli oggetti si trovano dietro la telecamera e possono essere rimossi dalla scena per liberare risorse.

L'altro vantaggio è che possiamo scegliere un punto in lontananza in cui creare oggetti. Ciò significa che quando gli oggetti si avvicinano al giocatore, nuovi oggetti o oggetti verranno continuamente creati in lontananza al di fuori della visuale dei giocatori.

Quando scompaiono dalla vista, a causa della collisione del giocatore con loro o per andare dietro al giocatore, questi oggetti vengono eliminati dalla scena per ridurre l'utilizzo della memoria.

Per creare questo effetto, dovremo fare due cose:in primo luogo, dobbiamo spostare proceduralmente ogni elemento lungo l'asse di profondità per spostare gli oggetti verso la telecamera. In secondo luogo, dobbiamo fornire alla nostra superficie dell'acqua un valore da compensare e aumentare questo offset nel tempo.

Questo darà l'effetto che la superficie delle acque si muova sempre più velocemente.

Ora che abbiamo risolto come faremo avanzare il razzo attraverso la scena, passiamo all'impostazione del nostro progetto.

Configurazione del progetto di gioco

Iniziamo a realizzare il nostro gioco! La prima cosa che dobbiamo fare è configurare il nostro ambiente di costruzione. Per questo esempio, ho scelto di utilizzare Typescript e Webpack. Questo articolo non riguarda i vantaggi di queste tecnologie, quindi non entrerò nei dettagli su di esse qui, tranne che per un breve riepilogo.

Usare Webpack significa che quando sviluppiamo il nostro progetto e quando salviamo i nostri file, Webpack vedrà che i nostri file sono cambiati e ricaricherà automaticamente il nostro browser con le modifiche salvate.

Ciò significa che non è necessario aggiornare manualmente il browser ogni volta che si apporta una modifica, risparmiando molto tempo. Significa anche che possiamo utilizzare plug-in come tre-minifier, che riduce le dimensioni del nostro pacchetto quando lo distribuiamo.

L'uso di TypeScript nel nostro esempio significa che il nostro progetto avrà la sicurezza del tipo. Trovo questo particolarmente utile quando lavoro con alcuni dei tipi interni di Three.js, come Vector3 se Quaternions . Sapere che sto assegnando il giusto tipo di valore a una variabile è molto prezioso.

Utilizzeremo anche Materialise CSS per la nostra interfaccia utente. Per i pochi pulsanti e schede che useremo come nostra interfaccia utente, questo framework CSS sarà di grande aiuto.

Per iniziare a lavorare sul nostro progetto, crea una nuova cartella. All'interno della cartella, crea un package.json e incolla i seguenti contenuti in:

{
  "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"
  }
}

Quindi, in una finestra di comando, digita npm i per installare i pacchetti nel tuo nuovo progetto.

Aggiunta di file Webpack

Ora dobbiamo creare tre file, un file di configurazione Webpack di base, seguito da una configurazione di sviluppo e produzione per il nostro progetto.

Crea un [webpack.common.js] file all'interno della cartella del progetto e incollare la seguente configurazione:

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'],
    },

}

Quindi, crea un [webpack.dev.js] archiviare e incollare in questi dettagli. Questo configura la funzionalità di ricarica a caldo del server di sviluppo 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
    },
})

Infine, crea un [webpack.production.js] file e incolla in questi dettagli:

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
        },
    },
})

Configurazione dell'ambiente TypeScript

La prossima cosa che dobbiamo fare è configurare il nostro ambiente TypeScript per permetterci di usare le importazioni da file JavaScript. Per fare ciò, crea un [tsconfig.json] archiviare e incollare nei seguenti dettagli:

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

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

Il nostro ambiente di compilazione è ora configurato. Ora è il momento di mettersi al lavoro creando una scena bella e credibile che i nostri giocatori possano esplorare.

Impostazione della scena del gioco

La nostra scena comprende i seguenti elementi:

  1. La scena stessa (questo è ciò a cui aggiungiamo i nostri oggetti per comprendere il mondo di gioco)
  2. Cielo
  3. Acqua
  4. Oggetti sullo sfondo (le rocce che si trovano su entrambi i lati dell'area di gioco dell'utente)
  5. La nave spaziale
  6. Le file che contengono i cristalli, le rocce e gli oggetti dello scudo (denominate "file della sfida")

Effettueremo la maggior parte del nostro lavoro in un file chiamato game.ts , ma suddivideremo anche parti del nostro gioco in file separati in modo da non ritrovarci con un file incredibilmente lungo. Possiamo andare avanti e creare il game.ts file ora.

Poiché abbiamo a che fare con un argomento piuttosto complesso, includerò anche collegamenti a dove si trova questo codice all'interno del progetto su GitHub. Si spera che questo dovrebbe aiutarti a mantenere l'orientamento e a non perderti in un progetto più ampio.

Creazione del Scene

La prima cosa che dobbiamo fare è creare un Scene quindi Three.js ha qualcosa da renderizzare. All'interno del nostro game.ts , aggiungeremo le seguenti righe per costruire il nostro Scene e inserisci un PerspectiveCamera nella scena, così possiamo vedere cosa sta succedendo.

Infine, creeremo un riferimento per il nostro renderer che assegneremo in seguito:

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

// Our three renderer
let renderer: WebGLRenderer;

Creazione di una funzione di inizializzazione

Per impostare la nostra scena, dobbiamo svolgere alcune attività come la creazione di un nuovo WebGLRenderer e impostando la dimensione della tela su cui vogliamo disegnare.

Per fare ciò, creiamo un init funzione e inserirlo all'interno del nostro game.ts anche. Questo init La funzione eseguirà la configurazione iniziale per la nostra scena e verrà eseguita solo una volta (al primo caricamento del gioco):

/// 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);
}

Dovremo anche sfruttare un ciclo di rendering e animazione per la nostra scena. Avremo bisogno del ciclo di animazione per spostare gli oggetti sullo schermo di cui abbiamo bisogno e avremo bisogno del ciclo di rendering per disegnare nuovi fotogrammi sullo schermo.

Andiamo avanti e creiamo il render funzione ora nel nostro game.ts . All'inizio, questa funzione sembrerà piuttosto spoglia perché richiede semplicemente un fotogramma di animazione e quindi esegue il rendering della scena.

Ci sono diversi motivi per cui richiediamo un frame di animazione, ma uno dei principali è che il nostro gioco si interromperà se l'utente cambia le schede, il che migliorerà le prestazioni e ridurrà l'eventuale spreco di risorse sul dispositivo:

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

Quindi, ora abbiamo la nostra scena vuota con una telecamera, ma nient'altro. Aggiungiamo un po' d'acqua alla nostra scena.

Creazione dell'acqua per il Scene

Fortunatamente, Three.js include un esempio di un oggetto acqua che possiamo usare nella nostra scena. Include riflessi in tempo reale e ha un bell'aspetto; puoi verificarlo qui.

Fortunatamente per noi, quest'acqua realizzerà la maggior parte di ciò che vogliamo fare all'interno della nostra scena. L'unica cosa che dobbiamo fare è cambiare leggermente lo shader per l'acqua in modo da poterlo aggiornare dall'interno del nostro ciclo di rendering.

Lo facciamo perché se compensiamo la nostra consistenza dell'acqua di una quantità crescente col passare del tempo, allora ci darà la sensazione di velocità.

Per dimostrare, questa è la scena di apertura del nostro gioco, ma sto aumentando l'offset a ogni fotogramma. All'aumentare dell'offset, sembra che la velocità dell'oceano sotto di noi stia aumentando (anche se il razzo è effettivamente fermo).

L'oggetto acqua può essere trovato qui su Three.js GitHub. L'unica cosa che dovremo fare è apportare una piccola modifica per rendere questo offset controllabile dal nostro ciclo di rendering (in modo da poterlo aggiornare nel tempo).

La prima cosa che faremo è prendere una copia dell'esempio Water.js nel repository Three.js. Metteremo questo file all'interno del nostro progetto in objects/water.js . Se apriamo il water.js file, circa a metà, inizieremo a vedere qualcosa che assomiglia a questo:

Questi sono gli shader per il nostro materiale oceanico. Gli shader stessi non rientrano nell'ambito di questo articolo, ma fondamentalmente sono istruzioni che il nostro gioco fornirà ai computer dei nostri utenti su come disegnare questo particolare oggetto.

Abbiamo anche il nostro codice shader qui, che è scritto in OpenGraph Shader Language (GLSL), incorporato in un file altrimenti JavaScript.

Non c'è niente di sbagliato in questo, ma se spostiamo questo codice shader in un file da solo, possiamo installare il supporto GLSL nel nostro IDE preferito e otterremo cose come la colorazione e la convalida della sintassi, che ci aiuta a personalizzare il nostro GLSL .

Per suddividere il GLSL in file separati, creiamo un shader directory all'interno del nostro attuale objects directory, seleziona i contenuti del nostro vertexShader e il nostro fragmentShader e spostali in waterFragmentShader.glsl e waterVertexShader.glsl file, rispettivamente.

In cima al nostro [waterFragmentShader.glsl] file, abbiamo un getNoise funzione. Per impostazione predefinita, si presenta così:

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;
}

Per rendere questo offset regolabile dal nostro codice di gioco, vogliamo aggiungere un parametro al nostro file GLSL che ci permetta di modificarlo durante l'esecuzione. Per fare ciò, dobbiamo sostituire questa funzione con la seguente funzione:

// 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;
}

Noterai che abbiamo incluso una nuova variabile in questo file GLSL:il speed variabile. Questa è la variabile che aggiorneremo per dare la sensazione di velocità.

All'interno del nostro game.ts , ora è necessario configurare le impostazioni dell'acqua. Nella parte superiore del nostro file, aggiungi le seguenti variabili:

// 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
    }
);

Quindi, all'interno del nostro init funzione, dobbiamo configurare la rotazione e la posizione del nostro piano d'acqua, in questo modo:

// 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);

Questo darà la rotazione corretta per l'oceano.

Creare il cielo

Three.js viene fornito con un cielo abbastanza convincente che possiamo utilizzare gratuitamente all'interno del nostro progetto. Puoi vedere un esempio di questo nella pagina di esempio di Three.js qui.

È abbastanza facile aggiungere un cielo al nostro progetto; dobbiamo semplicemente aggiungere il cielo alla scena, impostare una dimensione per lo skybox e quindi impostare alcuni parametri che controllano l'aspetto del nostro cielo.

All'interno del nostro init funzione che abbiamo dichiarato, aggiungeremo il cielo alla nostra scena e configureremo le immagini per il cielo:

// 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;

Scene finale preparazione

L'ultima cosa che dobbiamo fare con la nostra inizializzazione della scena iniziale è aggiungere un po' di illuminazione e aggiungere il nostro modello di razzo e il nostro modello di nave madre:

// 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;

Ora abbiamo la nostra scena con dell'acqua bella e un razzo. Ma ci manca tutto ciò che può effettivamente renderlo un gioco. Per risolvere questo problema, dobbiamo costruire alcuni parametri di base per controllare il gioco e consentire al giocatore di muoversi verso determinati obiettivi.

In cima al nostro game.ts file, aggiungeremo il seguente sceneConfiguration variabile, che ci aiuta a tenere traccia degli oggetti all'interno della nostra scena:

// 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
}

Ora, dobbiamo eseguire l'inizializzazione per il livello corrente in cui si trova il giocatore. Questa funzione di configurazione della scena è importante perché verrà richiamata ogni volta che l'utente inizia un nuovo livello.

Quindi, dobbiamo riportare la posizione del nostro razzo all'inizio e ripulire tutte le vecchie risorse che erano in uso. Ho inserito alcuni commenti in linea in modo da poter vedere cosa sta facendo ogni riga:

// 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;
}

Aggiunta della logica di gioco

Ci aspettiamo due tipi di dispositivi per giocare al nostro gioco:computer desktop e telefoni cellulari. A tal fine, abbiamo bisogno di ospitare due tipi di opzioni di input:

  • Tastiere (ovvero i tasti sinistro e destro della tastiera)
  • Touch screen (mostrando un joystick sullo schermo per manovrare l'imbarcazione da sinistra a destra)

configuriamoli ora.

Inserimento da tastiera

In cima al nostro game.ts , aggiungeremo le seguenti variabili per monitorare se i tasti sinistro o destro sono stati premuti sulla tastiera:

let leftPressed = false;
let rightPressed = false;

Quindi, all'interno del nostro init funzione, registreremo il keydown e keyup eventi per chiamare il onKeyDown e onKeyUp funzioni, rispettivamente:

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

Infine, per l'input da tastiera, registreremo cosa fare quando vengono premuti questi tasti:

// 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;
    }
}

Inserimento touch screen

I nostri utenti mobili non avranno una tastiera a cui dare il loro input, quindi useremo nippleJS per creare un joystick sullo schermo e useremo l'output del joystick per influenzare la posizione del razzo sullo schermo.

All'interno del nostro init funzione, verificheremo se il dispositivo è un dispositivo touch controllando se ha una quantità di punti di contatto diversa da zero sullo schermo. Se lo è, creeremo il joystick, ma riporteremo anche il movimento del razzo a zero una volta che il giocatore avrà rilasciato il controllo del joystick:

// 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;
        })
    }
}

All'interno del nostro animate funzione, teniamo traccia di cosa fare se in quel momento vengono premuti i tasti sinistro o destro o se il joystick è in uso. Inoltre, fissiamo la posizione del razzo su una posizione sinistra e destra accettabile, in modo che il razzo non possa muoversi completamente al di fuori dello schermo:

// 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);

Spostare oggetti all'interno della nostra scena

Come abbiamo già discusso, la navicella spaziale rimane ferma all'interno della nostra scena e gli oggetti si muovono verso di essa. La velocità di movimento di questi oggetti aumenta gradualmente man mano che l'utente continua a giocare, il che aumenta la difficoltà del livello nel tempo.

Sempre all'interno del nostro ciclo di animazione, vogliamo spostare progressivamente questi oggetti verso il giocatore. Quando gli oggetti lasciano la visuale del giocatore, vogliamo rimuoverli dalla scena in modo da non occupare risorse non necessarie sul computer del giocatore.

All'interno del nostro ciclo di rendering, possiamo impostare questa funzionalità in questo modo:

// 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);
    }

Possiamo vedere che ci sono alcune funzioni che fanno parte di questa chiamata:

  • detectCollisions
  • addBackgroundBit
  • addChallengeRow

Esploriamo ciò che queste funzioni realizzano all'interno del nostro gioco.

detectCollisions

Il rilevamento delle collisioni è una via importante del nostro gioco. Senza di essa, non sapremo se la nostra nave spaziale ha raggiunto uno degli obiettivi o se ha colpito una roccia e dovrebbe rallentare. Questo è il motivo per cui vogliamo utilizzare il rilevamento delle collisioni all'interno del nostro gioco.

Normalmente, potremmo usare un motore fisico per rilevare le collisioni tra oggetti nella nostra scena, ma Three.js non ha un motore fisico incluso.

Questo non vuol dire che i motori fisici non esistano per Three.js, però. Certamente lo fanno, ma per le nostre esigenze non abbiamo bisogno di aggiungere un motore fisico per verificare se il nostro razzo ha colpito un altro oggetto.

In sostanza, vogliamo rispondere alla domanda:"Il mio modello di razzo si interseca attualmente con altri modelli sullo schermo?" Dobbiamo anche reagire in determinati modi a seconda di ciò che è stato colpito.

Ad esempio, se il nostro giocatore continua a sbattere il razzo contro le rocce, dobbiamo terminare il livello una volta che è stata subito una quantità di danni.

Per ottenere ciò, creiamo una funzione che controlla l'intersezione del nostro razzo e degli oggetti nella scena. A seconda di ciò che il giocatore ha colpito, reagiremo di conseguenza.

Metteremo questo codice all'interno del nostro game directory all'interno di un file chiamato 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;
                        }
                    }
                }
            });
        })
    });
}

L'unica altra cosa che dobbiamo fare per il nostro rilevamento delle collisioni è aggiungere una breve animazione che viene riprodotta quando l'utente si scontra con un oggetto. Questa funzione prenderà la posizione in cui si è verificata la collisione e genererà alcune caselle da questo punto di origine.

Il risultato finale sarà simile a questo.

Per ottenere ciò, dobbiamo creare le scatole in un cerchio attorno al punto in cui si verifica la collisione e animarle verso l'esterno in modo che sembri che esplodano dalla collisione. Per fare ciò, aggiungiamo questa funzionalità all'interno del nostro collisionDetection.ts file:

// 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);
    }

E questo è il nostro rilevamento delle collisioni risolto, completo di una bella animazione quando l'oggetto viene distrutto.

addBackgroundBit

Man mano che la nostra scena avanza, vogliamo aggiungere alcune scogliere su entrambi i lati del giocatore in modo che sembri che il suo movimento sia opportunamente limitato all'interno di un certo spazio. Usiamo l'operatore modulo per aggiungere proceduralmente le rocce a destra o a sinistra dell'utente:

// 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

Man mano che la nostra scena avanza, vogliamo anche aggiungere le nostre "file di sfida" alla scena. Questi sono gli oggetti che contengono rocce, cristalli o oggetti scudo. Ogni volta che viene creata una di queste nuove file, assegniamo in modo casuale rocce, cristalli e scudi a ciascuna riga.

Quindi, nell'esempio sopra, alle celle 1, 2 e 4 non è stato aggiunto nulla, mentre alle celle 3 e 5 è stato aggiunto rispettivamente un cristallo e uno scudo.

Per raggiungere questo obiettivo, pensiamo a queste righe di sfida come suddivise in cinque celle diverse. Generiamo un determinato elemento in ogni cella a seconda dell'output della nostra funzione casuale, in questo modo:

// 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);
}

La funzione di creazione di roccia, cristallo e scudo può essere visualizzata in uno qualsiasi di questi collegamenti.

Aggiunte finali al nostro ciclo di rendering

Le ultime cose che dobbiamo completare all'interno del nostro ciclo di rendering sono le seguenti:

  • Sposta i detriti dagli oggetti raccolti verso la nave
  • Se l'utente completa il livello, mostra l'animazione "volando via" e il riepilogo del livello
  • Se il razzo sta "volando via", regola la telecamera per guardare il razzo in modo che l'utente possa vederlo volare verso la nave madre

Verso la fine della nostra funzione di rendering, possiamo aggiungere il seguente codice per soddisfare questa funzionalità:

// 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);
}

E questo è il nostro ciclo di rendering completato.

Creazione dell'interfaccia utente del gioco

Quando le persone caricano il nostro gioco, vedono alcuni pulsanti che danno loro la possibilità di iniziare a giocare.

Questi sono solo semplici elementi HTML che mostriamo o nascondiamo in modo programmatico a seconda di ciò che sta accadendo nel gioco. L'icona della domanda dà al giocatore un'idea di cosa tratta il gioco e include istruzioni su come giocare. Include anche le (molto importanti!) licenze per i nostri modelli.

E premendo il pulsante rosso si avvia il gioco. Nota che quando premiamo il pulsante rosso Riproduci, la telecamera si sposta e ruota dietro il razzo, preparando il giocatore per l'inizio della scena.

All'interno della nostra scena init funzione, registriamo l'evento per farlo sul onClick gestore di questo pulsante. Per creare le funzioni di rotazione e movimento, dobbiamo fare quanto segue:

  1. Ottieni la posizione corrente e la rotazione della telecamera
  2. Ottieni la posizione futura e la rotazione di dove vogliamo che sia la videocamera
  3. Crea un KeyframeTrack per gestire i movimenti e le rotazioni da entrambe le posizioni di gioco
  4. Assegna queste tracce a un mixer e inizia a riprodurle

Per fare ciò, aggiungeremo il seguente codice nel nostro init funzione, in questo modo:

// 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');
}

Dobbiamo anche impostare la nostra logica su cosa fare quando il nostro livello finisce, e il codice per farlo può essere visto qui.

Conclusione

La creazione di un gioco in Three.js ti dà accesso a un'incredibile quantità di possibili clienti. Poiché le persone possono giocare all'interno del proprio browser senza nulla da scaricare o installare sui propri dispositivi, diventa un modo piuttosto interessante per sviluppare e distribuire il tuo gioco.

Come abbiamo visto, è molto possibile creare un'esperienza coinvolgente e divertente per una vasta gamma di utenti. Quindi, l'unica cosa che devi capire è, cosa creerai in Three.js?

Stai aggiungendo nuove librerie JS per migliorare le prestazioni o creare nuove funzionalità? E se stessero facendo il contrario?

Non c'è dubbio che i frontend stiano diventando più complessi. Man mano che aggiungi nuove librerie JavaScript e altre dipendenze alla tua app, avrai bisogno di maggiore visibilità per assicurarti che i tuoi utenti non incontrino problemi sconosciuti.

LogRocket è una soluzione di monitoraggio delle applicazioni front-end che ti consente di riprodurre gli errori JavaScript come se si fossero verificati nel tuo browser, in modo da poter reagire ai bug in modo più efficace.

https://logrocket.com/signup/

LogRocket funziona perfettamente con qualsiasi app, indipendentemente dal framework, e dispone di plug-in per registrare un contesto aggiuntivo da Redux, Vuex e @ngrx/store. Invece di indovinare perché si verificano problemi, puoi aggregare e segnalare in quale stato si trovava la tua applicazione quando si è verificato un problema. LogRocket monitora anche le prestazioni della tua app, segnalando metriche come il carico della CPU del client, l'utilizzo della memoria del client e altro ancora.

Crea in sicurezza:inizia a monitorare gratuitamente.