如何决定哪个GameObject应该处理碰撞?


28

在任何碰撞中,都涉及两个GameObjects,对吗?我想知道的是,如何确定哪个对象应包含我的对象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);
    }
}

而在脚本位于Spike上的情况下,您要做的就是将相同的脚本添加到所有其他可能杀死Player的对象上,并命名该脚本为KillPlayerOnContact

另外,如果您在播放器和敌人之间发生碰撞,那么您可能想对两者都执行操作。那么在那种情况下,哪个对象应该处理碰撞?还是应该同时处理碰撞并执行不同的动作?

我以前从未开发过任何大小合适的游戏,而且我想知道,如果一开始就把这种事情弄错了,代码是否会变得混乱并且难以维护。也许所有方法都是有效的,这并不重要吗?


非常感谢任何见解!感谢您的时间 :)


首先,为什么要使用标签的字符串文字?枚举要好得多,因为错别字会变成编译错误,而不是难以跟踪的错误(为什么我的尖峰不会杀死我?)。
棘手怪胎

因为我一直在关注Unity教程,所以他们就是这么做的。因此,您是否只有一个名为Tag的公共枚举,其中包含您所有的标签值?原来是这样Tag.SPIKE吗?
Redtama

为了充分利用这两方面的优势,请考虑使用内部带有枚举的结构以及静态ToString和FromString方法
zcabjro

密切相关:OOP中的零行为对象-我的设计困境与埃里克·利珀特的《巫师与勇士》系列:ericlippert.com/2015/04/27/wizards-and-warriors-part-one

Answers:


48

我个人更喜欢对系统进行架构设计,以使碰撞中所涉及的任何对象都不能处理它,而某些碰撞处理代理可以通过像这样的回调来处理它HandleCollisionBetween(GameObject A, GameObject B)。这样,我不必考虑什么逻辑应该放在哪里。

如果那不是一个选择,例如当您无法控制碰撞响应体系结构时,事情就会变得有些模糊。通常,答案是“取决于情况”。

由于这两个对象都将获得碰撞回调,因此我将在两者中都放置了代码,但只添加了与该对象在游戏世界中的角色相关的代码,同时还尝试保持尽可能的可扩展性。这意味着,如果在任何一个对象中都具有相同的逻辑意义,则最好将其放在该对象中,从长远来看,随着添加新功能,这可能会导致较少的代码更改。

换句话说,在任何两个可能发生冲突的对象类型之间,这些对象类型中的一种通常对另一种对象具有相同的影响。从长远来看,将影响代码放到影响更多其他类型的类型中意味着较少的维护。

例如,玩家对象和子弹对象。可以说,“与子弹碰撞造成的损害”作为玩家的一部分是合乎逻辑的。作为子弹的一部分,“对所击中的物体施加损害”是合乎逻辑的。但是,子弹可能会比玩家造成更多类型的物体损坏(在大多数游戏中)。玩家不会受到地形块或NPC的伤害,但是子弹仍然可能会对它们造成伤害。因此,在这种情况下,我宁愿采用碰撞处理以将伤害施加到子弹中。


1
我发现每个人的答案都非常有帮助,但为了清楚起见,必须接受您的答案。您的最后一段对我来说真的很总结:)非常有用!非常感谢!
Redtama

12

TL; DR放置碰撞检测器的最佳位置是您认为它是碰撞中的主动元素,而不是碰撞中的被动元素,因为您可能需要在这种主动逻辑中执行其他行为(并且,遵循SOLID等良好的OOP准则是活动元素的责任)。

详细说明

让我们来看看 ...

无论我的游戏引擎如何,我都有相同的疑问时,我的方法是确定相对于我要在收集时触发的动作而言,哪些行为是主动的,哪些行为是被动的

请注意:我使用不同的行为作为逻辑的不同部分的抽象,以定义损坏和清单(作为另一个示例)。两者都是我以后要添加到播放器中的单独行为,而不是使我的自定义播放器具有单独的职责,而这些职责很难维护。使用RequireComponent之类的注释,我可以确保Player行为也具有我现在定义的行为。

这只是我的观点:避免根据标签执行逻辑,而是使用不同的行为和值(例如枚举)在中进行选择

想象一下这种行为:

  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;
    }

就此而言,请考虑DamageTypeInventoryObject作为枚举。

这些行为并不活跃,以至于它们在地面上或在周围飞来飞去时不会自己采取行动。他们什么都不做。例如,如果您有火球在空旷的空间中飞行,那么它尚不准备造成伤害(也许其他行为应被视为在火球中处于活跃状态,例如火球在旅行时消耗其力量并在此过程中降低其伤害力,但即使当潜在的FuelingOut行为可能被开发时,这是另一种行为,而不是相同的DamageProducer行为),如果您看到地面上的物体,那不是在检查整个用户并认为他们可以选择我吗?

为此,我们需要积极的行为。对应的将是:

  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的RPG中(例如,《Summon Night Swordcraft Story I》和《 Summon Night Swordcraft Story》),剑也可以通过击打坚硬的零件或阻挡装甲而受到伤害,并且可能需要修理。因此,由于损坏逻辑是相对于接收方而不是发送方的,因此我们仅将相关数据放入发送方,并将逻辑放入接收方。

  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

从这里的各种答案中可以看出,这些决定和根据您的特定游戏需求做出的判断涉及相当程度的个人风格-没有一种绝对的最佳方法。

我的建议是从您的方法随着游戏的发展,添加和迭代功能方面的扩展角度考虑。将冲突处理逻辑放在一个地方会在以后导致更复杂的代码吗?还是支持更多模块化行为?

因此,您的峰值会损坏播放器。问你自己:

  • 除了玩家以外,还有其他东西可能要损坏吗?
  • 除了尖峰以外,玩家还有其他东西要从碰撞中受到伤害吗?
  • 玩家的每个关卡都会出现峰值吗?每个带有尖刺的关卡都会有一个玩家吗?
  • 您可能需要执行不同操作的峰值变化吗?(例如,不同的损坏金额/损坏类型?)

显然,我们不想为此而烦恼,而要构建一个可以处理一百万个案例而不是仅仅需要一个案例的过度设计的系统。但是,如果这两种方法都是平等的工作(例如,将这4行放在A类和B类中),但是其中一个类的伸缩性更好,那么做出决定是一个很好的经验法则。

对于此示例,特别是在Unity中,我倾向于将损害处理逻辑以尖峰的形式放置在诸如CollisionHazard组件之类的组件中,该组件在发生冲突时会在组件中调用损害承受方法Damageable。原因如下:

  • 您无需为要区分的每个不同的碰撞参与者预留标签。
  • 随着您添加更多种类的可碰撞交互(例如,熔岩,子弹,弹簧,助力……),您的玩家类(或损坏组件)不会累积大量的if / else / switch案例来处理每个案例。
  • 如果您的一半关卡最终没有出现峰值,那么您就不会在玩家冲突处理程序中浪费时间检查是否涉及峰值。只有在发生碰撞时,它们才会使您付出代价。
  • 如果您以后决定尖刺也可以杀死敌人和道具,则无需从特定于玩家的类中复制代码,只需将Damageable组件粘贴在上面(如果您只需要对尖刺免疫,则可以添加损坏类型并让Damageable组件处理抗扰性)
  • 如果您在团队中工作,则需要更改尖峰行为并检查危险等级/资产的人员并不会阻止所有需要与玩家更新交互的人。对于新的团队成员(尤其是关卡设计师)来说,这也是直观的,他们需要使用这些脚本来更改尖峰行为-这是与尖峰相关的一个!

存在实体组件系统来避免像这样的IF。这些IF总是错误的(只是那些 if)。另外,如果尖峰正在移动(或是射弹),则尽管碰撞对象是否是玩家,但仍将触发碰撞处理程序。
Luis Masuelli

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

两个对象都将处理碰撞。

某些物体会因碰撞而立即变形,破裂或破坏。(子弹,箭头,水气球)

其他人只会反弹并滚到一边。(石头)如果它们击中的东西足够坚硬,它们可能仍然会受到伤害。岩石不会受到人类撞击的伤害,但可能会受到八角锤的伤害。

像人类这样的黏糊糊的物体会受到损害。

其他人将彼此束缚。(磁铁)

两次碰撞都将放置在事件处理队列中,以供演员在特定的时间和地点(和速度)对对象进行操作。只需记住,处理程序应仅处理对象发生的情况。一个处理程序甚至有可能将另一个处理程序放置在队列上,以根据参与者或所作用对象的属性来处理碰撞的其他影响。(炸弹爆炸了,现在产生的爆炸必须测试与其他物体的碰撞。)

编写脚本时,请使用具有属性的标签,这些属性将被您的代码转换为接口实现。然后在您的处理代码中引用接口。

例如,一把剑可能带有近战标记,这会转换为名为近战的接口,该接口可能会扩展DamageGiving接口,该接口具有使用固定值或范围等定义的损伤类型和损伤量属性。炸弹可能具有爆炸标签,其爆炸接口还扩展了DamageGiving接口,该接口具有半径以及伤害扩散速度的附加属性。

然后,您的游戏引擎将根据接口而不是特定的对象类型进行编码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.