How I wrote a browser-based 3D FPS shooter with Three.js, Vue and Blender

Game start screen
Game start screen

Motivation

On the way of every commercial developer (not only coders, but, I know, designers, for example, also) sooner or later come across swampy areas, dull gloomy places, wandering along which you can generally wander into the dead desert of professional burnout and / or even to a psychotherapist for an appointment for pills. Employers business obviously uses your most developed skills, squeezing the maximum stack most positions occupied by the same enterprise-tools do not seem to all cases of the most successful, comfortable and interesting, and you realize that you have just aggravate rake ton this legacy... Often, relationships in a team do not develop in the best way for you, and you do not get real understanding and feedback, drive from colleagues ... The ability to drag yourself "in Munich by the hair", fall in love with technology again, get carried away with something new [generally and / or for myself, maybe - a related field], IMHO, is not just an important quality of a professional, but, in fact, helps the developer to survive in capitalism, remaining not only externally in demand, competitive with young people advancing on the heels, but, above all, giving energy and movement from within. Sometimes you hear something like: "but my ex said that if it was possible not to code, he would not code!". Yes, and today's young people have realized that in today's situation, "honestly and normally" you can only earn in IT, and they are already standing in a crowd on the doorstep of the HR department ... I don't know,I liked coding since childhood, but I want to code something, if not useful, then at least interesting. In short, I am far from a gamer, but in my life there have been several short periods when I shamefully "wasted". Yes, the very passion for computers in childhood began, of course, with games. I remember how in the nineties the Spectrum was brought to the city. There was often practically nothing to eat then, but my father still took the last money from the stash, went, defended an unprecedentedly huge queue and bought my brother and me our first miracle car. We connected it via a cord with SG-5 connectors to a black and white Record TV, the picture shook and blinked, games had to be patiently loaded into RAM from an old cassette recorder [I still hear poisonous loading sounds], often experiencing failures. ..Despite the fact that early programmers and designers managed to place whole worlds with amazing gameplay with their code in 48 kilobytes of RAM, I quickly got tired of playing and I got carried away with programming in BASIC)), I drew sprite graphics (and vector "three-dimensional" then, too was, we even bought a complicated book), wrote simple music in the editor ... So, some time ago I got tired of everything again, it was a pandemic winter and I couldn't ride a bike, the rock group did not rehearse ... I read the forums and set myself several more or less fresh popular games made on Unity or Unreal Engine, obviously. I like RPG-open worlds-survival games, that's all ... After work, I began to plunge into virtual worlds every evening and hack-swing, but it didn't last long. The games are all similar in mechanics,monotonous gameplay is smeared over a small plot into a bunch of similar tasks with endless battles ... But, the funny thing is, it really shamelessly lags in important mechanics. Lagging commercial products that sell for money ... And any "bug", IMHO, this is a strong disappointment - it instantly brings a digital fairy tale out of the virtual environment into the real world ... Of course, excellent graphics, very cool drawn. But, exaggerating, I realized that all these crafts on enterprise engines, in fact, do not even code. They are assembled by managers and designers, simply "playing with the color of the cubes", but the cubes themselves, at the same time, practically "do not change" ... In general, when it became completely boring, I thought that "I can do that too," but right in browser onLagging commercial products that sell for money ... And any "bug", IMHO, this is a strong disappointment - it instantly brings a digital fairy tale out of the virtual environment into the real world ... Of course, excellent graphics, very cool drawn. But, exaggerating, I realized that all these crafts on enterprise engines, in fact, do not even code. They are assembled by managers and designers, simply "playing with the color of the cubes", but the cubes themselves, at the same time, practically "do not change" ... In general, when it became completely boring, I thought that "I can do that too," but right in browser onCommercial products that are sold for money are lagging ... And any "bug", IMHO, is a strong disappointment - it instantly brings a digital fairy tale out of the virtual environment into the real world ... Of course, excellent graphics, very cool drawn. But, exaggerating, I realized that all these crafts on enterprise engines, in fact, do not even code. They are assembled by managers and designers, simply "playing with the color of the cubes", but the cubes themselves, at the same time, practically "do not change" ... In general, when it became completely boring, I thought that "I can do that too," but right in browser onThey are assembled by managers and designers, simply "playing with the color of the cubes", but the cubes themselves, at the same time, practically "do not change" ... In general, when it became completely boring, I thought that "I can do that too," but right in browser onThey are assembled by managers and designers, simply "playing with the color of the cubes", but the cubes themselves, at the same time, practically "do not change" ... In general, when it became completely boring, I thought that "I can do that too," but right in browser ondisgusting not intended to save memory of serious programming javascript. Finally, I decided to fully comply with the fact that all the time with a smart look I repeat to my son: β€œto be able to make games is much more interesting than to play them”. In short, I set out to write my own custom browser-based FPS shooter using open technologies.





So, at the moment, the first result for this long-playing "task for oneself" - you can test: http://robot-game.ru/





Stack and architecture

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



, - «» β€” : , , , , . , Vue 2, , , , , Svelte. , , Three, , . , , , Vue, «» .



- 2D , 3D . , Linux Blender. , , UV- . ! , . «» Β« glTFΒ»: .glb- Β« Β». , , , Β«, Β». , β€” . ( ) ( ) .glb ( β€” ). , Β«glTF Β»: .gltf- β€” . : - - . , .





Spider drone model in Blender
- Blender

- Express MongoDB. , . FPS-, . , - . , , , ( -). β€” . ( ). β€” β€” , β€” glb- β€” , «» β€” . : Β« SPAΒ». Vue, , . , , , - «» β€” . : , , , , , , , - :



window.location.reload(true);







β€” β€” )) , , . , , β€” «» , , . ( ), (MP3, : 44100 16 , 128 / β€” ), - 100 β€” ... β€” Β« Β» β€” , β€” -, . , , «» . «» , , β€” ; …






All textures used in the game
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, Β«-, Β», . β€” - -: ... , , Β« »…



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





. . , .





- ... ... , , , ... β€” β€” , - …





, . , , . , , . ))



, ( β€” !), , «» . β€” β€” . , .





Dashboard

E :





The story of the future inside

. Β« Β», .





. β€” .





β€” β€” «» , β€” β€” , , Β« Β» β€” .





Flowers and bottles

Β« Β» β€” 25 . : «» β€” β€” , «« .





β€” , ( β€” ) , β€” .





Difficulty levels

, :





  • . , - β€” . «» (, , β€” Β« Β» ).





  • β€” β€” : β€” . .





  • . β€” , . - β€” , . β€” - β€” - . , . : - …





  • , β€” .





  • 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 , :





Error

: Esc. β€” P. FPS-: . - . Three, , . β€” Β« Β». . «» β€” . Β« Β» , . .





Optical sight of the winder
Shot up

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





. : . , , . , . .





First location model

:





  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Β». «» Β« Β» β€” .





Door pseudo-object
-
The door won't close

β€” β€” β€” β€” . . )





, β€” . Β« Β».





: . , , -. , Β«mesh`Β». β€” β€” -. Sphere



. β€” () (). β€” .





Pseudo-object helpers for items
-

«» β€” :





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



. :





Debugging with Arrow Wizards Enabled

Β« Β» β€” :





//  @/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 : , , β€” .





If you fall over the wall and run over the edge of the sky

, : .





( 10 ) . . β€” , .





Bulletproof glass

, React c TS !

FPS Three:









  •  





  • In all other possible aspects, we must optimize the animation cycle, casting and calculation of collisions in it in the context of gameplay as carefully as possible, so as to maintain drive, but avoid a drop in performance.





  • Static typing and unit tests are of no help in this experiment.





In principle, I am pleased with what has already happened. And I want to bring it to full beauty. Therefore, if you know someone who is fond of skeletal animation and may agree to add a few simple tracks to my glb - please throw off the link to the article for him?








All Articles