どのGameObjectが衝突を処理するかを決定する方法は?


28

衝突では、2つのGameObjectが関係していますか?私が知りたいのは、どのオブジェクトに自分が含まれているOnCollision*かをどのように決定するのですか?

例として、PlayerオブジェクトとSpikeオブジェクトがあるとします。私の最初の考えは、次のようなコードを含むスクリプトをプレーヤーに配置することです。

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Spike")) {
        Destroy(gameObject);
    }
}

もちろん、代わりに次のようなコードを含むスクリプトをSpikeオブジェクトに置くことで、まったく同じ機能を実現できます。

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Player")) {
        Destroy(coll.gameObject);
    }
}

どちらも有効ですが、この場合、衝突が発生するとPlayerでアクションが実行されるため、Playerでスクリプトを使用する方が理にかなっています。

しかし、私がこれを疑っているのは、将来、敵、溶岩、レーザービームなど、衝突時にプレイヤーを殺すオブジェクトをさらに追加したいかもしれないということです。これらのオブジェクトはおそらく異なるタグを持つでしょう。そのため、プレーヤーのスクリプトは次のようになります。

OnCollisionEnter(Collision coll) {
    GameObject other = coll.gameObject;
    if (other.compareTag("Spike") || other.compareTag("Lava") || other.compareTag("Enemy")) {
        Destroy(gameObject);
    }
}

一方、スクリプトがスパイク上にあった場合、あなたがしなければならないことは、プレーヤーを殺すことができ、スクリプトのような名前を付けることができる他のすべてのオブジェクトに同じスクリプトを追加することKillPlayerOnContactです。

また、プレイヤーと敵が衝突した場合、おそらく両方でアクションを実行したいと思うでしょう。その場合、どのオブジェクトが衝突を処理する必要がありますか?または、衝突を処理し、異なるアクションを実行する必要がありますか?

これまでに妥当なサイズのゲームを作成したことはありません。最初にこの種の問題が発生した場合、コードが大きくなり、メンテナンスが難しくなるのではないかと思っています。それとも、すべての方法が有効であり、それは本当に重要ではありませんか?


洞察は大歓迎です!お時間をいただきありがとうございます:)


まず、タグの文字列リテラルはなぜですか?タイプミスは追跡が困難なバグではなくコンパイルエラーになるため、列挙型の方がはるかに優れています(なぜスパイクが私を殺さないのですか?)。
ラチェットフリーク

私はUnityのチュートリアルに従っているので、それが彼らのやったことだからです。だから、あなたはすべてのタグ値を含むTagと呼ばれるパブリック列挙型を持っていますか?それでTag.SPIKE代わりになりますか?
Redtama

両方の世界を最大限に活用するには、enumを内部に持つ構造体と、静的なToStringメソッドとFromStringメソッドを使用することを検討して
ください-zcabjro

回答:


48

私は個人的には、衝突に関与するオブジェクトがそれを処理しないようにシステムを設計し、代わりにいくつかの衝突処理プロキシがのようなコールバックでそれを処理することを好みますHandleCollisionBetween(GameObject A, GameObject B)。そうすれば、どのロジックがどこにあるべきかを考える必要がなくなります。

衝突応答アーキテクチャを制御していない場合など、それがオプションではない場合、事態は少し曖昧になります。一般的に答えは「依存します」です。

両方のオブジェクトがコリジョンコールバックを取得するため、両方にコードを配置しますが、可能な限り拡張性を維持しながら、ゲームワールドのそのオブジェクトの役割に関連するコードのみを配置します。つまり、いずれかのオブジェクトで一部のロジックが同等の意味を持つ場合、新しい機能が追加されるにつれて長期的にコードの変更が少なくなる可能性が高いオブジェクトに配置することを選択します。

別の言い方をすれば、衝突する可能性のある2つのオブジェクトタイプの間で、これらのオブジェクトタイプの1つは一般に、他のオブジェクトタイプよりも多くのオブジェクトタイプに対して同じ効果を持ちます。より多くの他の型に影響を与える型にその効果のコードを入れると、長期的にはメンテナンスが少なくなります。

たとえば、プレーヤーオブジェクトと弾丸オブジェクト。おそらく、「弾丸との衝突でダメージを受ける」ことは、プレイヤーの一部として理にかなっています。「命中したものにダメージを与える」ことは、弾丸の一部として理にかなっています。ただし、弾丸は、プレーヤー(ほとんどのゲーム)よりもさまざまな種類のオブジェクトにダメージを与える可能性があります。プレイヤーは地形ブロックやNPCによるダメージを受けませんが、弾丸はそれらにダメージを与える可能性があります。そのため、この場合、弾丸にダメージを与えるために衝突処理を配置することを好みます。


1
みんなの答えは本当に役立つと思いましたが、明確にするためにあなたの答えを受け入れなければなりません。あなたの最後の段落は本当に私のためにそれを要約します:)非常に役に立ちました!どうもありがとうございます!
Redtama

12

TL; DRは、衝突検出器を置くのに最適な場所は、あなたがそれが考えるところであるアクティブな代わりに、衝突時における要素の受動的なアクティブ論理で実行される、おそらくあなたは、追加の行動が必要となりますので、衝突の要素(そして、SOLIDのような良いOOPガイドラインに従うことは、アクティブな要素の責任です)。

詳細

どれどれ ...

ゲームエンジン関係なく、同じ疑問がある場合の私のアプローチは、コレクションでトリガーするアクションに関して、どの動作がアクティブで、どの動作がパッシブであるかを判断することです。

注:ロジックのさまざまな部分の抽象化としてさまざまな動作を使用して、損傷と(別の例として)インベントリを定義します。どちらも後でPlayerに追加する個別の動作であり、カスタムPlayerを、保守が難しい個別の責任で混乱させることはありません。RequireComponentのような注釈を使用して、プレーヤーの動作に、現在定義している動作が含まれていることを確認します。

これは私の意見です。タグに応じてロジックを実行するのを避け、代わりに列挙型などの異なる動作と値を使用して選択します

この動作を想像してください:

  1. オブジェクトを地面上のスタックとして指定する動作。RPGゲームは、拾うことができる地面のアイテムにこれを使用します。

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
  2. 損傷を引き起こす可能性のあるオブジェクトを指定する動作。これは、溶岩、酸、有毒物質、または飛翔体である可能性があります。

    public class DamageDealer : MonoBehaviour {
        public Faction facton; //let's use it as well..
        public DamageType damageType = DamageType.BLUNT;
        public uint damage = 1;
    }

この範囲で、enumとして検討DamageTypeInventoryObjectます。

これらの行動はアクティブではありません。地面にいることや飛び回ることによって、自分で行動することはありません。彼らは何もしません。例えば、空の空間で火の玉が飛んでいる場合、それはダメージを与える準備ができていません(おそらく、火の玉が移動中に彼の力を消費し、その過程でそのダメージ力を減らすなど、火の玉で他の行動がアクティブであると考えられるべきですが、潜在的なFuelingOut動作を開発できる場合、それは別の動作であり、同じDamageProducer動作ではありません)、地面にオブジェクトが表示されている場合、ユーザー全体をチェックせず、私を選ぶことができると考えていますか?

そのためには積極的な行動が必要です。カウンターパートは次のとおりです。

  1. ダメージを受けるための1つの動作(生を下げることとダメージを受けることは通常別々の動作であり、この例をできるだけシンプルに保ちたいため、生と死を処理するための動作が必要になります)と、おそらくダメージに抵抗します。

    public class DamageReceiver : MonoBehaviour {
        public DamageType[] weakness;
        public DamageType[] halfResistance;
        public DamageType[] fullResistance;
        public Faction facton; //let's use it as well..
    
        private List<DamageDealer> dealers = new List<DamageDealer>(); 
    
        public void OnCollisionEnter(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && !this.dealers.Contains(dealer)) {
                this.dealers.Add(dealer);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && this.dealers.Contains(dealer)) {
                this.dealers.Remove(dealer);
            }
        }
    
        public void Update() {
            // actually, this should not run on EVERY iteration, but this is just an example
            foreach (DamageDealer element in this.dealers)
            {
                if (this.faction != element.faction) {
                    if (this.weakness.Contains(element)) {
                        this.SubtractLives(2 * element.damage);
                    } else if (this.halfResistance.Contains(element)) {
                        this.SubtractLives(0.5 * element.damage);
                    } else if (!this.fullResistance.Contains(element)) {
                        this.SubtractLives(element.damage);
                    }
                }
            }
        }
    
        private void SubtractLives(uint lives) {
            // implement it later
        }
    }

    このコードはテストされておらず、概念として機能するはずです。目的は何ですか?ダメージは誰かが感じるものであり、あなたが考えるように、ダメージを受けているオブジェクト(または生物)に関連しています。これは一例です:通常のRPGゲームでは、剣はあなたダメージを与えますが、それを実装する他のRPG(例:Summon Night Swordcraft Story IおよびII)では、剣はハードパーツを打ったり、鎧をブロックしたりしてダメージを受けることができます。修理します。したがって、損傷ロジックは送信者ではなく受信者に関連しているため、関連するデータのみを送信者に、ロジックを受信者に配置します。

  2. 同じことが在庫保有者にも当てはまります(別の必要な動作は、地面にあるオブジェクトに関連する動作と、そこからオブジェクトを削除する機能です)。おそらくこれは適切な動作です。

    public class Inventory : MonoBehaviour {
        private Dictionary<InventoryObject, uint> = new Dictionary<InventoryObject, uint>();
        private List<Treasure> pickables = new List<Treasure>();
    
        public void OnCollisionEnter(Collision col) {
            Treasure treasure = col.gameObject.GetComponent<Treasure>();
            if (treasure != null && !this.pickables.Contains(treasure)) {
                this.pickables.Add(treasure);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer treasure = col.gameObject.GetComponent<DamageDealer>();
            if (treasure != null && this.pickables.Contains(treasure)) {
                this.pickables.Remove(treasure);
            }
        }
    
        public void Update() {
            // when certain key is pressed, pick all the objects and add them to the inventory if you have a free slot for them. also remove them from the ground.
        }
    }

7

ここでのさまざまな回答からわかるように、これらの決定にはかなりの程度の個人的なスタイルがあり、特定のゲームのニーズに基づいた判断があります-絶対的な最善のアプローチはありません。

私の推奨事項は、ゲームの成長に合わせてアプローチをどのように拡張するかという観点から考え、機能を追加および反復することです。衝突処理ロジックを1か所に配置すると、後でより複雑なコードになりますか?それとも、よりモジュラーな動作をサポートしていますか?

そのため、プレイヤーにダメージを与えるスパイクがあります。自問してみてください:

  • スパイク以外のダメージを与える可能性があるプレイヤー以外のものはありますか?
  • スパイク以外に、プレイヤーが衝突時にダメージを受けるべきものはありますか?
  • プレイヤーのいるすべてのレベルにスパイクがありますか?スパイクのあるすべてのレベルにプレイヤーがいますか?
  • 別のことをする必要があるスパイクのバリエーションがありますか?(例えば、異なる損傷量/損傷タイプ?)

明らかに、これに夢中になり、必要な1つのケースだけでなく、100万のケースを処理できる過剰設計のシステムを構築したくありません。しかし、どちらの方法でも同じように機能する場合(つまり、これら4行をクラスAとクラスBに配置する)が、1つがより適切にスケーリングする場合、決定を行うための良い経験則です。

この例では、特にUnity では、スパイクダメージ処理ロジックCollisionHazardコンポーネントのような形で配置し、衝突するとコンポーネントのダメージを取得するメソッドを呼び出しDamageableます。その理由は次のとおりです。

  • 区別するコリジョン参加者ごとにタグを別に用意する必要はありません。
  • さまざまな衝突可能なインタラクション(溶岩、弾丸、スプリング、パワーアップなど)を追加すると、プレーヤークラス(またはDamageableコンポーネント)は、それぞれを処理するためのif / else / switchケースの巨大なセットを蓄積しません。
  • レベルの半分にスパイクがない場合、プレーヤーのコリジョンハンドラーがスパイクが関与しているかどうかをチェックする時間を無駄にしません。衝突に関与している場合にのみ費用がかかります。
  • スパイクで敵や小道具も殺すと後で決めた場合、プレーヤー固有のクラスからコードを複製する必要はありません。Damageableコンポーネントを追加するだけです(スパイクのみに耐性がある必要がある場合は、追加できます損傷の種類とDamageableコンポーネントが免疫を処理できるようにします)
  • チームで作業している場合、スパイクの動作を変更してハザードクラス/アセットをチェックアウトする必要がある人は、プレーヤーとのやり取りを更新する必要があるすべての人をブロックしていません。また、新しいチームメンバー(特にレベル設計者)にとって、スパイクの動作を変更するためにどのスクリプトを調べる必要があるかは直感的です。これは、スパイクに関連付けられているスクリプトです。

そのようなIFを回避するために、エンティティコンポーネントシステムが存在します。これらのIFは常に(単に間違っているものを IFS)。さらに、スパイクが動いている(または発射体である)場合、衝突するオブジェクトがプレーヤーであるかどうかにかかわらず、衝突ハンドラーがトリガーされます。
ルイスMasuelli 16

2

誰が衝突を処理するかは実際には関係ありませんが、誰の仕事であるかは一貫しています。

私のゲームGameObjectでは、衝突を制御するオブジェクトとして、他のオブジェクトに対して何かを「行う」ことを選択しようとします。IE Spikeはプレーヤーにダメージを与え、衝突を処理します。両方のオブジェクトが互いに何かを「行う」場合、それぞれが他のオブジェクトに対するアクションを処理するようにします。

また、衝突がより少ないもので衝突に反応するオブジェクトによって処理されるように、衝突を設計することをお勧めします。スパイクはプレイヤーとの衝突について知る必要があるだけで、Spikeスクリプトの衝突コードを美しくコンパクトにします。プレーヤーオブジェクトは、膨張した衝突スクリプトを作成する多くのものと衝突する可能性があります。

最後に、衝突ハンドラーをより抽象的なものにしてください。スパイクは、プレイヤーと衝突したことを知る必要はなく、ダメージを与える必要があるものと衝突したことを知るだけです。スクリプトがあるとしましょう:

public abstract class DamageController
{
    public Faction faction; //GOOD or BAD
    public abstract void ApplyDamage(float damage);
}

DamageControllerダメージを処理するためにプレーヤーのGameObjectでサブクラス化してから、Spikeのコリジョンコードで

OnCollisionEnter(Collision coll) {
    DamageController controller = coll.gameObject.GetComponent<DamageController>();
    if(controller){
        if(controller.faction == Faction.GOOD){
          controller.ApplyDamage(spikeDamage);
        }
    }
}

2

私が始めたときと同じ問題を抱えていたので、ここに行きます:この特定のケースでは敵を使います。通常、アクションを実行するオブジェクト(この場合-敵ですが、プレイヤーが武器を持っている場合、武器が衝突を処理する必要があります)によって衝突を処理する必要があります。 (敵の)プレイヤーのスクリプトではなく敵のスクリプトで間違いなく変更したい(特に複数のプレイヤーがいる場合)。同様に、プレイヤーが使用する武器のダメージを変更したい場合、ゲームのすべての敵でそれを変更したくないことは間違いありません。


2

両方のオブジェクトが衝突を処理します。

一部のオブジェクトは、衝突によって即座に変形、破損、破壊されます。(弾丸、矢印、水風船)

他の人はただ跳ね返って横に転がります。(ロック)ヒットしたものが十分に難しい場合、これらはまだダメージを受ける可能性があります。岩は人間が岩にぶつかってもダメージを受けませんが、スレッジハンマーからはダメージを受ける可能性があります。

人間のようないくつかのぐにゃぐにゃなオブジェクトはダメージを受けます。

他の人はお互いにバインドされます。(磁石)

両方の衝突は、特定の時間と場所(および速度)でオブジェクトに作用するアクターのイベント処理キューに配置されます。ハンドラーはオブジェクトに何が起こるかだけを処理する必要があることを覚えておいてください。ハンドラーがキューに別のハンドラーを配置して、アクタまたはアクション対象のオブジェクトのプロパティに基づいて衝突の追加の効果を処理することも可能です。(爆弾が爆発し、結果として生じる爆発は、他のオブジェクトとの衝突をテストする必要があります。)

スクリプトを作成するときは、コードでインターフェイス実装に変換されるプロパティを持つタグを使用します。次に、処理コードでインターフェイスを参照します。

たとえば、剣にはMeleeのタグがあり、Meleeという名前のインターフェイスに変換されます。これは、ダメージタイプと固定値または範囲などを使用して定義されたダメージ量のプロパティを持つDamageGivingインターフェイスを拡張します。 ExplosiveインターフェースのExplosiveタグも、半径と損傷の拡散速度に関する追加プロパティを持つDamageGivingインターフェースを拡張します。

ゲームエンジンは、特定のオブジェクトタイプではなく、インターフェイスに対してコーディングします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.