Class relationships (how can an instantiated class modify the attributes of its "owner"?)

Started by
7 comments, last by Serapth 10 years, 9 months ago

I have been working on a 2D platformer for some time now, and I've managed to lay out the basic framework pretty well. Right now I'm however

facing a problem. I'm trying to implement classes for things like animation, physics, input and AI. Due to their nature they need to be able to edit

the attributes of their "owner" (I don't know that proper word, but I mean the class that has instantiated it (it being physics for example)).

I know the editing can't be done directly, so I though about passing the pointer to the "owner" object to the update function in the target class, but I don't know where I can get this pointer from.

I also drew a rough illustration of the relevant class hierarchy to help explain my point (vertical arrows represent inheritance (or the "is a" -relationship) and horizontal arrows represent a "has a" -relationship (composition I believe it's called)).

My idea of the class hierarchy:

http://i.imgur.com/ytUZcnv.png?1

Also here's some relevant code (I've removed the irrelevant parts):

GameObject class (header):


class GameObject
{
public:
    GameObject();
    virtual ~GameObject();

    virtual void            Draw(sf::RenderWindow& window);
    virtual void            Update(sf::Time& time);

    virtual void            SetPosition(float x, float y);
    virtual sf::Vector2f    GetPosition() const;

    virtual bool            GetIsMoving() const {return isMoving;}
    virtual void            SetIsMoving(bool val) {isMoving = val;}

    virtual bool            IsAnimated() {return animated;}
    virtual void            SetAnimated(bool val) {animated = val;}

private:
    sf::Sprite              sprite;
    sf::Texture             texture;
    bool                    isMoving;
    bool                    animated;
protected:
    Animation               Anim;
    Physics                 Phys;
};

Player class (header):


class Player : public GameObject
{
public:
    Player();
    ~Player();

    void     Update(sf::Time& time);
    void     Draw(sf::RenderWindow& window);

    float    GetYVelocity() const;
    void     SetYVelocity(float yVelocity) {this->yVelocity = yVelocity;}
private:
    float    yVelocity;
    int      jumpHeight;
};

Physics class (header):


class Physics
{
public:
    Physics();
    ~Physics();

    void     Update(sf::Time& time);

    float    GetGravity() {return gravity;}
    void     SetGravity(float gravity) {this->gravity = gravity;}
private:
    float    gravity;
protected:

};

Physics class (source):


void Physics::Update(sf::Time& time)
{
    static bool isLoaded = false;

    if (!isLoaded)
    {
        std::cout << "Physics loaded!" << std::endl;
        isLoaded = true;
    }
    //Let's say I wanted to edit the player's yVelocity here, what would I do?
}

Physics::Update is called from Player::Update by Phys.Update(time).

I'd love to get this resolved as it has effectively halted all progress on the game, thank you.

Advertisement

Would it not be better if the physics class holds all the information involving the physical condition of a game object such as velocity, gravity and have the physics class hold all the methods revolving around this specific topic. This way, the Update function, could also tell the physics object: "Dude, see if you need to alter our velocity".

This might be a silly way to do it since I am not an expert, but that is how I would have done it at a first glance.

PS: Point it down if it is a stupid suggestion, this way I know I was being silly. *grins*

It is pretty common to pass in the owning object to the constructor. So it's quite reasonable for the Physics constructor to take a GameObject as a paramater. The only catch is you can start creating some weird dependencies.

If creating hard dependencies ( like Physics needing to know about GameObject ) you can look into Dependency Injection a pretty common design pattern. In your case that may be overkill though. While creating dependencies between objects is to be avoid, if two objects SHOULD know about each other, it's no biggy.

That does actually sound like a good idea as it would help decrease the size of the Player class for example, but what would I do if, say the Input class detected a key press and wanted to change the player's velocity or position, which is stored inside the Physics class, maybe I'm overseeing something here but I still see the issue of one class modifying the other?

That does actually sound like a good idea as it would help decrease the size of the Player class for example, but what would I do if, say the Input class detected a key press and wanted to change the player's velocity or position, which is stored inside the Physics class, maybe I'm overseeing something here but I still see the issue of one class modifying the other?

See this is a perfect example of a dependency you should remove. The input class should need to know NOTHING about things that are using it, only that they want to be updated when input occurs. I can't really see your physics class needing to know about Input either, leaving it to the player to get IO notification.

A very common design pattern used here is the Observer pattern, here is a discussion on implementing input using Observer in C++. Truth is though, you dont really need to think about it as a design pattern, that's a scary word. Basically all you do is have an (often global) object that handles input and has the ability to register a callback function/delegate/whatever when input occurs. Whether you implement this using inheritance ( derive from an object that registers with the IO handler... such as a pure virtual class IInputReceiver, then have your Player inherit from that class ) or composition ( Player owns an object that receives input ) or hard coded ( in Player constructor, register a callback function to the IO global ).

PseudoCode way of looking at it, you have:


class IOManager {
  var functionsToCallOnInput[];
  void addFunctionToCallOnInput(); //adds callback function to functionsToCallOnInput array
  void removeFunctionToCallOnInput();
  void update(){
    foreach(function in functionToCallOnInput)
      call function();
}

Then in some function receiving IO, such as player:


class Player {
  Player() {
    IOManager::addFunctionToCallOnInput(this.handleInput);
  }

  handleInput(IODetails data){
    // this function is called each time IO occurs
  }
}

I can do that in actual code if that didn't make sense. The downside to the above solution is it makes a hard dependency between the Player and the global IOManager object. This may not be a bad thing, and if it is a bad thing, can be designed around as well. Eventually you just say "Oh screw it, this is OK to be global!".

To be completely honest here, I didn't understand many of the things related to the Observer pattern (I'll definitely keep it in mind though), I understood the point the writer in the second link made, but that's about it. I'm still new to programming so I'll have to implement things that I can completely comprehend, even if they're inefficient and potentially dangerous, and then learn more advanced things as I go.

My current plan is to have the object pointer passed to the constructors of the various classes like you said earlier, so they can modify the owner object's attributes and access it's methods, but I didn't really understand what you meant with weird depencies.

Here is some code (not actual) of what I meant by that.

GameObject.h:


class GameObject
{
public:
    GameObject();
    virtual ~GameObject();

private:
    sf::Sprite              sprite;
    sf::Texture             texture;
    bool                    isMoving;
    bool                    animated;
protected:
    Animation               Anim(this);
    Physics                 Phys(this);
};

For the player there would also be Input Input(this); and for the enemy: AI AI(this)

And then take the Input constructor for example:


Input::Input(GameObject* obj)
{
      // If I understood correctly I need to create a pointer inside the Input class and set its address to the GameObject pointer's address here?
}

So my plan is to also update each aspect individually, so Player::Update would look like this (again not actual code):


Player::Update(sf::Time& time)
{
    ...
    ...
    ...
    Input.Update(); // For the Enemy::Update this would be something like AI.Update() as both the Input and AI handle the object's movement
    Phys.Update();
    Anim.Update();
}

And finally here's an idea of what the Input.Update() might look like


Input::Update()
{
    If (keyPressed::A)
          //Logic to move the player left
    if (keyPressed:D)
          //Logic to move the player right
    if (keyPressed::K)
          //Logic to attack for example
     ...
     ...
     and so on...
}

So the grand idea behind updating like that is: movement --> physics --> animation

First the object asks to move, and if the physics allow it (doesn't collide for example) then it's moved and some animation plays, I hope that makes any sense in terms of programming.

Sorry about the lengthy post, and I'd like to thank you Serapth for taking the time to write those in-depth answers.

See, this is a perfect example of where dependencies can bite you in the ass:


Player::Update(sf::Time& time)
{
...
...
...
Input.Update(); // For the Enemy::Update this would be something like AI.Update() as both the Input and AI handle the object's movement
Phys.Update();
Anim.Update();
}

Consider this for a second...

What happens to your code if you want to add another player? You have a situation here where a Player object is controlling the Input, Physics and Animations... does that really make sense at the end of the day? Not really, for exactly the reason I gave... now if you have two players, this logic needs to be reworking, or all of these things are going to be called twice.

Input should also not need to know about GameObject. Think about it this way... every time you have a dependency like this, its something else that breaks if GameObject is changed. Plus, what happens in the future if you want to apply input to a non-GameObject objecct?

Truth of the matter is, you learn how to design by making shit designs and learning from it. If this stuff is all over your head, ignore it for now and keep coding. When you run into the limitations of your design THEN you will understand the use and purpose behind design patterns. Until you actually run into a problem the pattern solves, you probably wont understand the design pattern, this completely makes sense.

So, I guess what I am trying to say is... if you are really new to coding, dont worry about it too much. You will learn from your own failures and this is a good thing.

I'm finally starting to grasp what you mean, and the problems I'm facing and going to face with my current design (solve one problem and two more appear). I managed to make

it so that the subclasses (physics, animation etc.) can modify the owner class, but only if it's a GameObject, so that means no physics for player and enemies, trying to fix that issue just kept creating more.

I did in fact try to create another player, but it wouldn't get drawn, and with my current design I suppose I would've controlled both players at the same time with the same keys.

I presume a step in the right direction would be creating a single class that manages whatever is in its area of responsibility, and then make that affect the objects?

But yeah, I pretty much wrote myself into a corner. Maybe I should be patient and shelf this project for now and finish reading up on OOP and then maybe read about basic game design. Thanks for your advice, you probably saved me a lot of time.

I presume a step in the right direction would be creating a single class that manages whatever is in its area of responsibility, and then make that affect the objects?

.

This is exactly what some people prescribe to. It's called the single responsibility principle and some people on this forum absolutely swear by it. It is something to aim for certainly, but I wouldn't blindly adhere to it, especially while starting out.

In your case, your exact BEST thing to do is continue creating your program, write yourself into a corner, then realize why. All the reading in the world about OO design wont be a good substitute for actually being in situations where your design broke. Nothing helps you understand something better than actually needing it.

This topic is closed to new replies.

Advertisement