3Dブラウザベースのフットボールをどのように書いたか。 パート1

こんにちは、Habr! 3Dブラウザベースのフットボールをどのように書いたかについての話を共有したいと思います。 それはすべて、私の夫がサッカーを愛しているという事実から始まりました。 彼は放送を見たり、ゲームに行ったり、電話で遊んだりします。 それで、彼を驚かせ、少なくともゲームで彼をデバイスから引き離すために、私は自分のゲームを書くことにしました。







カットの下で、TypeScriptとThree.jsがどのように友人であり、どのようになったかを説明します。



テクノロジーの選択について少し



Three.jsライブラリーの経験はすでにあるので、今回は3Dグラフィックスを操作するために使用することにしました。



TypeScriptは使用することに決めました。



環境設定



環境のセットアップに関するいくつかの言葉。 これはゲーム自体の開発に直接関係するものではありませんが、念のために、プロジェクトのアセンブリの構成について簡単に説明します。誰かに役立つかもしれません。



最初に:



$ npm init
      
      





npmパッケージを初期化し、package.jsonファイルを作成します。



package.jsonでは、スクリプトブロックが構成されます。これは、この方法で後で起動できる一連のスクリプトです。



 $ npm run <SCRIPT_NAME>
      
      





スクリプトのセットは次のとおりです。



 "scripts": { "clean": "rm -rf ./tmp ./dist", "copy": "./bin/copy", "ts": "./node_modules/.bin/tsc", "requirejs": "./bin/requirejs", "js": "npm run ts && npm run requirejs", "css": "./bin/compile-css", "build": "npm run clean && npm run js && npm run css && npm run copy", "server": "./node_modules/.bin/http-server ./dist", "dev": "./bin/watcher & npm run server" }
      
      





したがって:





いくつかの実行可能ファイル:



bin / compile-css-必要に応じてdist / cssディレクトリを作成し、スタイラススタイルのコンパイルを開始します。



bin / compile-css
 #!/usr/bin/env bash if [ ! -d ./dist/css ]; then mkdir -p ./dist/css fi ./node_modules/.bin/stylus ./src/styles/index.styl -o ./dist/css/styles.css
      
      







bin / copy-必要に応じて必要なディレクトリを作成し、node_modules、htmlファイル、およびリソースから依存関係をコピーします。



ビン/コピー
 #!/usr/bin/env bash cp ./src/*.html ./dist if [ ! -d ./dist/js/libs ]; then mkdir -p ./dist/js/libs fi if [ ! -d ./dist/js/libs/three/loaders ]; then mkdir -p ./dist/js/libs/three/loaders fi cp ./node_modules/three/build/three.js ./dist/js/libs/three.js cp -r ./node_modules/three/examples/js/loaders/sea3d ./dist/js/libs/three/loaders/sea3d cp -r ./node_modules/three/examples/js/loaders/TDSLoader.js ./dist/js/libs/three/loaders/TDSLoader.js cp -r ./src/resources ./dist/resources
      
      







bin / requirejs -jsファイルを1つのバンドルに収集します。



bin / requirejs
 #!/usr/bin/env node const requirejs = require('requirejs'); const config = { baseUrl: "tmp/js", dir: "./dist/js", optimize: 'none', preserveLicenseComments: false, generateSourceMaps: false, wrap: { startFile: './node_modules/requirejs/require.js' }, modules: [ { name: 'football' } ] }; requirejs.optimize(config, function (results) { console.log(results); });
      
      







最初の問題



最初の問題は、依存関係をインストールし、typescriptのコンパイルを開始する段階ですでに待機しています。



Three.jsとTypeSriptを依存関係として設定することにより:



 $ npm install three --save $ npm install typescript --save-dev
      
      





Three.jsの既製のtaipsがあるかどうかを確認することは論理的なステップのように思えました。 - @ types / 3があることが判明しました。 そして、私はそれらをインストールするために急いだ:



 $ npm install @types/three --save-dev
      
      





しかし、判明したように、これらのタイピングは非常に高品質ではないことが判明し、コンパイルが開始されたときに、すぐに次のような多くの同様のエラーが発生しました:



 $ npm run ts ... node_modules/@types/three/three-core.d.ts(1611,32): error TS2503: Cannot find namespace 'THREE'.
      
      





node_modules/@types/three/index.d.tsを見ると、次のようなものが見つかりました。



 export * from "./three-core"; export * from "./three-canvasrenderer"; ... export * from "./three-vreffect"; export as namespace THREE;
      
      





すなわち 最初はすべての内部記述が接続され、その後、これらすべてがTHREE名前空間によって宣言され、外部にエクスポートされます。 しかし、同時に、最初のインクルードでは、three-core.d.tsで3つのスペースがすでに使用されていますが、これは後で発表されます。



誰かのためにどのように機能したかは不明です(誰かがこれをすべてコミットしました)。



typescriptの以前のバージョンでは名前空間に「遡及効果」があり、現在のバージョンへのこのような浪費を放棄することを決めましたが、以前のバージョンへの一貫したロールバックは結果をもたらしませんでした。



次に、 three-core.d.tsTHREEが使用されている場所を正確に確認することにしました。結局のところ、すべての使用は2つの隣接する方法に集中していました。



 /** * Calls before rendering object */ onBeforeRender: (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, geometry: THREE.Geometry | THREE.BufferGeometry, material: THREE.Material, group: THREE.Group) => void; /** * Calls after rendering object */ onAfterRender: (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, geometry: THREE.Geometry | THREE.BufferGeometry, material: THREE.Material, group: THREE.Group) => void;
      
      





同時に、 THREE名前空間で指定されたすべてのタイプは、すぐそこにthree-core.d.tsで記述されました。 これは、それらを使用するために、ネームスペースまたは追加のインポートが必要ないことを意味します。 THREEを削除し、再度コンパイルを開始しました-出来上がり、コンパイルは成功しました。



ライト、カメラ、 モーター



光源とカメラは、3Dシーンの不可欠な部分です。 もちろん、これも作成する必要があります。



 import { Camera, Scene } from 'three'; export class App { protected scene: Scene; protected camera: Camera; constructor() { this.createScene(); this.createCamera(); this.createLight(); } protected createScene() { this.scene = new THREE.Scene(); } protected createCamera() { this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); } protected createLight() { const ambient = new THREE.AmbientLight(0xffffff); this.scene.add(ambient); } }
      
      





また、描画用のキャンバスを作成し、ドキュメントに追加してフルスクリーンに拡大する必要があります。



 ... protected renderer: WebGLRenderer; ... protected createRenderer() { this.renderer = new THREE.WebGLRenderer(); this.updateRendererSize(); document.body.appendChild(this.renderer.domElement); } protected updateRendererSize() { this.renderer.setSize(window.innerWidth, window.innerHeight); }
      
      





そして、コンストラクターでcreateRendererを呼び出します。



 ... constructor() { this.createRenderer(); }
      
      





さて、開始シーンのセットアップの最後のタッチは再描画です:



 constructor() { this.animate(); } protected animate() { window.requestAnimationFrame(() => this.animate()); this.renderer.render(this.scene, this.camera); }
      
      





競技場



シーンを準備したら、サッカーに直接関連するオブジェクトの追加を開始できます。 そして、私はフィールドから始めるのが論理的に思えました。



このフィールドのテクスチャは、インターネット上で問題なく見つかりました(これは3Dモデルについては言えませんが、以下で詳しく説明します)。











field.ts



 import { BASE_URL } from './const'; import { Scene, Texture } from 'three'; export const FIELD_WIDTH = 70; export const FIELD_HEIGHT = 15; export class Field { protected scene: Scene; constructor(scene: Scene) { this.scene = scene; const loader = new THREE.TextureLoader(); loader.load(`${ BASE_URL }/resources/textures/field.jpg`, (texture: Texture) => { const material = new THREE.MeshBasicMaterial({ map: texture }); const geometry = new THREE.PlaneGeometry(FIELD_HEIGHT, FIELD_WIDTH); const plane = new THREE.Mesh(geometry, material); plane.rotateX(-90 * Math.PI / 180); plane.rotateZ(90 * Math.PI / 180); this.scene.add(plane); }); } }
      
      





ご覧のとおり、最初にテクスチャがロードされ、次にPlaneGeometryクラスのオブジェクトが作成され 、このテクスチャがそれに重ねられます。 その後、オブジェクトはX軸とZ軸を中心に少し回転します。



その結果、次の図が得られます。









フィールドにゴールがない場合、サッカーは機能しません。 したがって、私はインターネット上でサッカーのゴールの無料3Dモデルを見つけ、2個のゴールオブジェクトを作成してシーンに追加する次のステップを決定しました。 しかし、それから不愉快な驚きが私を待っていました。そして、それについて小さな叙情的な余談が言います。



叙情的な余談



突然、自分にとって適切な3Dモデルを見つけるのは簡単ではないことがわかりました。 最も適切なモデルは支払われることが判明し、かなりの費用がかかりました(私の意見では)。 そして、不幸なサッカーのゴールを探すのにかなりの時間が費やされました。 もちろん、私は何でもすべてを無料で配布することを求めていませんが、ソフトウェア開発の分野では、無料のオープンソースソフトウェアの巨大な層があり、1つのgithubに価値があります。 無料のオーディオ、写真、その他多くの種類のファイルも、原則として見つけるのは難しくありません。 これらのすべての分野で、無料のアナログが何らかの形で商業的なオファーに負けてしまう可能性があります(そして、ある意味で勝ちます)が、少なくとも存在し、それらを見つけることは難しくありません。 3Dモデリングの分野について言えないこと。



おそらく私はいくつかの詳細を見逃しているか、3Dモデリングについて何も知らないので、すぐにすべてのiにドットを付けて、無料のモデルが非常に少ない理由や、見つけるのが難しい、および/または品質が著しく劣る理由を説明します。 コメントで別の視点を聞いてうれしいです。



ゲーム全体で、目標、選手、ボールのモデルを見つける必要がありました。 また、概算では、開発に費やした時間の20〜30%が適切なモデルの検索に費やされました。



新しい門への雄羊のように



しかし、私たちの羊に戻るか、むしろ門に戻ります。 それにもかかわらず、必要なモデルが見つかりました。これにより、門のクラスを実現できました。



Gate.ts



 import { BASE_URL } from './const'; import { Mesh, Object3D } from 'three'; export class Gate extends FootballObject { protected mesh: Mesh; load() { return new Promise((resolve, reject) => { const loader = new THREE.TDSLoader(); loader.load(`${ BASE_URL }/resources/models/gate.3ds`, (object: Object3D) => { this.mesh = new THREE.Mesh((<Mesh> object.children[0]).geometry, new THREE.MeshBasicMaterial({color: 0xFFFFFF})); this.mesh.scale.set(.15, .15, .15); this.scene.add(this.mesh); resolve(); }); }); } }
      
      





ゲートのサイズが私たちに合うように、私たちはそれらを少し絞らなければなりませんでした。



 this.mesh.scale.set(.15, .15, .15);
      
      





特に注意深い読者は、 GateクラスがFootballObjectクラスを継承していることに気付くかもしれませんが、その実装は提供されていません。 この重大な不正を直ちに排除します。



Object.ts



 import { Mesh, Scene } from 'three'; export abstract class FootballObject { protected abstract mesh: Mesh; protected scene: Scene; constructor(scene: Scene) { this.scene = scene; } setPositionX(x: number) { this.mesh.position.x = x; } setPositionY(y: number) { this.mesh.position.y = y; } setPositionZ(z: number) { this.mesh.position.z = z; } getPositionX(): number { return this.mesh.position.x; } getPositionY(): number { return this.mesh.position.y; } getPositionZ(): number { return this.mesh.position.z; } setRotateX(angle: number) { this.mesh.rotateX(angle * Math.PI / 180); } setRotateY(angle: number) { this.mesh.rotateY(angle * Math.PI / 180); } setRotateZ(angle: number) { this.mesh.rotateZ(angle * Math.PI / 180); } }
      
      





その後、クラスPlayer (players)およびBall (ball)もFootballObjectから継承されます。これには、ステージ上の位置を設定し、度で指定された特定の角度で回転するメソッドの実装が含まれます。



その後、ゲートオブジェクトを作成し、目的の座標に配置します。



app.ts



 ... import { Field, FIELD_HEIGHT, FIELD_WIDTH } from './field'; import { Gate } from './gate'; class App { ... protected leftGate: Gate; protected rightGate: Gate; ... constructor() { ... this.createGates(); } ... protected createGates() { const DELTA_X = 2; this.leftGate = new Gate(this.scene); this.rightGate = new Gate(this.scene); this.leftGate.load() .then(() => { this.leftGate.setPositionX(- FIELD_WIDTH / 2 + DELTA_X); this.leftGate.setPositionY(2); this.leftGate.setRotateX(-90); this.leftGate.setRotateZ(180); }); this.rightGate.load() .then(() => { this.rightGate.setPositionX(FIELD_WIDTH / 2 - DELTA_X); this.rightGate.setPositionY(2); this.rightGate.setRotateX(-90); }); } }
      
      





DELTA_X-特定のオフセット。これにより、目標の座標を明確にするために、目標の座標を調整する必要がありました。



ご覧のとおり、左のゲートはフィールドの半分を負の方向(つまり、左)に、右のゲートはフィールドの同じ半分の正の方向(つまり、右)にシフトしています。

両方のモデルが回転して、フィールド上の自然な位置を取得します。

結果はこの写真です:









当初、これをいくつかの記事に拡張する予定はありませんでしたが、どういうわけか膨大なものになることがわかったので、おそらくこの素晴らしいメモで、「自家製」サッカーに関する記事の最初の部分を完成させます。



第二部では、チームとプレーヤーの作成、フィールドへの配置と戦略について説明します。



陰謀を終わらせないために、記事の最後の部分でソースとデモをレイアウトします。



ご清聴ありがとうございました!



All Articles