检测形状之间碰撞的设计模式

12

我使用不同形状进行碰撞检测(矩形圆形锥形环形等等)。所有这些形状都派生自基本的抽象Shape类。我的游戏对象具有类型为Shape的属性。

class GameObject
{
    (...)
    public Shape CollisionShape { get; set; }
}

在初始化过程中,我决定每个对象将使用什么形状,例如:

GameObject person = new GameObject();
person.CollisionShape = new Circle(100); // 100 is radius

现在当我想要检查两个对象是否相交时,我使用以下类:

public class IntersectionChecker
{
   public bool Intersect(Shape a, Shape b)
   {
      Type aType = a.GetType();
      Type bType = b.GetType();

      if( aType == typeof(Rectangle) && bType == typeof(Rectangle))
          return Intersect(a as Rectangle, b as Rectangle);

      if( aType == typeof(Rectangle) && bType == typeof(Circle))
          return Intersect(a as Rectangle, b as Circle);

      // etc. etc. All combinations      
   }

   private bool Intersect(Rectangle a, Rectangle b)
   {
      // check intersection between rectangles
   }
}

我的代码看起来像:

IntersectionChecker ic = new IntersectionCHecker();
bool isIntersection = 
    is.Intersect(personA.CollisionShape, personB.CollisionShape);

有没有更好的方法来实现我的目标,而不需要在IntersectionChecker类中进行大量的“if”检查和类型检查?

编辑:

请注意,检查形状A和B之间的交集的方法也可以用于检查形状B和A之间的交集。在许多答案中(感谢您所有的想法!),建议从形状本身而不是IntersectionChecker对象调用交点检查。我认为这将强制我复制代码。现在我可以按照以下方式进行操作:

  if( aType == typeof(Rectangle) && bType == typeof(Circle))
      return Intersect(a as Rectangle, b as Rectangle);

  if( aType == typeof(Circle) && bType == typeof(Rectangle))
      return Intersect(b as Rectangle, a as Circle); // same method as above

看看我的建议,使用反射。如果你使用反射来分派调用,那么对于类型为矩形/圆和圆/矩形的对象,可以轻松地使用相同的方法。 - Achim
4个回答

13

你可以使用访问者模式(Visitor Pattern)这里有一个C#的例子

这将允许你简单地拥有Shape.Intersect(Rectangle),Shape.Intersect(Circle)等每个派生形状实现的方法。它会防止你对类型进行任何反射,但代价是多调用一个方法。

编辑 - 这里是一个示例实现,如果没有任何共享功能需要放在Shape中,最好使用一个接口IShape,但我只是插入了一个抽象基类。

public class GameObject
{
    private Shape _collisionShape;

    public GameObject(Shape collisionShape)
    {
        _collisionShape = collisionShape;
    }

    public bool Intersects(GameObject other)
    {
        return _collisionShape.IntersectVisit(other._collisionShape);
    }
}

public abstract class Shape
{
    public abstract bool IntersectVisit(Shape other);
    public abstract bool Intersect(Circle circle);
    public abstract bool Intersect(Rectangle circle);
}

public class Circle : Shape
{
    public override bool IntersectVisit(Shape other)
    {
        return other.Intersect(this);
    }

    public override bool Intersect(Circle circle)
    {
        Console.WriteLine("Circle intersecting Circle");
        return false; //implement circle to circle collision detection
    }

    public override bool Intersect(Rectangle rect)
    {
        Console.WriteLine("Circle intersecting Rectangle");
        return false; //implement circle to rectangle collision detection
    }
}

public class Rectangle : Shape
{
    public override bool IntersectVisit(Shape other)
    {
        return other.Intersect(this);
    }

    public override bool Intersect(Circle circle)
    {
        Console.WriteLine("Rectangle intersecting Circle");
        return true; //implement rectangle to circle collision detection
    }

    public override bool Intersect(Rectangle rect)
    {
        Console.WriteLine("Rectangle intersecting Rectangle");
        return true; //implement rectangle to rectangle collision detection
    }
}

以下是调用它的示例代码:

GameObject objectCircle = new GameObject(new Circle());
GameObject objectRect = new GameObject(new Rectangle());

objectCircle.Intersects(objectCircle);
objectCircle.Intersects(objectRect);
objectRect.Intersects(objectCircle);
objectRect.Intersects(objectRect);

生成输出:

Circle intersecting Circle
Rectangle intersecting Circle
Circle intersecting Rectangle
Rectangle intersecting Rectangle

访问者模式是我一直在寻找的东西!无论如何,我不得不将您的解决方案与我的当前代码混合在一起,以避免重复的代码。 - zgorawski

5
你可以让你的Shape类执行碰撞检查,为Shape添加一个IntersectsWith(Shape other) 方法。我还建议在GameObject中添加一个IntersectsWith(GameObject other)方法,这样可以使CollisionShape保持私有。

1

如果检查必须存在于某个地方。

您可以向Shape添加一个Intersects方法:

abstract class Shape
{
    public abstract Boolean Intersects(Shape other);
}

然后在IntersectionChecker中让您的Intersect方法是public static的,并为每种具体形状实现Intersects方法,如下所示:

class Rectangle : Shape
{
    public override Boolean Intersects(Shape other)
    {
        if (other is Rectangle)
        {
            return IntersectionChecker.Intersect(this, (Rectangle)other);
        }
        else if (other is Circle)
        {
            return IntersectionChecker.Intersect(this, (Circle)other);
        }

        throw new NotSupportedException();
    }
}

1
这个基于对象类型的 if..else if...else 结构通常可以被替换为虚函数。 - Uwe Keim
@Uwe Keim,是的,在这种情况下,您将会有一个无限递归,因为那个虚函数将是相同的Shape.Intersects实现。或者我理解错了吗? - Loki Kriasus
“IntersectionChecker” 已经似乎检查了类型,所以你在这里进行了双重检查? - Uwe Keim
@Uwe Keim,不是这种情况-在这种情况下,“IntersectionChecker.Intersect”将调用其类型化的私有方法(我建议将其制作为“public static”)。而且应该删除“IntersectionChecker.Intersect(Shape,Shape)”。 - Loki Kriasus
@Lori - 真的,抱歉,我是一个非常糟糕的程序员! :) - Will A
显示剩余2条评论

1

针对您的问题,没有简单的内置解决方案。您需要的是所谓的“双重分派”,这仅在像Smalltalk或Lisp这样的语言中得到支持。所有提出的解决方案都将强制您更改所有派生类,如果您添加一个新类。那是糟糕的代码!

我会这样解决问题:首先实现您的Shape派生类,不包含任何交集代码。然后实现一个Intersection类,如下所示:

public class Intersection {
    public bool Intersect(Shape a, Shape b) {....}

    private bool Intersect(Rectangle a, Circle b) {...}

    private bool Intersect(Circle a, Circle b) {...}
}

公共方法分析传入的形状并将工作分派(->双重分派)到匹配的私有方法中。其中包含原始交集逻辑。Intersect的实现不需要“ifs”。您可以使用反射来查找最佳匹配方法。详细信息取决于您的确切要求以及如何权衡复杂性和性能。但是,使用简单直接的实现很容易入手。因为所有内容都封装在一个地方,所以稍后进行优化很容易。这是我认为的一个好方法。;-)

你的“双重分派仅受支持”评论非常误导人。虽然C语言类似语言不真正支持双重分派,但仍然可以模拟。wiki double dispatch你的解决方案是糟糕的代码,因为你必须编写几乎两倍于实际需要的函数:Intersect(Rectangle, Circle)Intersect(Circle, Rectangle)。如果您将来保持更改最小化,则访问者模式效果很好。值得一提的是,“所有内容都封装在一个单一位置”不是一个好的范例。 - josaphatv
你能解释一下为什么访问者模式需要更少的函数进行更改吗?我甚至没有提到你举的例子中的函数! - Achim
访问者模式不需要太多更改。我的意思是,如果您只需要支持一些简单的形状,比如圆形、矩形和三角形,并且不打算添加更多的形状,那么访问者模式就可以很好地工作。我知道您没有提到我举的例子。我对反射不是很熟悉,但我认为您仍然需要检查类型以找到参数的顺序(无论您是否要调用“Intersect(Rectangle, Circle)”还是“Intersect(Circle, Rectangle)”)。您说您不需要“if”,但我认为您需要“if”或重复的代码。 - josaphatv
假设您只交叉两个元素,您可以使用签名的排序类型作为字典中的键。值是该类型组合的交集方法。如果您现在想要交叉两个元素,您只需根据它们的类型“计算”键,获取交集方法并将对象传递给它们即可。不需要任何if来将调用分派到正确的交集方法。 - Achim
但是现在你正在向一个已经丑陋的API(反射)中添加丑陋和不直观的过程(用于类型的排序字典查找)。访问者模式虽然可能很快变成维护噩梦,但对于支持的类对数量较少的情况下,更容易理解和遵循。 - josaphatv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接