Wie ich einen browserbasierten 3D-FPS-Shooter mit Three.js, Vue und Blender geschrieben habe

Spielstartbildschirm
Spielstartbildschirm

Motivation

Auf dem Weg eines jeden kommerziellen Entwicklers (nicht nur Programmierer, sondern, wie ich weiß, auch Designer) stoßen sie früher oder später auf sumpfige Gebiete, stumpfe, düstere Orte, auf denen man im Allgemeinen in die tote Wüste des professionellen Burnouts wandern kann und / oder sogar zu einem Psychotherapeuten für einen Termin für Pillen. Business-Arbeitgeber nutzen offensichtlich Ihre am weitesten entwickelten Fähigkeiten, um das Maximum herauszuholen. Der Stapel der meisten offenen Stellen wird von denselben Unternehmenstools belegt. Es scheint, dass nicht in allen Fällen die erfolgreichsten, bequemsten und interessantesten sind, und Sie verstehen, dass Sie diese haben werden eine Tonne solchen Erbes zu verschlimmern... Oft entwickeln sich die Beziehungen im Team nicht optimal für Sie, und Sie erhalten kein wirkliches Verständnis und Feedback, fahren nicht von Kollegen ... oder für mich selbst - vielleicht ein verwandtes Gebiet], IMHO, nicht Nur eine wichtige Eigenschaft eines Profis, die dem Entwickler jedoch hilft, im Kapitalismus zu überleben. Er bleibt nicht nur extern gefragt, sondern konkurriert mit jungen Menschen, die auf den Fersen sind, sondern gibt vor allem Energie und Bewegung von innen heraus. Manchmal hört man so etwas wie: "Aber mein Ex sagte, wenn es möglich wäre, nicht zu codieren, würde er nicht codieren!". Ja, und die jungen Leute von heute haben erkannt, dass man in der heutigen Situation "ehrlich und normal" nur in der IT verdienen kann, und sie stehen bereits in einer Menschenmenge vor der Haustür der Personalabteilung ... Ich weiß nicht,Ich habe das Codieren seit meiner Kindheit gemocht, aber ich möchte etwas codieren, wenn es nicht nützlich ist, dann zumindest interessant. Kurz gesagt, ich bin weit entfernt von einem Spieler, aber in meinem Leben gab es einige kurze Zeiträume, in denen ich schändlich "verschwendet" habe. Ja, die Leidenschaft für Computer in der Kindheit begann natürlich mit Spielen. Ich erinnere mich, wie in den neunziger Jahren das Spektrum in die Stadt gebracht wurde. Damals gab es oft praktisch nichts zu essen, aber mein Vater nahm immer noch das letzte Geld aus dem Vorrat, ging, verteidigte eine beispiellos große Schlange und kaufte meinem Bruder und mir unser erstes Wunderauto. Wir haben es über ein Kabel mit SG-5-Anschlüssen an einen Schwarzweiß-Plattenfernseher angeschlossen, das Bild zitterte und blinzelte, Spiele mussten geduldig von einem alten Kassettenrekorder in den RAM geladen werden [ich höre immer noch giftige Ladegeräusche], wobei häufig Fehler auftraten ...Trotz der Tatsache, dass frühe Programmierer und Designer es geschafft haben, ganze Welten mit erstaunlichem Gameplay mit ihrem Code in 48 Kilobyte RAM zu platzieren, wurde ich schnell müde zu spielen und ich wurde mit dem Programmieren in BASIC mitgerissen)), zeichnete ich Sprite-Grafiken (und Vektoren) "dreidimensional" war damals auch, wir kauften sogar ein kompliziertes Buch), schrieben einfache Musik im Editor ... Vor einiger Zeit hatte ich wieder alles satt, es war ein Pandemiewinter und ich konnte nicht reiten Ein Fahrrad, das die Rockgruppe nicht geprobt hat ... Ich habe die Foren gelesen und mir offensichtlich einige mehr oder weniger frische, beliebte Spiele auf Unity oder Unreal Engine ausgedacht. Ich mag RPG-Open-Worlds-Survival-Spiele, das ist alles ... Nach der Arbeit begann ich jeden Abend in virtuelle Welten einzutauchen und zu hacken, aber es dauerte nicht lange. Die Spiele sind alle in der Mechanik ähnlich,Das monotone Gameplay wird über eine kleine Handlung in eine Reihe ähnlicher Aufgaben mit endlosen Schlachten verschmiert ... Aber das Lustige ist, dass es in wichtigen Mechaniken wirklich schamlos zurückbleibt. Kommerzielle Produkte, die für Geld verkauft werden, bleiben zurück ... Und jeder "Bug", IMHO, ist eine starke Enttäuschung - er bringt sofort ein digitales Märchen aus der virtuellen Umgebung in die reale Welt ... Natürlich exzellente Grafik, sehr cool gezeichnet. Aber übertrieben habe ich festgestellt, dass all diese Handwerke auf Enterprise-Engines tatsächlich nicht einmal codieren. Sie werden von Managern und Designern zusammengestellt und spielen einfach "mit der Farbe der Würfel", aber die Würfel selbst ändern sich praktisch nicht "... Im Allgemeinen dachte ich das, als es völlig langweilig wurde "Das kann ich auch", aber direkt im Browser anKommerzielle Produkte, die für Geld verkauft werden, bleiben zurück ... Und jeder "Bug", IMHO, ist eine starke Enttäuschung - er bringt sofort ein digitales Märchen aus der virtuellen Umgebung in die reale Welt ... Natürlich exzellente Grafik, sehr cool gezeichnet. Aber übertrieben habe ich festgestellt, dass all diese Handwerke auf Enterprise-Engines tatsächlich nicht einmal codieren. Sie werden von Managern und Designern zusammengestellt und spielen einfach "mit der Farbe der Würfel", aber die Würfel selbst ändern sich praktisch nicht "... Im Allgemeinen dachte ich das, als es völlig langweilig wurde "Das kann ich auch", aber direkt im Browser anKommerzielle Produkte, die für Geld verkauft werden, bleiben zurück ... Und jeder "Bug", IMHO, ist eine starke Enttäuschung - er bringt sofort ein digitales Märchen aus der virtuellen Umgebung in die reale Welt ... Natürlich exzellente Grafik, sehr cool gezeichnet. Aber übertrieben wurde mir klar, dass all diese Handwerke auf Enterprise-Engines tatsächlich nicht einmal codieren. Sie werden von Managern und Designern zusammengestellt und spielen einfach "mit der Farbe der Würfel", aber die Würfel selbst ändern sich praktisch nicht "... Im Allgemeinen dachte ich das, als es völlig langweilig wurde "Das kann ich auch", aber direkt im Browser anSie werden von Managern und Designern zusammengestellt und spielen einfach "mit der Farbe der Würfel", aber die Würfel selbst ändern sich praktisch nicht "... Im Allgemeinen dachte ich das, als es völlig langweilig wurde "Das kann ich auch", aber direkt im Browser anSie werden von Managern und Designern zusammengestellt und spielen einfach "mit der Farbe der Würfel", aber die Würfel selbst ändern sich praktisch nicht "... Im Allgemeinen dachte ich das, als es völlig langweilig wurde "Das kann ich auch", aber direkt im Browser anEkelhaft, nicht dazu gedacht, Speicher für ernsthaftes Programmier-Javascript zu speichern. Schließlich habe ich mich entschlossen, der Tatsache voll und ganz nachzukommen, dass ich meinem Sohn immer wieder mit einem klugen Blick wiederhole: „Spiele machen zu können ist viel interessanter als sie zu spielen“. Kurz gesagt, ich habe mich vorgenommen, meinen eigenen browserbasierten FPS-Shooter zu schreiben, der auf offenen Technologien basiert.





Im Moment also das erste Ergebnis für diese langjährige "Aufgabe für sich selbst" - Sie können testen: http://robot-game.ru/





Stapel und Architektur

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



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



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





Spinnen-Drohnen-Modell in Blender
- Blender

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



window.location.reload(true);







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






Alle im Spiel verwendeten Texturen
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, «-, », . — - -: ... , , « »…



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





. . , .





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





, . , , . , , . ))



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





Instrumententafel

E :





Die Geschichte der Zukunft im Inneren

. « », .





. — .





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





Blumen und Flaschen

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





— , ( — ) , — .





Schwierigkeitsgrad

, :





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





  • — — : — . .





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





  • , — .





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





Optischer Anblick des Weinpropellers
Aufgeschossen

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





. : . , , . , . .





Erstes Standortmodell

:





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





TĂĽrpseudoobjekt
-
Die Tür lässt sich nicht schließen

— — — — . . )





, — . « ».





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



. — () (). — .





Pseudoobjekt-Helfer für Gegenstände
-

«» — :





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



. :





Debuggen mit aktivierten Pfeilassistenten

« » — :





//  @/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 : , , — .





Wenn Sie ĂĽber die Mauer fallen und ĂĽber den Rand des Himmels laufen

, : .





( 10 ) . . — , .





Kugelsicheres Glas

, React c TS !

FPS Three:









  •  





  • In allen anderen möglichen Aspekten mĂĽssen wir den Animationszyklus, das Casting und die Berechnung von Kollisionen im Kontext des Gameplays so sorgfältig wie möglich optimieren, um den Antrieb aufrechtzuerhalten, aber einen Leistungsabfall zu vermeiden.





  • Statische Typisierung und Komponententests helfen in diesem Experiment nicht weiter.





Grundsätzlich bin ich zufrieden mit dem, was bereits passiert ist. Und ich möchte es zur vollen Schönheit bringen. Wenn Sie also jemanden kennen, der Skelettanimationen mag und sich bereit erklärt, ein paar einfache Tracks zu meinem glb hinzuzufügen, werfen Sie bitte den Link zum Artikel für ihn weg.








All Articles