CĂłmo escribĂ­ un shooter 3D FPS basado en navegador con Three.js, Vue y Blender

Pantalla de inicio del juego
Pantalla de inicio del juego

MotivaciĂłn

En el camino de todos los desarrolladores comerciales (no solo los programadores, sino, lo sé, los diseñadores, por ejemplo, también) tarde o temprano se encuentran con áreas pantanosas, lugares lúgubres y aburridos, vagando por los cuales generalmente se puede vagar por el desierto muerto del agotamiento profesional. y / o incluso a un psicoterapeuta para una cita para pastillas. Los empleadores de negocios obviamente usan tus habilidades más desarrolladas, exprimiéndolas al máximo, la pila de la mayoría de las vacantes está ocupada por las mismas herramientas empresariales, parece que no para todos los casos las más exitosas, convenientes e interesantes, y entiendes que tendrás para agravar una tonelada de tal legado... A menudo, las relaciones en el equipo no se desarrollan de la mejor manera para usted, y no obtiene una comprensión y retroalimentación reales, impulsada por los colegas ... o por mí, tal vez, un campo relacionado], en mi humilde opinión, no lo es. solo una cualidad importante de un profesional, pero, de hecho, ayuda al desarrollador a sobrevivir en el capitalismo, manteniéndose no solo en demanda externa, competitiva con los jóvenes que avanzan pisándole los talones, sino, sobre todo, dando energía y movimiento desde adentro. A veces escuchas algo como: "pero mi ex dijo que si era posible no codificar, ¡él no codificaría!". Sí, y los jóvenes de hoy se han dado cuenta de que en la situación actual, "honestamente y normalmente" solo se puede ganar en TI, y ya están parados entre una multitud en la puerta del departamento de recursos humanos ... No sé,Me gustó codificar desde la infancia, pero quiero codificar algo, si no es útil, al menos interesante. En resumen, estoy lejos de ser un jugador, pero en mi vida ha habido varios períodos cortos en los que vergonzosamente "desperdicié". Sí, la pasión por las computadoras en la infancia comenzó, por supuesto, con los juegos. Recuerdo cómo en los noventa se trajo el Spectrum a la ciudad. A menudo no había prácticamente nada para comer entonces, pero mi padre todavía tomó el último dinero del alijo, fue, defendió una cola sin precedentes y nos compró a mi hermano y a mí nuestro primer auto milagroso. Lo conectamos a través de un cable con conectores SG-5 a un Record TV en blanco y negro, la imagen temblaba y parpadeaba, los juegos tenían que cargarse pacientemente en la RAM desde una grabadora de casete vieja [todavía escucho sonidos de carga venenosos], a menudo experimentando fallas ...A pesar de que los primeros programadores y diseñadores lograron colocar mundos enteros con una jugabilidad increíble con su código en 48 kilobytes de RAM, rápidamente me cansé de jugar y me dejé llevar por la programación en BASIC), dibujé gráficos de sprites (y vectores "tridimensional" entonces, también lo era, incluso compramos un libro complicado), escribimos música simple en el editor ... Entonces, hace un tiempo me cansé de todo otra vez, era un invierno pandémico y no podía montar una bicicleta, el grupo de rock no ensayó ... Leí los foros y me puse varios juegos populares más o menos frescos hechos en Unity o Unreal Engine, obviamente. Me gustan los juegos de rol, mundos abiertos, supervivencia, eso es todo ... Después del trabajo, comencé a sumergirme en los mundos virtuales todas las noches y al hack-swing, pero no duró mucho. Todos los juegos son similares en mecánica,El juego monótono se extiende sobre una pequeña trama en un montón de tareas similares con batallas interminables ... Pero, lo curioso es que realmente se retrasa descaradamente en mecánicas importantes. Los productos comerciales que se venden por dinero se están quedando atrás ... Y cualquier "error", en mi humilde opinión, es una gran decepción: instantáneamente trae un cuento de hadas digital del entorno virtual al mundo real ... Por supuesto, excelentes gráficos, muy fresco dibujado. Pero, exagerando, me di cuenta de que todas estas artesanías en motores empresariales, de hecho, ni siquiera codifican. Son ensamblados por gerentes y diseñadores, simplemente "jugando con el color de los cubos", pero los cubos en sí, al mismo tiempo, prácticamente "no cambian" ... En general, cuando se volvió completamente aburrido, pensé que "Yo también puedo hacer eso", pero directamente en el navegadorLos productos comerciales que se venden por dinero se están quedando atrás ... Y cualquier "error", en mi humilde opinión, es una gran decepción: instantáneamente trae un cuento de hadas digital del entorno virtual al mundo real ... Por supuesto, excelentes gráficos, muy fresco dibujado. Pero, exagerando, me di cuenta de que todas estas artesanías en motores empresariales, de hecho, ni siquiera codifican. Son ensamblados por gerentes y diseñadores, simplemente "jugando con el color de los cubos", pero los cubos en sí, al mismo tiempo, prácticamente "no cambian" ... En general, cuando se volvió completamente aburrido, pensé que "Yo también puedo hacer eso", pero directamente en el navegadorProductos comerciales rezagados que se venden por dinero ... Y cualquier "error", en mi humilde opinión, esto es una gran decepción: instantáneamente trae un cuento de hadas digital del entorno virtual al mundo real ... Por supuesto, excelentes gráficos, muy fresco dibujado. Pero, exagerando, me di cuenta de que todas estas artesanías en motores empresariales, de hecho, ni siquiera codifican. Son ensamblados por gerentes y diseñadores, simplemente "jugando con el color de los cubos", pero los cubos en sí, al mismo tiempo, prácticamente "no cambian" ... En general, cuando se volvió completamente aburrido, pensé que "Yo también puedo hacer eso", pero directamente en el navegadorSon ensamblados por gerentes y diseñadores, simplemente "jugando con el color de los cubos", pero los cubos en sí, al mismo tiempo, prácticamente "no cambian" ... En general, cuando se volvió completamente aburrido, pensé que "Yo también puedo hacer eso", pero directamente en el navegadorSon ensamblados por gerentes y diseñadores, simplemente "jugando con el color de los cubos", pero los cubos en sí, al mismo tiempo, prácticamente "no cambian" ... En general, cuando se volvió completamente aburrido, pensé que "Yo también puedo hacer eso", pero directamente en el navegadorrepugnante no pretende ahorrar memoria de programación seria javascript. Finalmente, decidí cumplir plenamente con el hecho de que todo el tiempo con una mirada inteligente le repito a mi hijo: “poder hacer juegos es mucho más interesante que jugarlos”. En resumen, me propuse escribir mi propio shooter de FPS basado en navegador personalizado basado en tecnologías abiertas.





Entonces, en este momento, el primer resultado para esta "tarea para uno mismo" de larga duraciĂłn, puede probar: http://robot-game.ru/





Pila y arquitectura

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



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



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





Modelo de dron Spider en Blender
- Blender

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



window.location.reload(true);







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






Todas las texturas utilizadas en el juego.
ActuaciĂłn

. — , ! , « » 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, «-, », . — - -: ... , , « »…



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





. . , .





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





, . , , . , , . ))



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





Tablero

E :





La historia del futuro en el interior

. « », .





. — .





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





Flores y botellas

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





— , ( — ) , — .





Niveles de dificultad

, :





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





  • — — : — . .





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





  • , — .





  • 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, , . — « ». . «» — . « » , . .





Mira óptica de la hélice de vino
Disparado

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





. : . , , . , . .





Primer modelo de ubicaciĂłn

:





  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». «» « » — .





Pseudoobjeto de puerta
-
La puerta no se cierra

— — — — . . )





, — . « ».





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



. — () (). — .





Ayudantes de pseudoobjetos para elementos
-

«» — :





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



. :





DepuraciĂłn con Arrow Wizards habilitados

« » — :





//  @/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 te caes por la pared y corres por el borde del cielo

, : .





( 10 ) . . — , .





Vidrio a prueba de balas

, React c TS !

FPS Three:









  •  





  • En todos los demás aspectos posibles, debemos optimizar el ciclo de animaciĂłn, el lanzamiento y el cálculo de las colisiones en el contexto del juego con el mayor cuidado posible, para mantener la conducciĂłn, pero evitar una caĂ­da en el rendimiento.





  • La mecanografĂ­a estática y las pruebas unitarias no ayudan en este experimento.





En principio, estoy satisfecho con lo que ya ha sucedido. Y quiero llevarlo a la máxima belleza. Por lo tanto, si conoce a alguien aficionado a la animación esquelética y puede aceptar agregar algunas pistas simples a mi glb, por favor, elimine el enlace al artículo para él.








All Articles