Como escrevi um jogo de tiro 3D FPS baseado em navegador com Three.js, Vue e Blender

Tela inicial do jogo
Tela inicial do jogo

Motivação

No caminho de cada desenvolvedor comercial (não apenas programadores, mas, eu sei, designers, por exemplo, também), mais cedo ou mais tarde se deparar com áreas pantanosas, lugares sombrios e sombrios, vagando ao longo dos quais você geralmente pode vagar pelo deserto morto do esgotamento profissional e / ou mesmo a um psicoterapeuta para uma consulta de comprimidos. Os empregadores obviamente usam suas habilidades mais desenvolvidas, espremendo ao máximo, a pilha de mais vagas é ocupada pelas mesmas ferramentas empresariais, parece que nem para todos os casos as mais bem-sucedidas, convenientes e interessantes, e você entende que terá para agravar uma tonelada de tal legado... Freqüentemente, os relacionamentos na equipe não se desenvolvem da melhor maneira para você e você não obtém compreensão e feedback reais, dirige de colegas ... ou para mim, talvez - um campo relacionado], IMHO, não é apenas uma qualidade importante de um profissional, mas, de fato, ajuda o desenvolvedor a sobreviver no capitalismo, permanecendo não só externamente em demanda, competitivo com os jovens que avançam nos calcanhares, mas, acima de tudo, dando energia e movimento por dentro. Às vezes você ouve algo como: "mas meu ex disse que se fosse possível não codificar, ele não codificaria!". Sim, e os jovens de hoje perceberam que na situação de hoje, "honestamente e normalmente" você só ganha em TI, e eles já estão no meio da multidão na porta do departamento de RH ... Não sei,Eu gosto de programar desde a infância, mas quero codificar algo, se não for útil, pelo menos interessante. Em suma, estou longe de ser um jogador, mas na minha vida houve vários curtos períodos em que vergonhosamente "desperdicei". Sim, a própria paixão por computadores na infância começou, é claro, com jogos. Lembro-me de como nos anos 90 o Spectrum foi trazido para a cidade. Muitas vezes não havia praticamente nada para comer na época, mas meu pai ainda pegava o último dinheiro do estoque, foi, defendeu uma fila enorme sem precedentes e comprou para mim e meu irmão nosso primeiro carro milagroso. Nós o conectamos por meio de um cabo com conectores SG-5 a uma TV Record preto e branco, a imagem tremeu e piscou, os jogos tiveram que ser pacientemente carregados na RAM de um gravador de fita cassete antigo [ainda ouço sons de carregamento venenosos], muitas vezes apresentando falhas ..Apesar do fato de que os primeiros programadores e designers conseguiram colocar mundos inteiros com jogabilidade incrível com seu código em 48 kilobytes de RAM, rapidamente me cansei de jogar e me empolguei com a programação em BASIC), desenhei gráficos sprite (e vetor "tridimensional" então também era, nós até compramos um livro complicado), escrevia música simples no editor ... Então, há algum tempo me cansei de tudo de novo, era um inverno pandêmico e eu não conseguia andar uma bicicleta, o grupo de rock não ensaiou ... Eu li os fóruns e me propus a vários jogos populares mais ou menos novos feitos no Unity ou no Unreal Engine, obviamente. Gosto de jogos RPG de sobrevivência em mundos abertos, só isso ... Depois do trabalho, comecei a mergulhar em mundos virtuais todas as noites e fazer hack-swing, mas não durou muito. Os jogos são todos semelhantes na mecânica,A jogabilidade monótona é espalhada por um pequeno enredo em um monte de tarefas semelhantes com batalhas intermináveis ​​... Mas, o engraçado é que ele realmente fica vergonhosamente atrasado em mecanismos importantes. Os produtos comerciais que são vendidos por dinheiro estão atrasados ​​... E qualquer "bug", IMHO, é uma grande decepção - traz instantaneamente um conto de fadas digital do ambiente virtual para o mundo real ... Claro, gráficos excelentes, muito legal desenhado. Mas, exagerando, percebi que todas essas habilidades em motores corporativos, na verdade, nem mesmo codificam. São montados por gestores e designers, simplesmente “brincando com a cor dos cubos”, mas os próprios cubos, ao mesmo tempo, praticamente “não mudam” ... Em geral, quando ficava completamente chato, pensei que "Eu também posso fazer isso", mas direto no navegadorOs produtos comerciais que são vendidos por dinheiro estão atrasados ​​... E qualquer "bug", IMHO, é uma grande decepção - traz instantaneamente um conto de fadas digital do ambiente virtual para o mundo real ... Claro, gráficos excelentes, muito legal desenhado. Mas, exagerando, percebi que todas essas habilidades em motores corporativos, na verdade, nem mesmo codificam. São montados por gestores e designers, simplesmente “brincando com a cor dos cubos”, mas os próprios cubos, ao mesmo tempo, praticamente “não mudam” ... Em geral, quando ficava completamente chato, pensei que "Eu também posso fazer isso", mas direto no navegadorOs produtos comerciais que são vendidos por dinheiro estão atrasados ​​... E qualquer "bug", IMHO, é uma grande decepção - traz instantaneamente um conto de fadas digital do ambiente virtual para o mundo real ... Claro, gráficos excelentes, muito legal desenhado. Mas, exagerando, percebi que todas essas habilidades em motores corporativos, na verdade, nem mesmo codificam. São montados por gestores e designers, simplesmente “brincando com a cor dos cubos”, mas os próprios cubos, ao mesmo tempo, praticamente “não mudam” ... Em geral, quando ficava completamente chato, pensei que "Eu também posso fazer isso", mas direto no navegadorSão montados por gestores e designers, simplesmente “brincando com a cor dos cubos”, mas os próprios cubos, ao mesmo tempo, praticamente “não mudam” ... Em geral, quando ficava completamente chato, pensei que "Eu também posso fazer isso", mas direto no navegadorSão montados por gestores e designers, simplesmente “brincando com a cor dos cubos”, mas os próprios cubos, ao mesmo tempo, praticamente “não mudam” ... Em geral, quando ficava completamente chato, pensei que "Eu também posso fazer isso", mas direto no navegadornojento, não tem a intenção de salvar a memória de javascript de programação sério. Por fim, decidi cumprir cabalmente o facto de o tempo todo com um look smart repito ao meu filho: “saber fazer jogos é muito mais interessante do que os jogar”. Resumindo, comecei a escrever meu próprio atirador FPS baseado em navegador personalizado baseado em tecnologias abertas.





Então, no momento, o primeiro resultado para esta longa "tarefa para si mesmo" - você pode testar: http://robot-game.ru/





Pilha e arquitetura

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



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



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





Modelo de drone aranha no Blender
- Blender

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



window.location.reload(true);







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






Todas as texturas usadas no jogo
Desempenho

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



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





. . , .





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





, . , , . , , . ))



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





Painel

E :





A história do futuro dentro

. « », .





. — .





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





Flores e garrafas

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





— , ( — ) , — .





Níveis de dificuldade

, :





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





  • — — : — . .





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





  • , — .





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





Erro

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





Visão ótica da hélice de vinho
Disparou

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





. : . , , . , . .





Primeiro modelo de localização

:





  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-objeto de porta
-
A porta não fecha

— — — — . . )





, — . « ».





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



. — () (). — .





Auxiliares de pseudo-objetos para itens
-

«» — :





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



. :





Depuração com os assistentes de seta 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 : , , — .





Se você cair da parede e correr pela borda do céu

, : .





( 10 ) . . — , .





Vidro a prova de balas

, React c TS !

FPS Three:









  •  





  • Em todos os outros aspectos possíveis, devemos otimizar o ciclo de animação, elenco e cálculo de colisões nele no contexto da jogabilidade o mais cuidadosamente possível, de modo a manter a unidade, mas evitar uma queda no desempenho.





  • A tipagem estática e os testes de unidade não ajudam neste experimento.





Em princípio, estou satisfeito com o que já aconteceu. E eu quero trazê-lo para a beleza total. Portanto, se você conhece alguém que adora animação esquelética e pode concordar em adicionar algumas faixas simples ao meu glb - por favor, jogue fora o link do artigo para ele.








All Articles