Comment j'ai écrit un jeu de tir FPS 3D basé sur un navigateur avec Three.js, Vue et Blender

Écran de dĂ©marrage du jeu
Écran de dĂ©marrage du jeu

Motivation

Sur le chemin de chaque dĂ©veloppeur commercial (pas seulement des codeurs, mais, je sais, des concepteurs, par exemple), tĂŽt ou tard, rencontrez des zones marĂ©cageuses, des endroits sombres et ternes, errant le long desquels vous pouvez gĂ©nĂ©ralement vous promener dans le dĂ©sert mort du burn-out professionnel et / ou mĂȘme chez un psychothĂ©rapeute pour un rendez-vous pour des pilules. Les employeurs utilisent Ă©videmment vos compĂ©tences les plus dĂ©veloppĂ©es, en tirant au maximum, la pile de la plupart des postes vacants est occupĂ©e par les mĂȘmes outils d'entreprise, il semble que ce ne soit pas dans tous les cas les plus rĂ©ussis, les plus pratiques et les plus intĂ©ressants, et vous comprenez que vous aurez pour aggraver une tonne d'un tel hĂ©ritage... Souvent, les relations au sein de l'Ă©quipe ne se dĂ©veloppent pas de la meilleure façon pour vous, et vous n'obtenez pas de rĂ©elle comprĂ©hension et de rĂ©troaction, de la part de collĂšgues ... ou pour moi-mĂȘme, peut-ĂȘtre - un domaine connexe], Ă  mon humble avis, n'est pas juste une qualitĂ© importante d'un professionnel, mais, en fait, aide le dĂ©veloppeur Ă  survivre dans le capitalisme, en restant non seulement en demande externe, compĂ©titif avec les jeunes progressant sur les talons, mais, surtout, en donnant de l'Ă©nergie et du mouvement de l'intĂ©rieur. Parfois, vous entendez quelque chose comme: "mais mon ex a dit que s'il Ă©tait possible de ne pas coder, il ne coderait pas!". Oui, et les jeunes d'aujourd'hui ont rĂ©alisĂ© que dans la situation actuelle, "honnĂȘtement et normalement" on ne peut gagner que dans l'informatique, et ils se trouvent dĂ©jĂ  dans une foule aux portes du dĂ©partement des ressources humaines ... Je ne sais pas,J'aimais coder depuis l'enfance, mais je veux coder quelque chose, sinon utile, du moins intĂ©ressant. Bref, je suis loin d'ĂȘtre un joueur, mais dans ma vie il y a eu plusieurs courtes pĂ©riodes oĂč j'ai honteusement «gaspillé». Oui, la passion mĂȘme pour les ordinateurs dans l'enfance a commencĂ©, bien sĂ»r, avec les jeux. Je me souviens comment, dans les annĂ©es 90, le Spectrum a Ă©tĂ© introduit dans la ville. Il n'y avait souvent pratiquement rien Ă  manger Ă  l'Ă©poque, mais mon pĂšre a quand mĂȘme pris le dernier argent de la rĂ©serve, est allĂ©, a dĂ©fendu une file d'attente sans prĂ©cĂ©dent et a achetĂ© Ă  mon frĂšre et moi notre premiĂšre voiture miracle. Nous l'avons connectĂ© via un cordon avec des connecteurs SG-5 Ă  un Record TV noir et blanc, l'image a tremblĂ© et clignĂ© des yeux, les jeux ont dĂ» ĂȘtre patiemment chargĂ©s dans la RAM Ă  partir d'un vieux magnĂ©tophone Ă  cassette [j'entends toujours des sons de chargement toxiques], souvent en Ă©chec . ..MalgrĂ© le fait que les premiers programmeurs et concepteurs aient rĂ©ussi Ă  placer des mondes entiers avec un gameplay incroyable avec leur code dans 48 kilo-octets de RAM, je me suis rapidement fatiguĂ© de jouer et je me suis emportĂ© avec la programmation en BASIC)), j'ai dessinĂ© des graphiques de sprite (et des vecteurs). "tridimensionnel" alors aussi, nous avons mĂȘme achetĂ© un livre compliquĂ©), Ă©crit de la musique simple dans l'Ă©diteur ... Donc, il y a quelque temps, je me suis Ă  nouveau fatiguĂ© de tout, c'Ă©tait un hiver pandĂ©mique et je ne pouvais pas rouler une moto, le groupe de rock n'a pas rĂ©pĂ©tĂ© ... J'ai lu les forums et me suis fixĂ© plusieurs jeux populaires plus ou moins frais rĂ©alisĂ©s sur Unity ou Unreal Engine, Ă©videmment. J'aime les RPG-mondes ouverts-les jeux de survie, c'est tout ... AprĂšs le travail, j'ai commencĂ© Ă  me plonger dans des mondes virtuels tous les soirs et Ă  hack-swing, mais ça n'a pas durĂ© longtemps. Les jeux sont tous similaires en mĂ©canique,Un gameplay monotone est enduit sur une petite intrigue dans un tas de tĂąches similaires avec des batailles sans fin ... Mais, ce qui est amusant, c'est qu'il traĂźne vraiment sans vergogne dans les mĂ©canismes importants. Les produits commerciaux qui sont vendus pour de l'argent sont Ă  la traĂźne ... Et tout "bug", Ă  mon humble avis, est une forte dĂ©ception - il apporte instantanĂ©ment un conte de fĂ©es numĂ©rique de l'environnement virtuel dans le monde rĂ©el ... Bien sĂ»r, d'excellents graphismes, trĂšs cool dessinĂ©. Mais, exagĂ©rant, je me suis rendu compte que tous ces mĂ©tiers sur les moteurs d'entreprise, en fait, ne codent mĂȘme pas. Ils sont assemblĂ©s par des managers et des designers, simplement "jouant avec la couleur des cubes", mais les cubes eux-mĂȘmes, en mĂȘme temps, pratiquement "ne changent pas" ... En gĂ©nĂ©ral, quand c'est devenu complĂštement ennuyeux, je pensais que "Je peux faire ça aussi", mais directement dans le navigateurLes produits commerciaux qui sont vendus pour de l'argent sont Ă  la traĂźne ... Et tout "bug", Ă  mon humble avis, est une forte dĂ©ception - il apporte instantanĂ©ment un conte de fĂ©es numĂ©rique de l'environnement virtuel dans le monde rĂ©el ... Bien sĂ»r, d'excellents graphismes, trĂšs cool dessinĂ©. Mais, exagĂ©rant, je me suis rendu compte que tous ces mĂ©tiers sur les moteurs d'entreprise, en fait, ne codent mĂȘme pas. Ils sont assemblĂ©s par des managers et des designers, simplement "jouant avec la couleur des cubes", mais les cubes eux-mĂȘmes, en mĂȘme temps, pratiquement "ne changent pas" ... En gĂ©nĂ©ral, quand c'est devenu complĂštement ennuyeux, je pensais que "Je peux faire ça aussi", mais directement dans le navigateurLes produits commerciaux qui sont vendus pour de l'argent sont Ă  la traĂźne ... Et tout "bug", Ă  mon humble avis, est une forte dĂ©ception - il apporte instantanĂ©ment un conte de fĂ©es numĂ©rique de l'environnement virtuel dans le monde rĂ©el ... Bien sĂ»r, d'excellents graphismes, trĂšs cool dessinĂ©. Mais, exagĂ©rant, je me suis rendu compte que tous ces mĂ©tiers sur les moteurs d'entreprise, en fait, ne codent mĂȘme pas. Ils sont assemblĂ©s par des managers et des designers, simplement "jouant avec la couleur des cubes", mais les cubes eux-mĂȘmes, en mĂȘme temps, pratiquement "ne changent pas" ... En gĂ©nĂ©ral, quand c'est devenu complĂštement ennuyeux, je pensais que "Je peux faire ça aussi", mais directement dans le navigateurIls sont assemblĂ©s par des managers et des designers, simplement "jouant avec la couleur des cubes", mais les cubes eux-mĂȘmes, en mĂȘme temps, pratiquement "ne changent pas" ... En gĂ©nĂ©ral, quand c'est devenu complĂštement ennuyeux, je pensais que "Je peux faire ça aussi", mais directement dans le navigateurIls sont assemblĂ©s par des managers et des designers, simplement "jouant avec la couleur des cubes", mais les cubes eux-mĂȘmes, en mĂȘme temps, pratiquement "ne changent pas" ... En gĂ©nĂ©ral, quand c'est devenu complĂštement ennuyeux, je pensais que "Je peux faire ça aussi", mais directement dans le navigateurdĂ©goĂ»tant pas destinĂ© Ă  Ă©conomiser la mĂ©moire de javascript de programmation sĂ©rieuse. Finalement, j'ai dĂ©cidĂ© de me conformer pleinement au fait que tout le temps avec un look intelligent je rĂ©pĂšte Ă  mon fils: «pouvoir faire des jeux est bien plus intĂ©ressant que d'y jouer». En bref, j'ai dĂ©cidĂ© d'Ă©crire mon propre jeu de tir FPS personnalisĂ© basĂ© sur un navigateur basĂ© sur des technologies ouvertes.





Donc, pour le moment, le premier résultat pour cette "tùche pour soi" de longue durée - vous pouvez tester: http://robot-game.ru/





Pile et architecture

, - (
 - quakejs WebAssembly), , , , . Three.js . , , , . .



, - «» — : , , , , . , Vue 2, , , , , Svelte. , , Three, , . , , , Vue, «» .



- 2D , 3D . , Linux Blender. , , UV- . ! , . «» « glTF»: .glb- « ». , , , «, ». , — . ( ) ( ) .glb ( — ). , «glTF »: .gltf- — . : - - . , .





ModÚle de drone araignée dans Blender
- Blender

- Express MongoDB. , . FPS-, . , - . , , , ( -). — . ( ). — — , — glb- — , «» — . : « SPA». Vue, , . , , , - «» — . : , , , , , , , - :



window.location.reload(true);







— — )) , , . , , — «» , , . ( ), (MP3, : 44100 16 , 128 / — ), - 100 — ... — « » — , — -, . , , «» . «» , , — ; 







Toutes les textures utilisées dans le jeu
Performance

. — , ! , « » Three (, , ). , . . . , . «» . , — , . , -.





«». , [ ] — ( ). : c « » scene.remove(object.mesh)



— — , :





//    Object3D  Three
object.mesh.visible = false;
//     
object.isPicked = true;
      
      



, , id



: number mesh` uuid



: string . — Three , « » ( - - — uuid



).





.dispose()



, « ». « — , , — ». , « ».





:





.
└─ /public //  
│  ├─ /audio // 
│  │  └─ ...
│  ├─ /images // 
│  │  ├─ /favicons //    
│  │  │  └─ ...
│  │  ├─ /modals //    
│  │  │  ├─ /level1 //   1
│  │  │  │  └─ ...
│  │  │  └─ ...
│  │  ├─ /models
│  │  │  ├─ /Levels
│  │  │  │  ├─ /level0 // -  (  0 -  )
│  │  │  │  │  └─ Scene.glb
│  │  │  │  └─ ...
│  │  │  └─ /Objects
│  │  │     ├─ Element.glb
│  │  │     └─ ...
│  │  └─ /textures
│  │     ├─ texture1.jpg
│  │     └─ ...
│  ├─ favicon.ico //   16  16
│  ├─ index.html //  
│  ├─ manifest.json //  
│  └─ start.jpg //    )
├─ /src
│  ├─ /assets //  
│  │  └─ optical.png //     )))
│  ├─ /components // ,   
│  │  ├─ /Layout //    UI-  
│  │  │  ├─ Component1.vue //  1
│  │  │  ├─ mixin1.js //  1
│  │  │  └─ ...
│  │  └─ /Three //  
│  │     ├─ /Modules //     
│  │     │  └─ ...
│  │     └─ /Scene
│  │        ├─ /Enemies //  
│  │        │  ├─ Enemy1.js
│  │        │  └─ ...
│  │        ├─ /Weapon //  
│  │        │  ├─ Explosions.js // 
│  │        │  ├─ HeroWeapon.js //  
│  │        │  └─ Shots.js //  
│  │        ├─ /World //    
│  │        │  ├─ Element1.js
│  │        │  └─ ...
│  │        ├─ Atmosphere.js //        ( , ,  )      
│  │        ├─ AudioBus.js // -
│  │        ├─ Enemies.js //   
│  │        ├─ EventsBus.js //  
│  │        ├─ Hero.js //  
│  │        ├─ Scene.vue //   
│  │        └─ World.js // 
│  ├─ /store //  Vuex
│  │  └─ ...
│  ├─ /styles //    SCSS
│  │  └─ ...
│  ├─ /utils //   js-   
│  │  ├─ api.js //     
│  │  ├─ constants.js //     -
│  │  ├─ i18n.js //  
│  │  ├─ screen-helper.js //  " "
│  │  ├─ storage.js //      
│  │  └─ utilities.js //   -
│  ├─ App.vue // "" 
│  └─ main.js //   Vue
└─ ... //      ,  : , gitignore, README.md  

      
      



UI- . . , .





« » — , GPU 60FPS Google Chrome ( Yandex Bro). Firefox , 2-3 . , , — «» . . « WebGL », - ))...





« » — FPS, «-, », . — - -: ... , , « » 



, . - - , , , , . . , , , , . , , , , . -, -. , , .





. . , .





- ... ... , , , ... — — , - 






, . , , . , , . ))



, ( — !), , «» . — — . , .





Tableau de bord

E :





L'histoire du futur à l'intérieur

. « », .





. — .





— — «» , — — , , « » — .





Fleurs et bouteilles

« » — 25 . : «» — — , «« .





— , ( — ) , — .





Niveaux de difficulté

, :





  • . , - — . «» (, , — « » ).





  • — — : — . .





  • . — , . - — , . — - — - . , . : - 






  • , — .





  • 2D- ( )





, , 






, .





, . . «», . , , , — , , . , . ( , ? React c CSS Modules — Flow, TS — , , !!! string
 , ?). « » TDD, « GUI». — GUI, . — , «» , , .





, ( TDD). — , — , . . — .





( DESIGN



), - constants.js.





Three -, , . , , . , — — — «»- — gld- . ( ) «» Sphere



Ray



Three. FPS-: , .





, « » Pointer_Lock_API. Three -, :





// Controls

// In First Person

...
      
      



! — « » Esc . UI/UX — P — . — — — Esc, — . 27 , :





Erreur

: Esc. — P. FPS-: . - . Three, , . — « ». . «» — . « » , . .





Viseur optique de l'hélice à vin
Explosé

Three , . , , . — — ( ). : «» «» — , . — T.





.





Scene.vue :





  • Three: Renderer, Scene , Camera Audio listener , Controls









  • — mesh` —





  • — Vuex





  • ( , ) ,





  • ,





  • ,









, , . - , mesh` . . « » — — — « » ( -?). — , ( ), . -.





— , , — :





import * as Three from 'three';

import { DESIGN } from '@/utils/constants';

function Module() {
  let variable; //   -             
  // ...

  // 
  this.init = (
    scope,
    texture1,
    material1,
    // ...
  ) => {
    // variable = ...
    // ...
  };

  //       -  (, ,   )
  this.animate = (scope) => {
    //             Scene.vue:
    scope.moduleObjectsSore.filter(object => object.mode === DESIGN.ENEMIES.mode.active).forEach((object) => {
      // scope.number = ...
      // scope.direction = new Three.Vector3(...);
      // variable = ... - , ,  ,   let variableNew;
      // ...
    });
  };
}

export default Module;

      
      



Vuex 3 . layout.js : - , API-. hero.js — , /. , , setScale



setUser



.





preloader.js boolean- false



. isGameLoaded



— — — false



true



— . — : , , .





, , :





import * as Three from 'three';

import { loaderDispatchHelper } from '@/utils/utilities';

function Module() {
  this.init = (
    scope,
    // ...
  ) => {
    const sandTexture = new Three.TextureLoader().load(
      './images/textures/sand.jpg',
      () => {
        scope.render(); //          "  "  
        loaderDispatchHelper(scope.$store, 'isSandLoaded');
      },
    );

  };
}

export default Module;
      
      



//  @/utils/utilities.js:

export const loaderDispatchHelper = (store, field) => {
  store.dispatch('preloader/preloadOrBuilt', field).then(() => {
    store.dispatch('preloader/isAllLoadedAndBuilt');
  }).catch((error) => { console.log(error); });
};
      
      



— - - « ?».





UI . , « ».





, , — . , ( ) LoadingManager`.





:





1) - PositionalAudio







2)





-API Three API . , . .





Hero [ ] :





//  @/components/Three/Scene/Hero.js:
import * as Three from "three";

import {
  DESIGN,
  // ...
} from '@/utils/constants';

import {
  loaderDispatchHelper,
  // ...
} from '@/utils/utilities';

function Hero() {
  const audioLoader = new Three.AudioLoader();
  let steps;
  let speed;
  // ...

  this.init = (
    scope,
    // ...
  ) => {
    audioLoader.load('./audio/steps.mp3', (buffer) => {
      steps = scope.audio.addAudioToHero(scope, buffer, 'steps', DESIGN.VOLUME.hero.step, false);
      loaderDispatchHelper(scope.$store, 'isStepsLoaded');
    });
  };

  this.setHidden = (scope, isHidden) => {
    if (isHidden) {
      // ...
      steps.setPlaybackRate(0.5);
    } else {
      // ...
      steps.setPlaybackRate(1);
    }
  };

  this.setRun = (scope, isRun) => {
    if (isRun && scope.keyStates['KeyW']) {
      steps.setVolume(DESIGN.VOLUME.hero.run);
      steps.setPlaybackRate(2);
    } else {
      steps.setVolume(DESIGN.VOLUME.hero.step);
      steps.setPlaybackRate(1);
    }
  };

  // ...

  this.animate = (scope) => {
    if (scope.playerOnFloor) {
      if (!scope.isPause) {
        // ...

        // Steps sound
        if (steps) {
          if (scope.keyStates['KeyW']
            || scope.keyStates['KeyS']
            || scope.keyStates['KeyA']
            || scope.keyStates['KeyD']) {
            if (!steps.isPlaying) {
              speed = scope.isHidden ? 0.5 : scope.isRun ? 2 : 1;
              steps.setPlaybackRate(speed);
              steps.play();
            }
          }
        }
      } else {
        if (steps && steps.isPlaying) steps.pause();

        // ...
      }
    }
  };
}

export default Module;

      
      



? — , . , , « » « » — . — — « ». — , . . — . . — .





. — . — — — . :





if (!isLoop) audio.onEnded = () => audio.stop();







!





import * as Three from "three";

import { DESIGN, OBJECTS } from '@/utils/constants';

import { loaderDispatchHelper } from '@/utils/utilities';

function Module() {
  const audioLoader = new Three.AudioLoader();
  // ...

  let material = null;
  const geometry = new Three.SphereBufferGeometry(0.5, 8, 8);
  let explosion;
  let explosionClone;

  let boom;

  this.init = (
    scope,
    fireMaterial,
    // ...
  ) => {
    //    -       
    audioLoader.load('./audio/mechanism.mp3', (buffer) => {
      loaderDispatchHelper(scope.$store, 'isMechanismLoaded');

      scope.array = scope.enemies.filter(enemy => enemy.name !== OBJECTS.DRONES.name);

      scope.audio.addAudioToObjects(scope, scope.array, buffer, 'mesh', 'mechanism', DESIGN.VOLUME.mechanism, true); 
    });

    //   -   - "  "  -     
    material = fireMaterial;

    explosion = new Three.Mesh(geometry, material);

    audioLoader.load('./audio/explosion.mp3', (buffer) => {
      loaderDispatchHelper(scope.$store, 'isExplosionLoaded');
      boom = buffer;
    });
  };

  // ...

  // ... -   :
  this.moduleFunction = (scope, enemy) => {
    scope.audio.startObjectSound(enemy.id, 'mechanism');
    // ...
    scope.audio.stopObjectSound(enemy.id, 'mechanism');
    // ...
  };

  //      :
  this.addExplosionToBus = (
    scope,
    // ...
  ) => {
    explosionClone = explosion.clone();
    // ..
    scope.audio.playAudioOnObject(scope, explosionClone, boom, 'boom', DESIGN.VOLUME.explosion);
    // ..
  };
}

export default Module;

      
      



, ? ))





: — . , , — — Clock



Three. .





. : . , , . , . .





Premier modĂšle de localisation

:





  1. .





  2. . OBJECTS



    «» , .





  3. , — . - — .





  4. . — «».





  5. .





glb , , — , . . , , . . , . , Mandatory , — . - — «» — . :





room.geometry.computeBoundingBox();







room.visible = false;







— — «» :





//  @/components/Three/Scene/World/Screens.js:
this.isHeroInRoomWithScreen = (scope, screen) => {
 scope.box.copy(screen.room.geometry.boundingBox).applyMatrix4(screen.room.matrixWorld); 
 if (scope.box.containsPoint(scope.camera.position)) return true;
 return false;
};
      
      



— «» , «» — , «mesh». «» « » — .





Pseudo-objet de porte
-
La porte ne se ferme pas

— — — — . . )





, — . « ».





: . , , -. , «mesh`». — — -. Sphere



. — () (). — .





Assistants pseudo-objets pour les objets
-

«» — :





//  @/components/Three/Scene/World.js:

const pseudoGeometry = new Three.SphereBufferGeometry(DESIGN.HERO.HEIGHT / 2,  4, 4); 
const pseudoMaterial = new Three.MeshStandardMaterial({
 color: DESIGN.COLORS.white,
 side: Three.DoubleSide,
});

new Bottles().init(scope, pseudoGeometry, pseudoMaterial);

      
      



:





//  @/components/Three/Scene/World/Thing.js:
import * as Three from 'three';

import { GLTFLoader } from '@/components/Three/Modules/Utils/GLTFLoader';

import { OBJECTS } from '@/utils/constants';

import { loaderDispatchHelper } from '@/utils/utilities';

function Thing() {
  let thingClone;
  let thingGroup;
  let thingPseudo;
  let thingPseudoClone;

  this.init = (
    scope,
    pseudoGeometry,
    pseudoMaterial,
  ) => {
    thingPseudo = new Three.Mesh(pseudoGeometry, pseudoMaterial);

    new GLTFLoader().load(
      './images/models/Objects/Thing.glb',
      (thing) => {
        loaderDispatchHelper(scope.$store, 'isThingLoaded'); //  

        for (let i = 0; i < OBJECTS.THINGS[scope.l].data.length; i++) {
          // eslint-disable-next-line no-loop-func
          thing.scene.traverse((child) => {
            // ... -  ""   
          });

          //    
          thingClone = thing.scene.clone();
          thingPseudoClone = thingPseudo.clone();

          //            
          thingPseudoClone.name = OBJECTS.THINGS.name;
          thingPseudoClone.position.y += 1.5; //     
          thingPseudoClone.visible = false; //  

          thingPseudoClone.updateMatrix(); // 
          thingPseudoClone.matrixAutoUpdate = false; //  

          //       
          thingGroup = new Three.Group();
          thingGroup.add(thingClone);
          thingGroup.add(thingPseudoClone);

          //        
          thingGroup.position.set(
            OBJECTS.THINGS[scope.l].data[i].x,
            OBJECTS.THINGS[scope.l].data[i].y,
            OBJECTS.THINGS[scope.l].data[i].z,
          );

          //   " " -      
          scope.things.push({
            id: thingPseudoClone.id,
            group: thingGroup,
          });
          scope.objects.push(thingPseudoClone);

          scope.scene.add(thingGroup); //   
        }
        loaderDispatchHelper(scope.$store, 'isThingsBuilt'); // 
      },
    );
  };
}

export default Thing;
      
      



«» Hero.js:





//  @/components/Three/Scene/Hero.js:
import { DESIGN, OBJECTS } from '@/utils/constants';

function Hero() {
  // ...

  this.animate = (scope) => {
    // ...

    // Raycasting

    // Forward ray
    scope.direction = scope.camera.getWorldDirection(scope.direction);
    scope.raycaster.set(scope.camera.getWorldPosition(scope.position), scope.direction);
    scope.intersections = scope.raycaster.intersectObjects(scope.objects);
    scope.onForward = scope.intersections.length > 0 ? scope.intersections[0].distance < DESIGN.HERO.CAST : false;

    if (scope.onForward) {
      scope.object = scope.intersections[0].object;

      //   THINGS
      if (scope.object.name.includes(OBJECTS.THINGS.name)) {
        // ...
      }
    }

    // ...
  };
}

export default Hero;
      
      



. , - , , . :





//  @/utils/utilities.js:

// let arrowHelper;

const fixNot = (value) => {
 if (!value) return Number.MAX_SAFE_INTEGER;
 return value;
};

export const isEnemyCanMoveForward = (scope, enemy) => {
 scope.ray = new Three.Ray(enemy.collider.center, enemy.mesh.getWorldDirection(scope.direction).normalize());

 scope.result = scope.octree.rayIntersect(scope.ray);
 scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray);
 scope.resultEnemies = scope.octreeEnemies.rayIntersect(scope.ray);

 // arrowHelper = new Three.ArrowHelper(scope.direction, enemy.collider.center, 6, 0xffffff);
 // scope.scene.add(arrowHelper);

 if (scope.result || scope.resultDoors || scope.resultEnemies) {
   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance), fixNot(scope.resultEnemies.distance));
   return scope.number > 6;
 }
 return true;
};

      
      



Three ArrowHelper



. :





Débogage avec les assistants de flÚche activés

« » — :





//  @/utils/utilities.js:
export const isToHeroRayIntersectWorld = (scope, collider) => {
 scope.direction.subVectors(collider.center, scope.camera.position).negate().normalize();
 scope.ray = new Three.Ray(collider.center, scope.direction);

 scope.result = scope.octree.rayIntersect(scope.ray);
 scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray);
 if (scope.result || scope.resultDoors) {
   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance));
   scope.dictance = scope.camera.position.distanceTo(collider.center);
   return scope.number < scope.dictance;
 }
 return false;
};

      
      



, Enemies.js . - :





//  @/utils/constatnts.js:
export const DESIGN = {
  DIFFICULTY: {
    civil: 'civil',
    anarchist: 'anarchist',
    communist: 'communist',
  },
  ENEMIES: {
    mode: {
      idle: 'idle',
      active: 'active',
      dies: 'dies',
      dead: 'dead',
    },
    spider: {
      // ...
      decision: {
        enjoy: 60,
        rotate: 25,
        shot: {
          civil: 40,
          anarchist: 30,
          communist: 25,
        },
        jump: 50,
        speed: 20,
        bend: 30,
      },
    },
    drone: {
      // ...
      decision: {
        enjoy: 50,
        rotate: 25,
        shot: {
          civil: 50,
          anarchist: 40,
          communist: 30,
        },
        fly: 40,
        speed: 20,
        bend: 25,
      },
    },
  },
  // ...
};
      
      



//  @/components/Three/Scene/Enemies.js:
import { DESIGN } from '@/utils/constants';

import {
  randomInteger,
  isEnemyCanShot,
  // ...
} from "@/utils/utilities";

function Enemies() {
  // ...


  const idle = (scope, enemy) => {
    // ...
  };

  const active = (scope, enemy) => {
    // ...

    // -    :    ( )
    scope.decision = randomInteger(1, DESIGN.ENEMIES[enemy.name].decision.shot[scope.difficulty]) === 1;
    if (scope.decision) {
      if (isEnemyCanShot(scope, enemy)) {
        scope.boolean = enemy.name === OBJECTS.DRONES.name;
        scope.world.shots.addShotToBus(scope, enemy.mesh.position, scope.direction, scope.boolean);
        scope.audio.replayObjectSound(enemy.id, 'shot');
      }
    }
  };

  const gravity = (scope, enemy) => {
    // ...
  };

  this.animate = (scope) => {
    scope.enemies.filter(enemy => enemy.mode !== DESIGN.ENEMIES.mode.dead).forEach((enemy) => {
      switch (enemy.mode) {
        case DESIGN.ENEMIES.mode.idle:
          idle(scope, enemy);
          break;

        case DESIGN.ENEMIES.mode.active:
          active(scope, enemy);
          break;

        case DESIGN.ENEMIES.mode.dies:
          gravity(scope, enemy);
          break;
      }
    });
  };
}

export default Enemies;

      
      



, ( , , ) .





! : idle — — . — + . .





«» 3D- — , .





, — / . — — « » ( , ).





: : 1) , , , , 2) 3) . «» « ». 





. - — . : / .





-. , : 1) 2) . «» .





— . , «», — , — «»: -. . )





— «» . , , . — . .





//  @/utils/constatnts.js:
export const DESIGN = {
  OCTREE_UPDATE_TIMEOUT: 0.5,
  // ...
};
      
      



//  @/utils/utilities.js:
//       
import * as Three from "three";
import { Octree } from "../components/Three/Modules/Math/Octree";

export const updateEnemiesPersonalOctree = (scope, id) => {
  scope.group = new Three.Group();
  scope.enemies.filter(obj => obj.id !== id).forEach((enemy) => {
    scope.group.add(enemy.pseudoLarge);
  });
  scope.octreeEnemies = new Octree();
  scope.octreeEnemies.fromGraphNode(scope.group);
  scope.scene.add(scope.group);
};

      
      



//  
const enemyCollitions = (scope, enemy) => {
  //  c  - , ,   
  scope.result = scope.octree.sphereIntersect(enemy.collider);
  enemy.isOnFloor = false;

  if (scope.result) {
    enemy.isOnFloor = scope.result.normal.y > 0;
    //  ?
    if (!enemy.isOnFloor) {
      enemy.velocity.addScaledVector(scope.result.normal, -scope.result.normal.dot(enemy.velocity));
    } else {
      //           
      // ...
    }

    enemy.collider.translate(scope.result.normal.multiplyScalar(scope.result.depth));
  }

  //  c 
  scope.resultDoors = scope.octreeDoors.sphereIntersect(enemy.collider);
  if (scope.resultDoors) {
    enemy.collider.translate(scope.resultDoors.normal.multiplyScalar(scope.resultDoors.depth));
  }

  //       ,    
  if (scope.enemies.length > 1
    && !enemy.updateClock.running) {
    if (!enemy.updateClock.running) enemy.updateClock.start();

    updateEnemiesPersonalOctree(scope, enemy.id);

    scope.resultEnemies = scope.octreeEnemies.sphereIntersect(enemy.collider);
    if (scope.resultEnemies) {
      result = scope.resultEnemies.normal.multiplyScalar(scope.resultEnemies.depth);
      result.y = 0;
      enemy.collider.translate(result);
    }
  }

  if (enemy.updateClock.running) {
    enemy.updateTime += enemy.updateClock.getDelta();

    if (enemy.updateTime > DESIGN.OCTREE_UPDATE_TIMEOUT && enemy.updateClock.running) {
      enemy.updateClock.stop();
      enemy.updateTime = 0;
    }
  }
};

      
      



Atmosphere.js : , , — .





Si vous tombez par-dessus le mur et courez sur le bord du ciel

, : .





( 10 ) . . — , .





Du verre résistant aux balles

, React c TS !

FPS Three:









  •  





  • Dans tous les autres aspects possibles, il faut optimiser au maximum le cycle d'animation, le casting et le calcul des collisions dans le cadre du gameplay, de maniĂšre Ă  conserver le dynamisme, mais Ă©viter une baisse des performances.





  • Le typage statique et les tests unitaires ne sont d'aucune utilitĂ© dans cette expĂ©rience.





En principe, je suis satisfait de ce qui s'est déjà produit. Et je veux lui apporter toute sa beauté. Par conséquent, si vous connaissez quelqu'un qui aime l'animation squelettique et peut accepter d'ajouter quelques pistes simples à mon glb - veuillez supprimer le lien vers l'article pour lui?








All Articles