母は質問を何と言いましたか!私はこれを自分の奇抜な考えで試して恥ずかしいかもしれません(私が本当にオフの場合は、提案を聞きたいです)。しかし、私がドメインで最近学んだ最も有用なこと(以前はゲームを含んでいたが、今はVFXです)は、抽象インターフェース間の相互作用をデカップリングメカニズムとしてのデータに置き換えることです(そして、最終的に必要な情報の量を減らします)物事やお互いについて、最低限の絶対的な極限まで)。これはまったく正気に聞こえないかもしれません(そして、私はあらゆる種類の不適切な用語を使用している可能性があります)。
それでも、私があなたに合理的に管理可能な仕事を与えるとしましょう。レンダリングするシーンとアニメーションデータを含むこのファイルがあります。ファイル形式をカバーするドキュメントがあります。あなたの唯一の仕事は、ファイルをロードし、パストレースを使用してアニメーションのきれいな画像をレンダリングし、結果を画像ファイルに出力することです。これはかなり小規模なアプリケーションであり、かなり洗練されたレンダラー(数百万ではない)であっても、LOCの数万を超えることはおそらくありません。
このレンダラーには、独自の小さな孤立した世界があります。外界の影響を受けません。独自の複雑さを分離します。このシーンファイルを読み取って結果を画像ファイルに出力することの懸念を超えて、レンダリングだけに集中できます。このプロセスで何か問題が発生した場合、この図には他に何も関与していないので、それはレンダラーにあり、他には何もないことがわかります。
一方、実際には数百万のLOCを含む大きなアニメーションソフトウェアのコンテキストでレンダラーを機能させる必要があるとしましょう。レンダリングに必要なデータを取得するために、合理化され、文書化されたファイル形式を読み取るだけでなく、あらゆる種類の抽象インターフェースを通過して、作業に必要なすべてのデータを取得する必要があります。
突然、あなたのレンダラーはもはやそれ自身の小さな孤立した世界にいなくなりました。これは、とても複雑に感じます。多くのソフトウェアの全体的な設計を、潜在的に多くの可動部品を含む1つの有機的な全体として理解する必要があります。また、ボトルネックやバグのいずれかが発生した場合、メッシュやカメラなどの実装について考える必要がある場合もあります。関数。
機能性と合理化されたデータ
理由の1つは、機能が静的データよりもはるかに複雑であるためです。また、関数呼び出しが静的データの読み取りでは失敗する可能性があるという点で、多くの原因が考えられます。概念的にはレンダリング用に読み取り専用データを取得しているだけの場合でも、これらの関数を呼び出すと発生する可能性のある非常に多くの隠れた副作用があります。変更する理由は他にもたくさんあります。数か月後、まったく同じデータをフェッチしていても、レンダラーの大量のセクションを書き直してそれらの変更に対応する必要がある方法で、メッシュまたはテクスチャインターフェースがパーツを変更または非推奨にする場合があります。レンダラーへのデータ入力はまったく変更されていません(最終的にすべてにアクセスするために必要な機能のみ)。
したがって、可能な場合、合理化されたデータは、システム全体を全体として考える必要をなくし、システムの非常に特定の部分に集中して作成できる種類の非常に優れた分離メカニズムであることがわかりました改善、新機能の追加、物事の修正などです。ソフトウェアを構成するかさばる部分については、非常にI / Oの考え方に従っています。これを入力し、あなたのことを実行し、それを出力し、何十もの抽象インターフェースを経由せずに、途中で無限の関数呼び出しを終了します。そして、それはある程度、関数型プログラミングに似始めています。
したがって、これは1つの戦略にすぎず、すべての人に適用できるとは限りません。そしてもちろん、単独で飛行している場合でも、すべて(データ自体の形式を含む)を維持する必要がありますが、違いは、そのレンダラーに改善を加えるために座っているとき、本当にレンダラーに集中できることです。ほとんどの場合、他には何もありません。それはそれ自体の小さな世界で非常に分離されます-入力が非常に合理化されるために必要なデータと同じくらい分離されます。
そして、私はファイル形式の例を使用しましたが、それは入力のために関心のある合理化されたデータを提供するファイルである必要はありません。インメモリデータベースの場合もあります。私の場合、対象のデータを格納するコンポーネントを備えたエンティティコンポーネントシステムです。それでも、合理化されたデータへのデカップリングのこの基本的な原則を見つけました(ただし、それを行ったとしても)抽象化と、たくさんの、そしてこれらすべての間で行われているたくさんの相互作用を中心とした以前に取り組んだ以前のシステムよりも、私の精神能力への負担ははるかに少ないです1つのことに腰を下ろし、それについてのみ考え、それ以外はほとんど考えられないようにする抽象的なインターフェース。私の脳は、以前のタイプのシステムで危機に瀕していて、爆発することを望んでいました。
デカップリング
大きなコードベースが脳にかかる負担を最小限に抑えたい場合は、ソフトウェアの大きな部分(レンダリングシステム全体、物理システム全体など)が可能な限り最も孤立した世界に存在するようにしてください。最も合理化されたデータを介して、最小限の最小限に至るコミュニケーションと相互作用の量を最小限に抑えます。交換がはるかに分離されたシステムであり、その作業を実行する前に他の何十ものものと通信する必要がない場合は、ある程度の冗長性(プロセッサまたはユーザー自身のための冗長な作業)を受け入れることさえできます。
そして、それを始めると、巨大なものではなく、12の小規模なアプリケーションを維持しているように感じます。そして、それはとても楽しいことでもあります。座って、外の世界を気にすることなく、1つのシステムで思いのままに作業できます。正しいデータを入力し、最後に正しいデータを他のシステムがアクセスできる場所に出力するだけです(その時点で、他のシステムが入力して処理を実行する可能性がありますが、そのことを気にする必要はありません。システムで作業する場合)。もちろん、たとえば、すべてがユーザーインターフェイスにどのように統合されるかについて考える必要があります(まだ、GUIのすべてのデザイン全体について考える必要があります)が、少なくとも、既存のシステムに座って作業するときはそうではありません。または新しいものを追加することを決定します。
おそらく私は、最新のエンジニアリング手法を最新に保つ人々に明白な何かを説明しています。知りません。しかし、それは私には明らかではありませんでした。相互に作用し合うオブジェクトや、大規模なソフトウェアに求められる機能を中心としたソフトウェアの設計に取り組みたかったのです。そして、私がもともと大規模なソフトウェア設計について読んだ本は、実装やデータなどのインターフェース設計に焦点を当てていました(当時のマントラは、実装はそれほど重要ではなく、インターフェースのみです。前者は簡単に交換または置換できるためです。 )。ソフトウェアの相互作用を、この合理化されたデータを除いて、互いにほとんど通信しない巨大なサブシステム間でのデータの入力と出力だけに煮詰めるようなものだと考えることは、最初は直感的ではありませんでした。しかし、そのコンセプトを中心に設計することに焦点を移し始めたとき、それは物事を非常に簡単にしました。頭が爆発することなく、はるかに多くのコードを追加できました。タワーの代わりにショッピングモールを建設しているような気分でした。タワーを追加しすぎた場合、または一部が破損した場合、倒れる可能性があります。
複雑な実装と複雑な相互作用
これは私が言及すべきもう1つの問題です。これは、キャリアの初期のかなりの部分を最も単純な実装を探すために費やしたためです。それで、私は保守性を改善していると考えて、物事を最も小さくて単純な断片に分解しました。
後になって、私はあるタイプの複雑さを別のタイプの交換と交換していることに気付かなかった。すべてを最も単純な部分にまで削減することで、これらの10代の部分の間で行われた相互作用は、関数呼び出しとの最も複雑な相互作用のWebになり、コールスタックの30レベルに達することもあります。そしてもちろん、1つの関数を見ると、それはとても単純で、関数の機能を簡単に理解できます。しかし、各関数はほとんど機能していないので、その時点ではあまり有用な情報は得られません。次に、あらゆる種類の機能をトレースし、あらゆる種類のフープをジャンプして、脳が2つ以上の大きなものを爆発させようとするような方法でそれらがすべて何を行うかを実際に理解する必要があります。
それは神のオブジェクトやそのようなものを示唆するものではありません。しかし、おそらくメッシュオブジェクトを頂点オブジェクト、エッジオブジェクト、面オブジェクトなどの細かいものに分割する必要はありません。たぶん、コードの相互作用を根本的に減らす代わりに、やや複雑な実装を背後に置いて "メッシュ"に保つことができます。適度に複雑な実装をあちこちで処理できます。私は、誰がどこで、どのような順番で知っているかという副作用を伴う、無数の相互作用を処理することができません。
少なくとも私は、脳への負担がはるかに少ないことを発見しました。それは、大規模なコードベースで脳を傷つける相互作用だからです。特別なことはありません。
一般性と特定性
上記に関係しているかもしれませんが、私は一般性とコードの再利用が大好きで、優れたインターフェースを設計する際の最大の課題はさまざまなニーズでさまざまなものに使用されるため、幅広いニーズを満たすことだと考えていました。そして、それを行うと、必然的に一度に100のことを考える必要があります。なぜなら、一度に100のことのニーズのバランスをとろうとしているからです。
物事を一般化するには、非常に時間がかかります。私たちの言語に付属している標準ライブラリを見てください。C ++標準ライブラリにはほとんど機能が含まれていませんが、その設計について議論し、提案を行う人々の委員会全体でチームを維持し、調整する必要があります。これは、その小さな機能が世界中のニーズ全体を処理しようとしているためです。
おそらく、これまでのところは必要ないでしょう。おそらく、インデックス付きメッシュ間の衝突検出にのみ使用される空間インデックスのみを使用しても問題ありません。多分私達は他の種類の表面のために別のものを使用し、レンダリングのために別のものを使用できます。以前はこのような冗長性を排除することに集中していたが、その理由の一部は、さまざまな人々によって実装された非常に非効率的なデータ構造を扱っていたためだった。当然、単に300kの三角形メッシュに1ギガバイトを使用するoctreeがある場合、メモリ内にさらに1つを配置したくありません。
しかし、そもそもなぜオクツリーがそれほど非効率なのでしょうか。ノードごとに4バイトしか使用せず、メガバイト未満でoctreeを作成し、ギガバイトバージョンと同じことを実行しながら、短時間で構築し、より高速な検索クエリを実行できます。その時点で、ある程度の冗長性は完全に受け入れられます。
効率
したがって、これはパフォーマンスが重要なフィールドにのみ関連しますが、メモリ効率のようなものをよりよく取得すればするほど、生産性を優先して、もう少し無駄にすることができます(一般性の低下またはデカップリングと引き換えに、冗長性をもう少し受け入れることができます)。 。また、コードがすでに非常に効率的であり、生産性と引き換えに生産性と引き換えに、効率を犠牲にするだけの余裕があるので、プロファイラーとかなりうまくやり、コンピューターアーキテクチャとメモリ階層について学ぶことができます。重要な領域でさえも、効率性はやや劣りますが、競合他社をしのいでいます。この領域を改善することで、よりシンプルな実装も回避できることがわかりました。
信頼性
これは明白なことですが、言及するかもしれません。最も信頼性の高いものには、最小限の知的オーバーヘッドが必要です。それらについて多くを考える必要はありません。彼らはただ働く。その結果、徹底的なテストを通じて「安定」している(変更する必要がない)超信頼性パーツのリストを大きくするほど、考える必要が少なくなります。
詳細
上記のすべてが私に役立ついくつかの一般的なことをカバーしていますが、あなたの地域のより具体的な側面に移りましょう:
私の小さなプロジェクトでは、プログラムのすべての部分がどのように機能するかについてのメンタルマップを覚えることは簡単です。これにより、変更がプログラムの残りの部分にどのように影響するかを十分に認識し、バグを非常に効果的に回避できるだけでなく、新機能がコードベースにどのように適合するかを正確に確認できます。しかし、より大きなプロジェクトを作成しようとすると、非常に面倒なコードと多数の意図しないバグにつながる優れたメンタルマップを維持することが不可能であることがわかりました。
私にとって、これは複雑な副作用と複雑な制御フローに関連する傾向があります。これはかなり低レベルのビューですが、見栄えの良いすべてのインターフェースと、具体的なものから抽象的なものへの分離によって、複雑な制御フローで発生する複雑な副作用について簡単に推論することはできません。
副作用を単純化/軽減し、制御フローを単純化します。理想的には両方です。そして、一般的に、はるかに大きなシステムが何をするのか、また変更に応じて何が起こるのかを推論するのは非常に簡単です。
この「メンタルマップ」の問題に加えて、自分のコードを他の部分から切り離しておくことは難しいと思います。たとえば、マルチプレーヤーゲームでプレーヤーの動きの物理を処理するクラスとネットワークを処理するクラスがある場合、これらのクラスの1つが他のクラスに依存してプレーヤーの動きデータをネットワークシステムに取得する方法はありませんネットワーク経由で送信します。このカップリングは、優れたメンタルマップを妨げる複雑さの重要な原因です。
概念的には、いくつかのカップリングが必要です。人々がデカップリングについて話すとき、それらは通常、ある種類を別のより望ましい種類に置き換えることを意味します(通常は抽象化に対して)。私にとって、私のドメイン、私の脳の働きなどを考えると、「メンタルマップ」の要件を最小限に抑えるために最も望ましい種類は、上記の合理化されたデータです。1つのブラックボックスが別のブラックボックスに送られるデータを吐き出し、両方がお互いの存在を完全に認識していません。彼らが知っているのは、データが保存されている中央の場所(例:中央のファイルシステムまたは中央のデータベース)だけであり、それらを介して入力をフェッチし、何かを実行して、他のブラックボックスが入力する可能性のある新しい出力を吐き出します。
このようにすると、物理システムは中央データベースに依存し、ネットワーキングシステムは中央データベースに依存しますが、お互いについては知りません。彼らはお互いが存在していることさえ知る必要はないでしょう。彼らは、お互いに抽象的なインターフェースが存在することを知る必要すらありません。
最後に、他のクラスを調整する1つ以上の「マネージャー」クラスを考え出すことがよくあります。たとえば、ゲームでは、クラスがメインのティックループを処理し、ネットワーククラスとプレーヤークラスの更新メソッドを呼び出します。これは、各クラスは他とは独立して単体テスト可能で使用可能でなければならないという私の研究での発見の哲学に反しています。そのようなマネージャークラスは、その目的によってプロジェクトの他のほとんどのクラスに依存しているためです。さらに、プログラムの残りのマネージャークラスオーケストレーションは、メンタルマッピング不可能な複雑さの重要な原因です。
ゲーム内のすべてのシステムを調整するために何かが必要になる傾向があります。Centralはおそらく、完了後にレンダリングシステムを呼び出す物理システムのようなものよりも、複雑さが少なく、管理が容易です。ただし、ここでは必然的にいくつかの関数を呼び出す必要があり、できればそれらは抽象的です。
したがって、抽象update
関数を使用してシステムの抽象インターフェースを作成する場合があります。その後、中央エンジンに登録され、ネットワークシステムは、「私はシステムです。ここに私のアップデート機能があります。時々電話してください。」と言うことができます。そして、エンジンはそのようなすべてのシステムをループして、特定のシステムへの関数呼び出しをハードコーディングすることなくそれらを更新できます。
これにより、システムは独自の孤立した世界のようにより多く生きることができます。ゲームエンジンは、それらについて具体的に(具体的に)知る必要がなくなりました。そして、物理システムは更新関数が呼び出される場合があります。その時点で、中央システムから必要なデータをすべてのモーションに入力し、物理を適用して、結果のモーションを出力します。
その後、ネットワークシステムの更新関数が呼び出されることがあります。この時点で、中央データベースから必要なデータが入力され、たとえばソケットデータがクライアントに出力されます。繰り返しになりますが、私が見ている目標は、各システムを可能な限り分離して、外の世界についての最小限の知識で独自の小さな世界に住むことができるようにすることです。これは基本的に、ECSで採用されている種類のアプローチであり、ゲームエンジンの間で人気があります。
ECS
上記の私の考えの多くはECSを中心に展開されており、このデータ指向のデカップリングへのアプローチにより、保守がオブジェクト指向およびCOMベースのシステムよりもはるかに簡単になった理由を合理化しようとしているので、ECSについて少し説明する必要があると思います過去、私がSEについて学んだ、神聖なものすべてに違反したにもかかわらず、また、大規模なゲームを作ろうとするなら、それはあなたにとって非常に理にかなっているかもしれません。したがって、ECSは次のように機能します。
上記の図のようにMovementSystem
、update
関数の呼び出しが行われる場合があります。この時点でPosAndVelocity
、入力するデータとしてコンポーネントを中央データベースに照会する場合があります(コンポーネントは単なるデータであり、機能はありません)。次に、それらをループして、位置/速度を変更し、新しい結果を効果的に出力します。次に、RenderingSystem
更新関数が呼び出され、その時点でデータベースPosAndVelocity
とSprite
コンポーネントが照会され、そのデータに基づいて画像が画面に出力されます。
すべてのシステムは、互いの存在について完全に気づいていないため、それらが何であるかを理解する必要さえありません。 Car
です。1つを表すために必要なデータを構成する各システムの関心の特定のコンポーネントを知る必要があるだけです。各システムはブラックボックスのようなものです。データの入力と出力は、外界に関する最小限の知識で行われ、外界も最小限の知識しか持っていません。あるシステムからプッシュして別のシステムからポップするイベントが発生する可能性があります。たとえば、物理システム内の2つのエンティティの衝突により、オーディオに衝突イベントが表示され、サウンドが再生されますが、システムはまだ気づいていません。お互いについて。そして、私はそのようなシステムを非常に簡単に推論することができました。何十ものシステムがあるとしても、それぞれが非常に隔離されているので、それらは私の脳を爆発させたくさせません。あなたは ズームインして特定の1つを操作するときは、全体としてすべての複雑さを考慮する必要があります。そのため、変更の結果を予測することも非常に簡単です。