C#のコンポーネント指向エンジン[パート2]

独立したコンポーネント、最小限の相互認識、目標のみを満たすことができます-これは非常にクールです! 以前の投稿で最初のスケッチとアイデアについて書きました。



それ以来、システムの拡張と修正を余儀なくされた新しい問題に直面しなければなりませんでした。



すべての長所と短所、およびわずかな蓄積された経験から、重力、力、衝突を伴う非常に単純な2D物理学の例を検討します。 猫、友達へようこそ!



はじめに



空き時間には、トレーニングのためだけに作成された自分のプロジェクトに集中的に取り組んでいます。そのため、自転車でいっぱいです。 この「エンジン」はおそらくそれらの1つです。



注:もちろん、実際のタスクのフレームワーク内で、既成のソリューションを使用して、木こりがaを取り、それで木を切るときに使用することをお勧めします。 単純に、これらの軸がどのように機能するかを理解したいことがあります。



現在の状態



最後の話からあまり時間はありませんでしたが、数晩で、システムまだ解決できない多くのタスクがあることに気付きました。



それで、私たちがすでに知っていることを簡単に見てみましょう:



新しい問題



私は何をすべきかを長い間考えていました。最初に問題が発生した問題を伝えるか、問題、その解決策、そしてタスク自体を示します。 最終的に、彼は2番目の選択肢に落ち着きました。 まず、イノベーションと拡張について学び、それらを詳細に検討し、次に最新のデータを入手して問題を解決します。



それなのに、どのギアがフルパワーで動作するには不十分ですか?



まず、前のパートでは、1つのコンポーネントが(親コンテナ内で)別のコンポーネントにメッセージを送信できるメッセージメカニズムを導入する予定があることは言及しませんでした。 読者がすでに気づいているように、イベントベースのアプローチにより、アーキテクチャに影響を与えることなく柔軟性を獲得できます。 さらに、誰もそれを課していません 。「 私はすべてのコンポーネントにメッセージを送信します。あなたは聞きたい、聞きたくない、したくない 」。



第二に、前提条件に関する不確実性が残った。 場合によっては、前提条件が満たされていない場合、契約違反の口実の下でプログラムを完全に破る必要があります(テクスチャレンダリングコンポーネントはテクスチャコンポーネントなしでは意味がありません)、それ以外の場合は無視できますか? 「 今、このコンポーネントはありませんが、いつか表示されます! 」。



さらに、小さな欠陥があり、ほとんどの場合、そのまま残っています。



前提条件



前提条件についてすぐに話しましょう。 解決策は非常に簡単です。 新しいリスト:

public enum OnValidationFail { Throw, Skip }
      
      





それだけです! これで、次のように使用できます。

 [AttachHandler(OnValidationFail.Skip)]
      
      





メッセージ転送



メッセージの転送がより興味深い。 まず、各コンポーネントは自分自身を送信者および受信者として配置できるようになりました。



一般的に非常に簡単に説明されているメッセージを送信できます。

 public class Message<T> : Message { public T Sender { get; private set; } public Message(T sender) { Sender = sender; } }
      
      





それを得るために、リフレクションを使用して同じアプローチを再度(3回目)使用しました。



クラス本体のコンポーネントは、任意のメッセージをパラメーターとして取るメソッドを記述できます。 [MessageReceiver]属性は、有効な受信者にするのに役立ちます。 例:

 public class SimpleComponent : Component { [MessageReceiver] public void MessageFromFriend(DetachMessage<FriendComponent> message) { // , ,      . //    ? _container.RemoveComponent(this); } [MessageReceiver] public void MessageFromSomeComponent(DetachMessage<SomeComponent> message) { //  ,   ,   . // - ,   . //_container.RemoveComponent(this); } }
      
      





なぜなら 追加や削除などのコンポーネントのライフサイクルの段階があることは明らかです。 コンポーネントクラスに保護されたユーティリティメソッドをスマートに記述し、相続人が現在の状態についてすべての「隣人」に簡単に通知できるようにします。

 protected void SendAttachMessage<TSender, TContainer> (TSender sender, TContainer container) where TSender : Component { SendMessage( new ComponentAttachMessage<TSender, TContainer>(sender, container)); } protected void SendDetachMessage<T>(T sender) where T : Component { SendMessage(new ComponentDetachMessage<T>(sender)); }
      
      





これについては、主な「主要な」改善(実際には1つだけ)がすべてです。 次に、新しい機能だけでなく、一般にシステムの適用範囲について直接お話しします。



重力問題



開発の初期段階では、次の些細なタスクが発生しました。 少なくとも 2種類の「ボックス」をステージに配置できるようにすることです。



それは非常に単純に聞こえますが、新しく設計されたシステムがそれを行うことができるかどうかは明らかではありませんか?



ソリューションの一般的なスケッチを、実装を参照せずに論理的な順序で展開します。

1.シーンのX、Y座標に配置して描画できるゲームオブジェクトがあるとします

2. ソリッドボディの概念-ゲームオブジェクトのプロパティを導入します。これにより、対応する座標の変化率を設定できます。

3.原則として、ゲームオブジェクトには移動のためのすべてがあります:座標、それらを変更することを可能にするメカニズム。 十分なプッシュ、つまり強さはありません。

4.他に何かありますか? ステージ上でオブジェクトを移動できるようにすることは問題ありませんが、やはり衝突する方法を教える必要があります。 これを行うために、衝突の原因となるオブジェクトシェルの概念を定義します



ゲームオブジェクト



上記のスケッチに従って、元のエンティティ( ゲームオブジェクト )を選び出します 。これは、組み合わせてコンポーネントのコンテナになります。

 public class GameObject : ComponentContainer { }
      
      





すべてのゲームオブジェクトをシーンに配置できるわけではないため、相続人であるシーンオブジェクトを紹介します

 public class SceneObject : GameObject { public float X { get; set; } public float Y { get; set; } public event Action<TimeSpan> Updating; public event Action<TimeSpan> Drawing; public void Draw(TimeSpan deltaTime) { Updating(deltaTime); } public void Updated(TimeSpan deltaTime) { Drawing(deltaTime); } }
      
      





注:不要なコードを大量に記述しないために、実装の詳細を一部省略します。 最も重要なことはアイデアですが、Updated and Drawnのnullをチェックしなかったか、ダミーに署名しなかったという事実は二次的な質問です。



固体



ソリッドはゲームオブジェクトのプロパティであるため(そうでない場合もあります)、コンポーネントにカプセル化できます。 コードを見てみましょう:

 public class RigidBody : Component { private SceneObject _sceneObject; private float _newX; private float _newY; public float VelocityX { get; set; } public float VelocityY { get; set; } [AttachHandler] public void OnSceneObjectAttach(SceneObject sceneObject) { _sceneObject = sceneObject; _sceneObject.Updating += OnUpdate; _sceneObject.Drawing += OnDraw; } private void OnUpdate(TimeSpan deltaTime) { //          //  . if (VelocityX != 0.0f) _newX = (float) (_sceneObject.X + (VelocityX * deltaTime.TotalSeconds)); if (VelocityY != 0.0f) _newY = (float) (_sceneObject.Y + (VelocityY * deltaTime.TotalSeconds)); } private void OnDraw(TimeSpan deltaTime) { //     . _sceneObject.X = _newX; _sceneObject.Y = _newY; } }
      
      





前のコードがどのように機能するかを誰もが理解できる、本当に意味のある複雑な物理構造を思いつくほど愚かであることを願っています。





そこで、「永遠の」運動の最後の鍵を握りました。 結局のところ、強度は、本質的には固体の速度を変えるものにすぎません。 どの速度をどのように変更するかを理解する必要があります。



注:おそらく、誰かが同じ方法ですでにこの問題を何千回も解決している可能性がありますが、プロジェクトは教育的なものです。



何を思いつきましたか?



oX軸とoY軸の値が[-1〜1]の範囲で表される、いわゆる「単位円」を見てみましょう。



青い矢印が体に対する力の作用のベクトルであり、円の中心が体の重心であるとします。 そして、例えば、力自体の条件値は100です。

力がオブジェクトの速度を変更する場合、垂直と水平の両方で速度を変更する必要があり、緑とオレンジの線が答える限りです。 それらはそれぞれ約0.9と0.5に等しくなります。

したがって、オブジェクトの垂直速度は100 * 0.9、水平方向-100 * 0.5ずつ変化します。

これらの非常に単純な結論をコードで修正します。

 public class Force { public int Angle { get; set; } public float Power { get; set; } public void Add(RigidBody rigidBody) { //     ( ). var radians = GeometryUtil.DegreesToRadians(Angle); var horizontalCoefficient = GetHorizontalCoefficient(radians); var verticalCoefficient = GetVerticalCoefficient(radians); rigidBody.VelocityX += Power * horizontalCoefficient; rigidBody.VelocityY += Power * verticalCoefficient; } private float GetHorizontalCoefficient(double radians) { var scaleX = Math.Cos(radians); if (Math.Abs(scaleX) <= 0) return 0; return (float) scaleX; } private float GetVerticalCoefficient(double radians) { //     -1, ..      oY . var scaleY = Math.Sin(radians) * -1; if (Math.Abs(scaleY) <= 0) return 0; return (float) scaleY; } }
      
      





Forceクラスはコンポーネントでもコンテナでもないことに注意してください。 これは、後で他の人が再利用するプリミティブ型です。



固体に力をかけることができる誰かが行方不明になっているように感じますよね?



重力



最も困難を引き起こしたもの。 重力場のようなことをしたかったのですが、それがどこから来て、どのようにオブジェクトに適用されるかは明確ではありませんでした。 コンポーネントですか、コンテナですか?



最後に、彼の考えを集めて、彼はそのコンポーネントを決定しました。 重力場に存在するオブジェクトに追加されるコンポーネント。

コンポーネントのタスクは、更新サイクルごとに9.83の値で270度(下)の角度で力を加えることです。



 [RequiredComponent(typeof(RigidBody))] public class Gravitation : Component { private SceneObject _sceneObject; private RigidBody _rigidBody; private Force _gravitationForce = new Force(270, 9.83); [AttachHandler(OnValidationFail.Skip)] public void OnSceneObjectAttach(SceneObject sceneObject) { _sceneObject = sceneObject; _rigidBody = GetComponent<RigidBody>(); _sceneObject.Updating += OnUpdate; } private void OnUpdate(TimeSpan deltaTime) { _gravitationForce.Add(_rigidBody); } //  ,   ,    - . [MessageReceiver] public void OnRigidBodyAttach( ComponentAttachMessage<RigidBody, SceneObject> message) { _rigidBody = message.Sender; _sceneObject = message.Container; _sceneObject.Updating += OnUpdate; } [MessageReceiver] public void OnRigidBodyDetach(ComponentDetachMessage<RigidBody> message) { _sceneObject.Updating -= OnUpdate; } }
      
      





したがって、オブジェクトに固体成分が含まれていなくても、重力は壊れません。 前述のコンポーネントがコンテナに追加されるまで、機能しません。



この例は実例であり、1つのコンポーネントが親コンテナ内の別のコンポーネントにメッセージを送信する状況を明確に示しています。



衝突



宇宙が基づいている物理エンジンの最も興味深い部分のままでした。



問題の解決策のスケッチから、 衝突の原因となるシェルは、固体のようなオブジェクトのプロパティでもあることがわかります。 したがって、ためらうことなく、それをコンポーネントとして定義します。



さらに、シェルは異なると推定しました。最も単純なものは長方形ですが、数えやすい丸いものがまだあり、最後に、N個の頂点を持つ任意の形状のシェルがあります。



ある時点で、2つの異なるシェル間の衝突を計算する必要があります。 たとえば、円は長方形の地面に落ちました。



抽象シェルクラスについて説明します。

 public abstract class Collider : Component { protected SceneObject SceneObject; [AttachHandler] public OnSceneObjectAttach(SceneObject sceneObject) { SceneObject = sceneObject; } //    . public abstract bool ResolveCollision(Collider collider); public abstract bool ResolveCollision(BoxCollider collider); }
      
      





主なアイデアは、ベースColliderタイプのパラメーターを受け入れるResolveCollisionメソッドを使用して衝突を計算することですが、このメソッド内では、シェルの特定のバージョンごとに、特定のタイプで機能する別のメソッドに呼び出しをリダイレクトします。 アドホックポリモーフィズムがボールを動かします。



長方形の簡単な例を示します。

 public class BoxCollider : Collider { public float Width { get; set; } public float Height { get; set; } public RectangleF GetBounds() { return new RectangleF(SceneObject.X, SceneObject.Y, Width, Height); } public override bool ResolveCollision(Collider collider) { return collider.ResolveCollision(this); } public override bool ResolveCollision(BoxCollider boxCollider) { var bounds = GetBounds(); var colliderBounds = boxCollider.GetBounds(); if (!bounds.IntersectsWith(colliderBounds)) return false; // :         bounds. // ..    ,     "" //  ,       - . //         (    ). bounds.Intersect(colliderBounds); if (bounds.Height <= 1f) return false; SceneObject.Y -= bounds.Height; return true; } }
      
      





主な問題を整理しました。ゲームオブジェクトを作成し、さまざまなプロパティを付与し、移動し、一緒にプッシュできるようになりました。 最後の小さな詳細が欠落しています:衝突を計算する場所?



シーン



SceneObjectがある場合、それらがすべて配置されているシーンが存在する必要があります。 彼女はオブジェクトの状態とレンダリングを更新する責任があります。 彼女はまた衝突を考慮します。



これまで重力の問題の解決に使用されていなかった相互作用レイヤーが役立ちます。 2つのシーンオブジェクト間の衝突を計算するInteractorクラスについて説明します。

 public class CollisionDetector : Interactor { [InteractionMethod] [RequiredComponent("first", typeof(Collider))] [RequiredComponent("second", typeof(Collider))] public void SceneObjectsInteraction(SceneObject first, SceneObject second) { var firstCollider = first.GetComponent<Collider>(); var secondCollider = second.GetComponent<Collider>(); if (!firstCollider.ResolveCollision(secondCollider)) return; //  ,           ,   . first.IfContains<RigidBody>(TryAddInvertedForce); second.IfContains<RigidBody>(TryAddInvertedForce); } private void TryAddInvertedForce(RigidBody rigidBody) { var lastAddedForce = rigidBody.ForceHistory.Last(); var invertedForce = new Force { Angle = GeometryUtil.GetOppositeAngle(lastAddedForce.Angle), Power = rigidBody.GetKineticEnergyY() }; invertedForce.Add(rigidBody); } }
      
      





注:この例は、物理学の分野の知識を示すものではありません。 一定の速度で移動する空中で衝突する2つの物体に対しては機能しませんが、単純な落下の場合の外観を明確に示しています。



一般的な概念は次のとおりです:物体が地面に落ちて衝突すると、地球は落下と反対方向に作用し、物体の現在の運動エネルギーに等しい新しい力で落下速度を補償します。



インターアクター自体は、各フレームが実行する更新メソッドのシーンから呼び出されます。

衝突計算メカニズムでは、私はまだ粗いO(n 2 )メソッドを使用しています。

 //   . public void Update(TimeSpan deltaTime) { foreach (var sceneObject in _sceneObjects) { sceneObject.Update(deltaTime); foreach (var anotherSceneObject in _sceneObjects) { if (ReferenceEquals(sceneObject, anotherSceneObject)) continue; sceneObject.Interact(anotherSceneObject).Using<CollisionDetector>(); } } }
      
      





おわりに



正直なところ、実際には、物理​​エンジンの小さな構造は、最初はそれほど明白ではないようでした。 特に衝突と重力。 物理エンジンがそのように書かれている可能性は低いと思いますが、自分で「プローブ」するのは面白かったです。



その過程で、私はコンポーネント指向アプローチのいくつかの明らかな利点を強調することができました。これは私の観察と常識の両方に基づいています。

1.コードの記述は、コンポーネントが何をするのか、誰に依存するのかを明確に見ると非常に簡単です(目を引く)。

2.コードは非常に柔軟で独立しています。 各コンポーネントには、コンテナに対する観察された明確な影響度があります。



タスクが比較的単純だった可能性が高く、負荷が高くなると、システムがクラッシュし、コードに不要な複雑さが導入され始めます。 私はすぐにそれをチェックすると思います。



みんなありがとう!



All Articles