Component/Strategy C++ 中的模式、设计和实现

Component/Strategy Pattern in C++, Design and Implementation

我有一个 C++ 项目,使用 OpenFrameworks 进行渲染,在 window 中弹跳几个球(圆圈)。它们是使用组件模式(有时称为策略模式)分别封装外观和行为来实现的。这使得外观(此处:Amiga Ball)和行为(此处:重力弹跳)在运行时可交换。

模式本身的灵感来自 Robert Nystrom's book Game Programming Patterns 中的组件模式章节。 按下一个键后,"bouncing with gravity" 的行为与 "bouncing with no-gravity" 交换(这可能是我编程而不是设计的原因)。 我想使用智能指针实现该模式(在这种情况下 unique_ptr 可以,因为没有共享,并且一个组件对象始终由 GameObject 唯一保存)。 我的代码中有几个 "ugly" 部分与 unique_ptr 的使用以及我对组件使用多态性但似乎有时必须将它们视为派生的 class 对象而不是基础-class.

我想知道它是否是一个好的和可行的设计和实现w.r.t。一般模式的设计和实现,特别是智能指针的使用。

注意:我不关心这里的常量正确性,尽管我应该关心。所以我确定那里有很多常量,但请不要 bash 我只是为了这个,除非它会影响其他问题。

所以这里...

我们有一个 class 游戏对象,其中包含屏幕上任何对象应具有的最少信息: GameObject.hpp:

    class GameObject {
    public:
        float x;
        float y;

        GameObject(unique_ptr<PhysicsComponent> physics, unique_ptr<GraphicsComponent> graphics);
        GameObject();
        GameObject(float x, float y);

        void update();
        void draw();
        void setPhysicsComponent(unique_ptr<PhysicsComponent> pc);
        void setGraphicsComponent(unique_ptr<GraphicsComponent> pc);
        unique_ptr<PhysicsComponent>& getPhysicsComponent();
        unique_ptr<GraphicsComponent>& getGraphicsComponent();

    private:
        unique_ptr<PhysicsComponent> physics_;
        unique_ptr<GraphicsComponent> graphics_; 
    };

GameObject.cpp:

GameObject::GameObject(unique_ptr<PhysicsComponent> physics, unique_ptr<GraphicsComponent> graphics) : physics_(move(physics)), graphics_(move(graphics)) {}

GameObject::GameObject() {}

GameObject::GameObject(float x, float y) : x(x), y(y) {}

void GameObject::update(){
    physics_->update(*this);
}

void GameObject::draw() {
    graphics_->draw(*this);
}

void GameObject::setPhysicsComponent(unique_ptr<PhysicsComponent> pc) {
    physics_ = std::move(pc);
}

void GameObject::setGraphicsComponent(unique_ptr<GraphicsComponent> gc) {
    graphics_ = std::move(gc);
}

std::unique_ptr<GraphicsComponent>& GameObject::getGraphicsComponent() {
    return graphics_;
}

std::unique_ptr<PhysicsComponent>& GameObject::getPhysicsComponent() {
    return physics_;
}

然后是组件的基础 class(在这种情况下,让我们关注物理组件,因为图形组件是等效的):

class GameObject;

class PhysicsComponent {
    public:
        virtual ~PhysicsComponent() {std::cout<<"~PhysicsComponent()"<<std::endl;}
        virtual void update(GameObject& obj) = 0;

        void setXSpeed(float xs) {xSpeed = xs;};
        float getXSpeed() {return xSpeed;};
        void setYSpeed(float ys) {ySpeed = ys;};
        float getYSpeed() {return ySpeed;};

    protected:
        float ySpeed;
        float xSpeed;
};

PhysicsComponent 的具体实现如下所示:

class GravityBouncePhysicsComponent : public PhysicsComponent {
    public:
        virtual ~GravityBouncePhysicsComponent()  {std::cout<<"~GravityBouncePhysicsComponent()"<<std::endl;};
        GravityBouncePhysicsComponent();
        GravityBouncePhysicsComponent(float radius, float xSpeed, float ySpeed, float yAccel);
        virtual void update(GameObject& obj);
        void setRadius(float r) {radius = r;};
        float getRadius() {return radius;};
    private:
        // Physics
        float yAcceleration;
        float radius;
};

在 GravityBouncePhysicsComponent.cpp 中,空构造函数创建了这样一个组件,它具有 xSpeed 和 ySpeed(存在于基础 class 中)和 yAcceleration(仅在派生组件中有意义 [=71] =]). 然后还有一个名为 FloatBouncePhysicsComponent 的类似代码。

现在我想在主程序中使用这些class,正好是OpenFrameworks中的ofAppclass。头文件看起来像这样(省略了一些无关紧要的东西):

class ofApp : public ofBaseApp{
    public:
        void setup();
        void update();
        void draw();

        void keyPressed(int key);
        // ... lots of other event functions for mouse etc.

    private:
        std::vector<GameObject> balls;
        GameObject createAmigaBall(float x, float y);

        // more variables and some methods here ...

};

现在在 ofApp.cpp 文件中它开始变得丑陋。例如,在创建一个新球的辅助函数中,它看起来像这样:

GameObject ofApp::createAmigaBall(float x, float y) {
    GameObject go(x,y); // create an empty GameObject on the stack at position x,y
    go.setGraphicsComponent(std::make_unique<AmigaBallGraphicsComponent>());

    if(gravity) {
        go.setPhysicsComponent(std::make_unique<GravityBouncePhysicsComponent>());
        float r = (static_cast<AmigaBallGraphicsComponent*>((go.getGraphicsComponent()).get()))->getRadius();
        (static_cast<GravityBouncePhysicsComponent*>((go.getPhysicsComponent()).get()))->setRadius(r);
    } else {
        go.setPhysicsComponent(std::make_unique<FloatBouncePhysicsComponent>());
        float r = (static_cast<AmigaBallGraphicsComponent*>((go.getGraphicsComponent()).get()))->getRadius();
        (static_cast<FloatBouncePhysicsComponent*>((go.getPhysicsComponent()).get()))->setRadius(r);
    }

    return go; // calls the copy constructor for return value, so we have no dangling pointers or refs
}

此处 AmigaBallGraphicsComponent 空构造函数生成随机半径。这个半径属于这个派生的class,因为它肯定不在每个GraphicsComponent中。 然而,这导致不得不笨拙地从生成的组件中提取半径,并访问原始指针,只是为了在 GravityBouncePhysicsComponent 中设置相同的半径。

如果您认为那很丑陋,请查看此片段:

void ofApp::keyPressed(int key){
    if(key == 'a') {
        if(gravity) {
            for(int i=0; i<balls.size(); i++) {
                unique_ptr<GravityBouncePhysicsComponent> opc (static_cast<GravityBouncePhysicsComponent*>((balls[i].getPhysicsComponent()).release()));
                float r = opc->getRadius();
                float xs = opc->getXSpeed();
                float ys = opc->getYSpeed();
                balls[i].setPhysicsComponent(std::make_unique<FloatBouncePhysicsComponent>(r,xs,ys));
            }
        } else {
                // similar code for the non-gravity case omitted ...
            }
        }
        gravity =!gravity;
    }
}

而且,好像转换为派生-classes 的东西还不够,结果证明这段代码不起作用

auto gc = std::make_unique<AmigaBallGraphicsComponent>();
go.setGraphicsComponent(gc);

虽然这个是。什么……?

go.setGraphicsComponent(std::make_unique<AmigaBallGraphicsComponent>());

这不应该一模一样吗?

在此先感谢您对整个混乱局面的任何见解(或者是吗?)。

我认为您的大部分问题都来自这样一个事实,即您让所有权 (std::unique_ptr) 和类型擦除(多态性)问题从您的 API 中渗出,这需要很多不相关的生命周期管理和用户端的失望。 getGraphicsComponent(及其伙伴)可以这样实现:

template <class DerivedGraphicsComponent = GraphicsComponent>
auto &getGraphicsComponent() {
    return static_cast<DerivedGraphicsComponent &>(*graphics_);
}

这使您能够编写:

float r = go.getGraphicsComponent<AmigaBallGraphicsComponent>().getRadius();

或者省略 <AmigaBallGraphicsComponent> 并接收普通的 GraphicsComponent &

您确实失去了 GameObject 撤销其组件之一的所有权的能力,并且 return 通过 std::unique_ptr 取消它的所有权,但看起来您实际上并不需要它。您使用该功能的代码段最终会执行以下操作:

balls[i].setPhysicsComponent(std::make_unique<FloatBouncePhysicsComponent>(r,xs,ys));

... 无论如何都会通过 graphics_ 的移动赋值运算符破坏前一个组件。如果您确实需要该功能,请务必添加另一个功能:

template <class DerivedGraphicsComponent = GraphicsComponent>
auto releaseGraphicsComponent() {
    return std::unique_ptr<DerivedGraphicsComponent>(
        static_cast<DerivedGraphicsComponent *>(graphics_.release())
    );
}