SFML:如何检查一个点是否包含在一组变换后的可绘制对象中

4

上下文

我有一个代表文本框的类。文本框包含标题、一些文本和一个矩形来围住它。目前它只显示自己:

struct Textbox : public sf::Drawable, public sf::Transformable{
  sf::Text header;
  sf::Text text;
  sf::RectangleShape border;

  Textbox(){
    // set relative locations of the members
    header.setPosition(0,0);
    auto header_bounds = header.getGlobalBounds();
    // the text should be just below the header
    text.setPosition(0, header_bounds.top + header_bounds.height);
    auto source_bounds = text.getGlobalBounds();

    // this function just returns a rectangle enclosing two rectangles
    sf::FloatRect rect = enclosing_rect(header_bounds, source_bounds);
    // this function sets the position, width and length of border to be equal to rect's.
    setRectParams(border, rect);
  }

  void draw(sf::RenderTarget& target, sf::RenderStates states){
    states.transform = getTransform();
    target.draw(header,states);
    target.draw(text,states);
    target.draw(border,states);
};

问题

我想要的

我想添加一个contains方法。如果coor在方框的边界内,则应返回true。以下是我的朴素实现:

bool Textbox::contains(sf::Vector2i coor) const {
  return border.getGlobalBounds().contains(coor.x, coor.y);
}

为什么这个实现不起作用

当我通过Transformable的非虚拟函数移动Textbox时,这个实现就会出问题。 Textbox会移动,并且它也会绘制转换后的形状。但是!它实际上并没有对它们进行转换!它只是显示它们已经被转换了。所以border甚至不知道它已经被移动了。

可能的解决方案

  1. 我可以将Transformable API的所有功能添加到这个类中,从而遮蔽它们并在每个成员上自己调用transform。我不喜欢这样做,因为它让我写了比我想要的多得多的代码。这也引发了如何处理双重转换(Textbox和其成员的转换)的问题。
  2. 我可以编写一个完全不同的类Group,它包含drawabletransformable的向量,并具有所有的遮蔽API机制。剩下的就是继承它。这听起来实际上并不那么糟糕。
  3. 我听说过实体系统组件 - 它听起来有点过度设计。
  4. 当调用contains时应用变换。该函数是const - 它是一个查询。此外,在看似随机的调用上更新数据是不好的设计。
  5. 就像以前一样,只是变换适用于函数本地矩形。这也有点不对劲 - 为什么我要在整个Textbox上调用变换函数,以便在每个方法调用(到目前为止只有它的drawcontains,但未来谁知道)上应用它们。
  6. 使成员mutable,并在draw方法内部以某种方式对其进行变换。这听起来有点hackish。

问题

如何通过人性化的API将变换分组到多个实体上?

2个回答

0

唯一真正需要“更改”的方法,但公平地说,您可以添加自己的内容是getGlobalBounds()

当您从sf::Transformablesf::Drawable继承时,您应该将基类(您的Textbox结构体)视为一个形状本身,因此您只需要调用myTextbox.getGlobalBounds().contains(x,y),其中myTextbox是一个Textbox

使用您自己的代码:

struct Textbox : public sf::Drawable, public sf::Transformable{
    sf::Text header;
    sf::Text text;
    sf::RectangleShape border;

sf::FloatRect getGlobalBounds() const {
    auto header_bounds = header.getGlobalBounds();
    auto source_bounds = text.getGlobalBounds();

    sf::FloatRect rect = enclosing_rect(header_bounds, source_bounds);
    //Don't really know what it does but let say that it returns Top and Left as 0, and calculates Height, Width.
    return sf::FloatRect(getPosition(), sf::Vector2f(rect.width,rect.height));
    }
};

但是在计算全局边界时,您仍然需要管理旋转、调整大小等。

编辑:

实现旋转和缩放的一种方法。

sf::FloatRect getGlobalBounds() const {
    auto header_bounds = header.getGlobalBounds();
    auto source_bounds = text.getGlobalBounds();

    sf::FloatRect rect = enclosing_rect(header_bounds, source_bounds);
    //Don't really know what it does but let say that it returns Top and Left as 0, and calculates Height, Width.
    sf::RectangleShape textbox(sf::Vector2f(rect.width, rect.height));
    //at this point textbox = globalBounds of Textbox without transformations
    textbox.setOrigin(getOrigin());//setOrigin (point of transformation) before transforming
    textbox.setScale(getScale());
    textbox.setRotation(getRotation());
    textbox.setPosition(getPosition());
    //after transformation get the bounds
    return textbox.getGlobalBounds();
}

我调用Textbox对象上的转换仍然不适用于成员。因此,getGobalBounds将返回成员未更改的包围矩形。 - Eyal Kamitchi
是的,因为你不需要改变成员的属性,因为它们都是相对于文本框本身的,除非你想一个一个地改变成员,这将需要手动更改它们,我是指相对于文本框更改它们。 - Dnafiqvss
我们可以使用return sf::FloatRect(getPosition(), sf::Vector2f(rect.width * getScale(), rect.height * getScale());以考虑缩放因素,不是吗?但是,我是在模拟运动,就好像它已经发生了一样,我不确定这种方法是否可以随着项目需求的增加而扩展(无意冒犯)。 - Eyal Kamitchi
然而, getGlobalBounds 将返回正确的 FloatRect,因为它根据 Textbox[0,0,width,height] 计算出围绕其的包围矩形,然后通过将 TopLeft 设置为 Textbox 的位置来将其转换为世界坐标 **(Textbox 的原点必须为 [0,0]**)。 - Dnafiqvss
你不需要在计算矩形的过程中进行缩放。更简单的方法是将 FloatRect 转换为 sf::RectangleShape,这样您就不必担心编写自己的旋转/缩放代码,之后调用 rectangleShape.setRotation(getRotation()) 等等设定旋转角度,最后返回该 RectangleShapeglobalBounds 即可。 - Dnafiqvss
这有点过度设计了...如果你想要自己的 getGlobalBounds,你只需要 return this->getTransform().transformRect(border.getGlobalBounds());。然而,如果你旋转文本框,这仍然不起作用,因为一个旋转的形状的全局边界(即使使用你的代码)包含在形状本身之外的点。 - Miguel

0

解决方案可能比您期望的要简单得多。不必对可转换的子元素/成员应用所有变换,只需反向变换要检查的点(将其转换为本地空间)。

试试这个:

bool Textbox::contains(sf::Vector2i coor) const {
  // Get point in the local space of the rectangle
  sf::Transform inverseTr = this->getInverseTransform();
  sf::Vector2f pointAsLocal = inverseTr.transformPoint(coor.x, coor.y);
  
  // Check if the point, now in local space, is containted in the rectangle
  return border.getLocalBounds().contains(pointAsLocal);
  //            ^
  //            Important! Use local bounds here, not global
}

为什么这个有效?

数学!

当你使用变换矩阵时,可以将它们视为空间之间的门户。你有一个本地空间,其中没有应用任何变换,还有一个最终空间,其中应用了所有变换。

可变形成员的全局边界问题在于它们既不属于本地空间也不属于最终空间。它们只是一个矩形,可能在中间空间中限制形状,甚至不考虑旋转。

我们在这里做的是将存在于最终空间中的坐标带到矩形的本地空间中,这要归功于逆变换矩阵。因此,无论你对矩形应用多少次平移、旋转或缩放(甚至如果你自定义了矩阵,则包括扭曲),逆矩阵都会将该点带到一个新空间,你可以检查它是否属于该空间,就好像从未应用过任何变换一样。


仍然我会在每次调用函数时支付转换的成本。将来如果我想要一个名为getsizeintersects的函数,我将需要对我的边界成员进行前向变换。 - Eyal Kamitchi
转换的成本非常非常低,您可以每帧执行数千次而不会注意到它。关于 getsizeintersects... 您没有提到。在 getsize 的情况下,您只需获取 borders.width * getScale().x * borders.height * getScale().y。对于 intersects... 它取决于您想要与之相交的形状。 - Miguel

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