Three.js、Vue、Blenderを使用してブラウザベースの3DFPSシューティングゲームを作成した方法

ゲーム開始画面
ゲーム開始画面

動機

すべての商用開発者(コーダーだけでなく、たとえばデザイナーも)の途中で、遅かれ早かれ沼沢地、鈍い暗い場所に出くわし、そこをさまよって、一般的にプロの燃え尽き症候群の死んだ砂漠に迷い込むことができますおよび/またはピルの予約のために心理療法士にさえ。ビジネス雇用者は明らかにあなたの最も発達したスキルを最大限に絞り出し、ほとんどの欠員のスタックは同じエンタープライズツールで占められています、すべての場合で最も成功し、便利で興味深いとは限らないようです、そしてあなたはあなたが持っていることを理解していますそのような遺産のトン悪化させるために...多くの場合、チーム内の関係はあなたにとって最善の方法で発展せず、あなたは本当の理解とフィードバックを得られず、同僚から駆り立てられます...または私自身にとっては、おそらく-関連分野]、私見はそうではありません専門家の重要な資質ですが、実際には、開発者が資本主義で生き残るのに役立ち、外部からの需要を維持し、後を追う若者と競争するだけでなく、何よりも、内部からエネルギーと動きを与えます。 「しかし、私の元は、コーディングしないことが可能であれば、彼はコーディングしないだろうと言っていました!」のようなことを時々耳にします。はい、そして今日の若者は、今日の状況では「正直にそして普通に」あなたはITでしか稼げないことに気づきました、そして彼らはすでに人事部門の玄関口で群衆の中に立っています...私は知りません、私は子供の頃からコーディングが好きでしたが、役に立たないとしても、少なくとも面白いものをコーディングしたいと思っています。要するに、私はゲーマーからはほど遠いのですが、私の人生の中で、恥ずかしそうに「無駄に」した短い期間がいくつかありました。はい、もちろん、子供の頃のコンピューターへの情熱はゲームから始まりました。 90年代にスペクトラムがどのように街にもたらされたかを覚えています。当時はほとんど何も食べられなかったのですが、父はそれでも最後のお金を隠し場所から取り出し、前例のない巨大な列を守り、兄と私に最初の奇跡の車を買いました。 SG-5コネクタ付きのコードを介して白黒のレコールTVに接続し、画像が揺れて点滅し、ゲームを古いカセットレコーダーからRAMに辛抱強くロードする必要がありました[まだ有毒なロード音が聞こえます]、しばしば障害が発生しました...。初期のプログラマーやデザイナーは、48キロバイトのRAMにコードを入れて素晴らしいゲームプレイで全世界を配置することができましたが、すぐにプレイに飽きて、BASICでのプログラミングに夢中になりました))、スプライトグラフィックス(およびベクトル「立体」もそうだったし、複雑な本も買って)、エディターで簡単な音楽を書いていた…だから、しばらく前にまた飽きて、パンデミックな冬だったので乗れなかった。バイク、ロックグループはリハーサルをしませんでした...私はフォーラムを読んで、明らかに、UnityまたはUnrealEngineで作成された多かれ少なかれ新鮮な人気のゲームをいくつか設定しました。私はRPG-オープンワールド-サバイバルゲームが好きです、それだけです...仕事の後、私は毎晩仮想世界に飛び込み、ハックスイングを始めましたが、それは長くは続きませんでした。ゲームの仕組みはすべて似ていますが、単調なゲームプレイは、小さなプロットにまみれて、無限の戦いを伴う同様のタスクの束になります...しかし、面白いことに、それは本当に恥知らずに重要なメカニズムに遅れをとっています。お金で売られている商品は遅れています...そして、どんな「バグ」、IMHOも、大きな失望です-それは、仮想環境から現実の世界にデジタルのおとぎ話を即座にもたらします...もちろん、優れたグラフィックス、とてもかっこいいです。しかし、誇張して、エンタープライズエンジンのこれらすべての技術は、実際にはコーディングすらしていないことに気づきました。マネージャーやデザイナーが組み立てるのは、単に「立方体の色で遊ぶ」だけですが、同時に、立方体自体は実質的に「変わらない」…一般的に、完全に退屈になったとき、私は「私もそれができます」が、ブラウザでお金で売られている商品は遅れています...そして、どんな「バグ」、IMHOも、大きな失望です-それは、仮想環境から現実の世界にデジタルのおとぎ話を即座にもたらします...もちろん、優れたグラフィックス、とてもかっこいいです。しかし、誇張して、エンタープライズエンジンのこれらすべての技術は、実際にはコーディングすらしていないことに気づきました。マネージャーやデザイナーが組み立てるのは、単に「立方体の色で遊ぶ」だけですが、同時に、立方体自体は実質的に「変わらない」…一般的に、完全に退屈になったとき、私は「私もそれができます」が、ブラウザでお金で売られている商品は遅れています...そして、どんな「バグ」、IMHOも、大きな失望です-それは、仮想環境から現実の世界にデジタルのおとぎ話を即座にもたらします...もちろん、優れたグラフィックス、とてもかっこいいです。しかし、誇張して、エンタープライズエンジンのこれらすべての技術は、実際にはコーディングすらしていないことに気づきました。マネージャーやデザイナーが組み立てるのは、単に「立方体の色で遊ぶ」だけですが、同時に、立方体自体は実質的に「変わらない」…一般的に、完全に退屈になったとき、私は「私もそれができます」が、ブラウザでマネージャーやデザイナーが組み立てるのは、単に「立方体の色で遊ぶ」だけですが、同時に、立方体自体は実質的に「変わらない」…一般的に、完全に退屈になったとき、私は「私もそれができます」が、ブラウザでマネージャーやデザイナーが組み立てるのは、単に「立方体の色で遊ぶ」だけですが、同時に、立方体自体は実質的に「変わらない」…一般的に、完全に退屈になったとき、私は「私もそれができます」が、ブラウザで深刻なプログラミングJavaScriptのメモリ節約することを意図していない嫌な最後に、私はいつもスマートな表情で息子に繰り返すという事実に完全に従うことにしました。「ゲームを作ることは、ゲームをすることよりもはるかに面白い」。要するに、私はオープンテクノロジーに基づいた独自のカスタムブラウザベースのFPSシューティングゲームを書き始めました。





したがって、現時点では、この長時間の「自分のためのタスク」の最初の結果-テストできます:http//robot-game.ru/





スタックとアーキテクチャ

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



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



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





Blenderのスパイダードローンモデル
- Blender

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



window.location.reload(true);







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






ゲームで使用されるすべてのテクスチャ
パフォーマンス

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



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





. . , .





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





, . , , . , , . ))



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





ダッシュボード

E :





内部の未来の物語

. « », .





. — .





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





花とボトル

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





— , ( — ) , — .





難易度

, :





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





  • — — : — . .





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





  • , — .





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





エラー

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





ワインプロペラの光学的光景
撃ちます

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





. : . , , . , . .





最初のロケーションモデル

:





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





ドア疑似オブジェクト
-
ドアが閉まらない

— — — — . . )





, — . « ».





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



. — () (). — .





アイテムの疑似オブジェクトヘルパー
-

«» — :





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



. :





ArrowWizardsを有効にしてデバッグする

« » — :





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





壁に倒れて空の端を走ったら

, : .





( 10 ) . . — , .





防弾ガラス

, React c TS !

FPS Three:









  •  





  • 他のすべての可能な側面では、ドライブを維持しながらパフォーマンスの低下を回避するために、ゲームプレイのコンテキストでアニメーションサイクル、キャスト、および衝突の計算を可能な限り慎重に最適化する必要があります。





  • この実験では、静的型付けと単体テストは役に立ちません。





原則として、私はすでに起こったことに満足しています。そして、私はそれを完全に美しくしたいと思っています。したがって、スケルタルアニメーションが好きで、私のグラブにいくつかの簡単なトラックを追加することに同意するかもしれない誰かを知っているなら、彼のために記事へのリンクを捨ててください。








All Articles