形状碰撞 C++ 的设计模式

Posted

技术标签:

【中文标题】形状碰撞 C++ 的设计模式【英文标题】:Design pattern for Shape Collision C++ 【发布时间】:2013-12-06 12:13:09 【问题描述】:

所以我目前正在研究从 Shape 继承的对象(圆形、矩形等)的最佳设计。我目前正在使用动态调度,但这需要在添加新形状时进行大量修改。理想情况下,我想要一些类似的东西,这样当我添加一个新形状时,我就不必花费很长时间来更新多个文件。

public class Shape() 
    public:
    virtual bool detectCollision(Shape *shape);
;

然后让其他类能够以不同的方式派生它,IE

public class Circle : public Shape 
    public:
    bool detectCollision(Square *square);
    bool detectCollision(Circle *circle);
;

如果可能的话,我想避免使用冗长的if/else 语句,并且不认为这一切的逻辑都应该在Shape 内部使用某种形式的dynamic_casting 来完成,理想情况下Shape 会不知道是什么继承了它。

是否有适合这些需求的解决方案?我听说过Templates,但我不确定我将如何在这里实现它,也许有人可以举个例子吗?

【问题讨论】:

你考虑过工厂设计模式吗?您可以将其与模板实现一起使用。 这里似乎需要双重调度方法。 Alexadrescu 撰写的 Modern C++ design 第 11 章准确描述了您面临的问题以及一个非常优雅的解决方案。如果我没记错的话,Alexandrescu 的库“Loki”中已经实现了双重调度方法:loki-lib.sourceforge.net。 嘿@linuxfever,这实际上是我想要避免的。双重调度意味着我必须确保 Shape 新所有派生它的类,并且我必须在多个地方更新代码。 嘿@Nik,我还没有考虑过工厂设计模式。这可能是我的问题的一个很好的解决方案 @Thomas:这不一定是真的。 Alexandrescu 的书提出了一个解决方案,其中您有一个动态映射,其键是 type_identidiers(例如“Circle”、“Rectangle”等)和指向适当函数的映射值。最好的部分是,当您在层次结构中添加新类时,您不需要更改现有代码。如果您进行谷歌搜索,您可以找到该书的章节可供下载。 【参考方案1】:

您好 Thomas,感谢您的提问,

通常使用 dynamic_cast 会更糟。大多数类型特定的方法调用应该在编译时而不是运行时解决。因此,您必须使用 RTTI,浪费性能,并且您的代码不像编译器检查的类型安全那样容易出错。

class Shape

public:
  bool colidesWithOtherShape( Shape other )  return CollisionManager::areShapesCollding( *this, other );  


class CollisionManager

public:
  template< typename _T, typename _N >
  static bool areShapesColliding( _T, _N );
;

template <>
bool CollisionManager::areShapesColliding( Circle c, Circle c2 )

  return Point::distance( c.getMidPoint(), c.getMidPoint() ) < c.getRadius() + c2.getRadius(); 

这样你的代码是类型安全的,如果你愿意,你的所有碰撞检测都可以在一个或多个文件中实现,但请注意你的命名空间,这可能会导致方法查找出现一些奇怪的行为。

如果有多个形状,则将具有特定形状作为第二个参数的所有方法映射到以形状作为第一个参数的相同方法。

template< typename _T >
bool CollisionManager::areShapesColliding( _T shape, Circle c )

  return CollisionManager::areShapesColliding( c, shape );

现在只需为所有其他形状实现 Circle 中的所有碰撞检测。 IE。对于轴对齐的矩形:

template<>
bool CollisionManager::areShapesColliding( Circle circle, Rectangle rectangle )

  Point closestPointToCircle = circle.midPoint();

  if( closestPointToCircle.x() > rectangle.right() ) closestPointToCircle.setX( rectangle.right() );
  else if( closestPointToCircle.x() < rectangle.left() ) closestPointToCircle.setX( rectangle.left() );

  if( closestPointToCircle.y() > rectangle.bottom() ) closestPointToCircle.setY( rectangle.bottom() );
  else if( closestPointToCircle.y() < rectangle.top() ) closestPointToCircle.setY( rectangle.top() );

  return point_distance( closestPointToCircle, circle.midPoint() ) <= circle.radiant();


template<>
bool CollisionManager::areShapesColliding( Rectangle rect1, Rectangle rect2 )

  Point distancePoint = rect1.getMidPoint() - rect2.getMidPoint();
  unsigned long xAxisDistance = abs( distancePoint.x() );
  unsigned long yAxisDistance = abs( distancePoint.y() );

  return xAxisDistance <= ( rect1.width() + rect2.width() ) / 2 &&
         yAxisDistance <= ( rect1.heigth() + rect2.height() ) / 2;

如果您使用 C++11 (-std=c++11 Linkeflag),您可以在编译时检测未实现的冲突检测。

template< typename _T, typename _N >
bool CollisionManager::areShapesColliding( )

  static_assert( false, "your assert" );

对于运行时解决方案,您需要在运行时求解类型。因为我的时间不多了,这里只是一个例子:

template<>
bool CollisionManager::areShapesColliding( Shape* shape1, Shape* shape2 )

  if( ( Circle* c1 = dynamic_cast<Circle*>( shape1 ) ) != nullptr )
  
    if( ( Circle* c2 = dynamic_cast<Circle*> ) != nullptr )
    
      return CollisionManager::areShapesColliding( *c1, *c2 );
    
    else if( ( Square* s2 = dynamic_cast< Square* >( shape2 ) ) != nullptr )
    
      return CollisionManager::areShapesColliding( *c1, *s2 );
    
  
  //else... if first is square...
  return false; //no matching type found

不是一个很好的解决方案,但它可以封装您的问题,并且不会将您的代码弄乱。

希望能帮到你。有其他问题,请告诉我。

迈克尔

【讨论】:

您好,打扰了,您能再提供一点帮助吗?我到底需要输入什么 Circle.cpp 和 Square.cpp 来确保如果用户尝试使用 circle.collideWith(square) 和 square.collideWith(circle),他们会得到相同的结果?这段代码非常好,感谢支持。 首先使用给定的代码扩展您的形状基类。其次创建一个新类,如给出的碰撞管理器。第三,在这种情况下为您的类型提供专业化 _T = circle 和 _N = square 我想我跟着。我已经按照您建议的方式实现了碰撞处理程序,并且还更新了 Shapes.cpp 以使用它。现在,如果我创建 circle.cpp,扩展 Shapes 并运行 circle.detectCollision(square),假设我在 Collisionhandler 中有该算法,它应该可以工作吗? ;D 感谢您的更新。我唯一的问题是现在我收到了error LNK2028: unresolved token (0A000013) "public: static bool __cdecl CollisionManager::detectCollision&lt;class Shape,class Shape&gt;(class Shape,class Shape)" (??$detectCollision@VShape@@V1@@CollisionManager@@$$FSA_NVShape@@0@Z) referenced in function "public: bool __thiscall Shape::detectCollision(class Shape)" (?detectCollision@Shape@@$$FQAE_NV1@@Z)。这是因为我必须实现一些其他功能来确保我们传递正确的类型吗? 这正是 c++ 如此强大且如果使用正确容易出错的主要方面之一:) 我已经用一个如何在系统中实现矩形的示例更新了我的答案。您不必在碰撞管理器的实现文件中实现所有碰撞检测,也不需要将其包装到一个类中。但它让生活更轻松,正如我所说,如果你将专业划分为不同的文件,你需要确切地知道你在做什么。如果您以后有不同的碰撞系统或在 OpenCL 中实现一个,可以将静态碰撞管理器更改为原型...【参考方案2】:

您需要首先决定是否需要在编译时知道对象的类型。即,这种方法:

public class Shape
    public:
    virtual bool detectCollision(Square *square);
    virtual bool detectCollision(Circle *circle);
;

Shape* shape1,shape2;
shape1 = GetNewCircleOrSquareByRandom();
shape2 = GetNewCircleOrSquareByRandom();
shape1->detectCollision(shape2);

不会工作,因为编译器不(也不能)知道哪个对象实际上存在。

正如@linuxfever 所指出的,这是double dispatch 的一个已知问题。 Wikipedia 文章中的链接提供了一种解决方案(与您的非常相似)。

如果您知道在编译时什么与什么发生冲突,那么您可以使用模板,正如您猜对的那样:

// In header
template<class _T1, class _T2> bool detectCollision(_T1* object1, _T2* object2) 
    // Generic implementation
    // Or just an #error "Sorry! Don't know how to collide those two objects"


template<> bool detectCollision(Square* object1, Circle* object2) 
    // Specific implementaiton of Square vs. Circle


template<> bool detectCollision(Circle* object1, Square* object2) 
    return detectCollision(object2,object1);

// etc

【讨论】:

【参考方案3】:

我认为您无法绕过专门的算法来检测/改进碰撞检测。有一些形状 A 和 B 你需要一个知道这两种类型的函数来应用正确的算法。这导致:

class Shape() 
    public:
    // Not virtual!
    // Avoiding pointers.
    bool detectCollision(const Shape&) const;
;

bool Shape::detectCollision(const Shape& other)  const 
   if(is_rectangle(*this) && is_rectangle(other)) 
       return rectangle_rectangle_collision(*this, other); 
       // Where rectangle_rectangle_collision is a local function
       // in a source file 

       // and so on ...

概括它,您可以拥有一个碰撞函数图,在 Shape 中注册新算法,并在 detectCollision 中选择一个专用或通用算法(基于多边形)。

您可能很想使用模板进行碰撞检测,但在动态多态性中您是无用的(除非您分派)。

【讨论】:

绕过算法并不是目标。在我的脑海里,我在想“如果我要把它作为一个库来选择,并且想添加我自己的形状三角形,只能继承现有的形状对象,我该怎么做?”。算法 atm 的逻辑在每个相应的类(圆形、方形等)中定义。 @ThomasAnderson 为什么一个圆应该知道一个矩形(你是在重复圆形和矩形的算法)?

以上是关于形状碰撞 C++ 的设计模式的主要内容,如果未能解决你的问题,请参考以下文章

俄罗斯方块游戏开发系列教程5:形状碰撞检测(下)

使用 JavaFX 检查形状的碰撞

复杂形状的碰撞检测

复杂形状的碰撞检测

俄罗斯方块游戏开发系列教程4:形状碰撞检测(上)

如何使用 HTML5 Javascript Canvas 获得三个碰撞形状的交集并删除未碰撞的部分?