Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » Begginer c++ code design decisions. How do simplify passing objects?

This thread is locked; no one can reply to it. rss feed Print
Begginer c++ code design decisions. How do simplify passing objects?
piskypom
Member #16,813
February 2018

For a while now I've been using a design decision that works, but I think needs to be refactored. I'm pretty sure I'm doing it all wrong.

My goal is to avoid circular dependency, and to do that I'm trying really hard not to have one class rely on another class. The goal is to have one class that holds, creates, and passes all the game objects to the other classes. A top down, sort of structure, except there's only 2 layers, top, and everything else on the same level below.

I still haven't worked that into the code yet, but it will one day.

The problem now is that I cornered myself into situations where I need to pass an object/objects from one function, and then to another.

#SelectExpand
1// game.h ******* 2Game game 3{ 4 Game(); 5 6 Update update; 7}; 8// ************** 9 10// game.cpp ***** 11while(game_is_running) { 12 update(entity_a, entity_b); 13} 14// ************** 15 16// update.cpp *** 17void Update::update(Entity &_ent_a, Entity &_ent_b) 18{ 19 update_ent_a(ent_a); 20 udpate_ent_b(ent_b); 21} 22 23void Update::update_ent_a(Entity &_ent) 24{ 25 // ... 26} 27 28void Update::update_ent_b(Entity &ent) 29{ 30 // ... 31} 32// **************

Hopefully you can see how this look like the code is just trickling down. From game -> update -> update_a/update_b. Sometimes it gets a little tedious. So often I'm tempted to just make every object global and not even use arguments just to avoid so much typing. Which I'm sure isn't very wise in the long run.

Does this make sense? The tricky part about resolving this choice is that I want to keep the tiered approach. This way I can keep my functions smaller and more clear. The update function handles what needs updates. So the game class doesn't directly call the individual components of the game that need updating.

I'm kind of far along and don't have the energy for a major refactoring. Simple suggestions would be appreciated. However, I'm also interested in ideas involving "ground up" restructuring for future projects. Thanks for the help and suggestions, everybody. :)

torhu
Member #2,727
September 2002
avatar

You could put the entities in the Game class, and give the Entity class an update method that takes what it needs as arguments. So no need for the Update class. But you probably need a Game.update method, and some Entity methods to tell the entity that it has crashed, etc.

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

There's nothing wrong with making external controllers. It's the whole principle behind MVC (Model View Controller). However, most classes should know what to do with their data. So I would advise keeping the data model and the controller inside a single class, and having a separate class for the application's View.

In your case, you're using an external Update class, which is equivalent to a Controller. Keeping it separate is fine, but can easily complicate things.

Normally, I just use a hierarchy of composition. The World is composed of a Camera (View), a set of objects (Model), and the code used to control them (Controller).

Also, you should look into inheritance and polymorphism. Instead of having several different and unrelated types of objects, have them all be derived from a base interface. This allows for polymorphic behavior. You need to design your base class carefully though, as it will be part of every class that uses it.

piskypom
Member #16,813
February 2018

@torhu: Actually, what you said is exactly what I have at the moment. I was planning on making an update class, but I'm going to reconsider now.

@Edgar Reynaldo: You gave me a lot to consider. I'm not familiar with much of the terminology you use in reference to game development. I must have skipped that step. And that's really the major flaw to being self-taught is not knowing about some fundamentals and such. I end up re-inventing the wheel more often than not. Which I'm perfectly OK with because it's still rewarding.

I'll just have to spend some time to research the concepts you shared with me. I really do appreciate it, though. Whatever makes me smarter.

In the past I used polymorphism, but didn't really find much need for it. Like, I'd make and Entity parent class, then make: enemy,projectile,hero etc. class inherited from that. But it just didn't seem to be helping me. What exactly is the benefit to polymorphism? Just to be more organized? That could probably be something useful if my projects ever got to such a size that it could need that. But I'm just not seeing it with simple arcade clones like asteroids, arkanoid, galaga. Which aren't actually that simple. For me at least.

I will look further into MVC. If it does what I think I understood, it could make things better organized and take away some headaches.

Thank you both!

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

Polymorphism allows you to reference multiple objects by the same type.

Instead of having :

   a.update();
   b.update();

You have :

BASECLASS* o[2] = {&a , &b};
for (int i = 0 ; i < num_objects ; ++i) {
   o[i]->update();
}

Each object can have it's own update function that automatically does the right thing for the object of that type. Polymorphism is just a selection mechanism for determining dynamic function calls at run time.

piskypom
Member #16,813
February 2018

So {&a, &b}; are addresses to previously created objects a & b?

Hm. I could experiment with that, I suppose.

Now about objects updating themselves, I entertained the idea before. Actually, I used the idea. Updates and Draw calls handled by the objects themselves. I'm not sure what made me think an update class would be better. I suppose I thought I was being more structured by separating the game into functionality eg. all the draw stuff, update stuff, physics stuff etc., segregated each handling their respective actions game-wide. So an object wouldn't know how to draw, update, or detect if it was colliding. All it could do is pass on it's attributes (coords, dimension) and inventory.

bamccaig
Member #7,536
July 2006
avatar

The main point of polymorphism is to reduce the amount of code that you have to write. If you can find a common interface to multiple objects then you can write code that uses the interface once instead of once for each concrete type. Having a common type also allows you to store many objects of different types in the same collection. You can attempt to do this with void pointers, but without some magic to determine the type it becomes messy.

For example, a game object may be "renderable", meaning it can be drawn. If all renderable objects have a render(surface) method then you can easily draw all of your objects by looping over them and calling their render method. It doesn't matter that some are blitting bitmaps while others are drawing primitives, or a mixture of both. It doesn't matter that some are scoreboards and others are players. At the moment, everything is a "renderable" object and that's all that you care about.

It's true that polymorphism is rarely useful in the way that they typically try to teach it in schools. The real power of polymorphism is actually "code as data". The way that you can change the behavior of code by injecting different types of objects.

A simple example ripped off from a Google tester years ago is a "credit card processor" object: CCProcessor p = CCProcessor(...); p.charge(cc);. In production, the CCProcessor needs to interact with a database and some remote services and who knows what else to complete the transaction. In a test environment, you probably don't care about that stuff. There's separate tests for those processes. To merely test the processor you only want to verify that internally it's wired properly. If you pass in mock objects that use the same interface as the database and remote services objects then you can change the behavior of the processor object within the test environment without changing its code: instead of actually charging your credit card, it can just set some flags so you can verify that the correct sequence of events occurs.

This is an indirect way to accomplish higher order functions. Essentially functions as data. Dynamically changing the way the program works by passing code in addition to data. Higher-level languages have been doing this in a more pure way for decades. E.g., Lisps. It can take some time to wrap your head around it, but it's immensely powerful.

As far as how to pass data around, you can group all of the data that you need to pass into another object type. For example, you could store the entire game state in an object and pass that. Ideally, you don't want to spread that around too far because obviously that opens you up to more bugs and bad design decisions (it would be equivalent to global variables with the added pain of passing around). However, when it comes to processing the game logic, I recommend you create "service" objects or functions to do the logic. The game objects themselves should be dumb (e.g., player, or block). The reason for this is because the logic often depends on more than one object at a time. A service function/object is allowed to see many objects (or all objects) at once so it's able to figure out the logic. Meanwhile, a player object should only really know what it means to be a player. It shouldn't know about the rest of the game universe.

Whatever you call it, you should try to write "service" objects that don't have any state (data) themselves, but that operate on data passed to them as parameters. Data is passed into the function or method, some calculation is performed, and either the state is altered through indirection (pointers) or the function returns new state which the caller should use to replace the original. The main logic routine might calculate all of the logic for the whole game frame. To do it, it might call 10s or 100s of other service functions internally which are smaller and lower-level. Depends on the complexity of the program. Ultimately though, you clearly separate the logic from the data, which will make the program design automatically better, more reliable, and easier to get right and build upon.

As an aside: passing parameters really isn't very hard. If you find typing is troublesome consider upgrading your editor. I would personally recommend something vi/vim based. The standard editors that most people are familiar with are shit. Your keyboard is far superior to your mouse.

piskypom
Member #16,813
February 2018

@bamccaig, You certainly gave a lot to consider. Pretty deep stuff from my standpoint. I'm just a beginner.

The service objects went over my head. But it's probably something that will be useful in the future.

I'm nearly done refactoring a little proof of concept. I'll post it as soon as I finish. It's a an Arkanoid-like demo. Just primitives. Less code. I'm using inheritance in a way that I think works well. We could use it as a basis for explanations.

As I get more familiar with game dev and c++ I'm sure I'll understand more concepts. But for now everything is over my head and causes anxiety. Until then I'm just doing my caveman style approach and learn as I go.

BTW, I've been using Vim for a couple years now. I know I sounded a little melodramatic saying how it's tedious to write code, but it really isn't with Vim's registers and macros. I'm not a whiz with it or anything, but I use it so much that using traditional editors really is like walking through molasses. Perhaps you can pass on some vim tips for the rest of us :)

bamccaig
Member #7,536
July 2006
avatar

piskypom said:

The service objects went over my head. But it's probably something that will be useful in the future.

It's an arbitrary name. A service object is just an object that has no state of its own. Contrived example:

#SelectExpand
1#include <iostream> 2 3struct Foo 4{ 5 int X; 6 int Y; }; 7 8struct IBar 9{ 10 virtual ~IBar(void); 11 virtual void baz(Foo &, Foo &) = 0; 12}; 13 14IBar::~IBar(void) {} 15 16struct Bar: public IBar 17{ 18 virtual ~Bar(void); 19 void baz(Foo &, Foo &) ; 20}; 21 22Bar::~Bar(void) {} 23 24void Bar::baz(Foo & a, Foo & b) 25{ 26 a.X += b.X; 27 a.Y += b.Y; 28} 29 30struct MockBar: public IBar 31{ 32 virtual ~MockBar(void); 33 void baz(Foo &, Foo &) ; 34}; 35 36MockBar::~MockBar(void) {} 37 38void MockBar::baz( Foo & a, Foo & b) 39{ 40 std::cerr << "Mock: Pretending to add `b` to `a`." << std::endl; 41} 42 43std::ostream & operator<<(std::ostream & out, Foo & foo) 44{ 45 return out << "{X:" << foo.X << ",Y:" << foo.Y << "}"; 46} 47 48void example(IBar & bar) 49{ 50 Foo foo1, foo2, foo3; 51 52 foo1.X = 2; 53 foo1.Y = 3; 54 55 foo2.X = 3; 56 foo2.Y = 5; 57 58 foo3 = foo1; 59 60 bar.baz(foo3, foo2); 61 62 std::cout << foo1 << std::endl << foo2 << std::endl << foo3 << std::endl; 63} 64 65int main(int argc, char * argv[]) 66{ 67 std::cout << "First example (using `Bar`):" << std::endl; 68 Bar bar; 69 example(bar); 70 71 std::cout << "Second example (using `MockBar`):" << std::endl; 72 MockBar mock; 73 example(mock); 74 75 return 1; 76}

Output:

First example (using `Bar`):
{X:2,Y:3}
{X:3,Y:5}
{X:5,Y:8}
Second example (using `MockBar`):
Mock: Pretending to add `b` to `a`.
{X:2,Y:3}
{X:3,Y:5}
{X:2,Y:3}

In this example, Bar is a service object. It has no state of its own, but it operates on objects passed to it. MockBar is a mock object with the same interface (`IBar`) as Bar. example accepts any IBar object and uses it to do some work with foo3 and foo2. The exact work that is done depends on what IBar you give it.

In this case, the Bar object adds the properties of b onto a. The MockBar object doesn't do anything, but it prints a message to the console pretending that it did. You can see that in the first example foo3 becomes {5,8}, which is the sum of {2,3} and {3,5}. In the second example, foo3 doesn't change. It is still a copy of foo1 which was {2,3}. We didn't need to rewrite example or add any special logic into it like an if statement to check the type. The difference is automatic.

The only real reason to keep "service" objects such as "Bar" stateless is that it forces a better design. You have to anticipate not controlling what is given to you and always working with it no matter what. This will influence your design not only of the service object, but also of the data objects that the service object operates on. We could have just had Foo do this itself, but that would have limited the usefulness of example. It would have only ever done one thing. Instead, it can do countless things depending entirely on what IBar it is given.

The program becomes much less rigid as a result and becomes much more malleable. That's the whole point of software.

Of course, this is a contrived example. It can take a bit of a eureka moment to really grasp what this all means and what makes it useful. Don't feel bad if it doesn't click right away. It can take years of experience for that to happen.

Quote:

BTW, I've been using Vim for a couple years now. I know I sounded a little melodramatic saying how it's tedious to write code, but it really isn't with Vim's registers and macros. I'm not a whiz with it or anything, but I use it so much that using traditional editors really is like walking through molasses. Perhaps you can pass on some vim tips for the rest of us :)

Good for you! 8-) If you've been using Vim for a couple of years there's probably not much that I could teach you. I'm not an expert by any means. There's a lot more that I could try to squeeze out of it, but as you seem to have experienced there's immense gains just from using the basics.

One of the simple things I know is using macros. Type q, some other character as a name (e.g., I usually use q again if I don't need many at once), and then any sequence of commands you want. Vim will remember what you do until you press q again and it'll save the sequence of commands as a macro to whatever character you said. To recall it, you type @ followed by the character you saved it under (in the above example, q again). You can execute the macro many times over by first typing a number like say 100, then @, then the character. For tedious programming tasks if you can think of the right search patterns and movements and editing keystrokes you can save yourself a lot of time by automating it.

Another thing that comes in handy is typing % to jump to a matched brace. This can help with figuring out where mismatched braces go wrong.

The key to being efficient in Vim is moving around efficiently. I'm not very good at guestimating how many characters or words I am away from my target, and I'm often too close to bother with a regex. I usually use w to jump a word at a time to my target, and hjkl to move within a word. It's not the ideal way to move, but it's the lazy way to move. You can instead count characters or words first and be precise. I haven't really experimented with which technique is faster. It probably depends on how how sharp you are (and how good your eyesight is).

A sort of really basic thing that can help on a single line is type f and then the character you want to jump to. It can jump immediately to the next n instances of that character which is often as much precision as you need. Similarly, F goes backwards. t jumps BEFORE the character you specify, and T does the same backwards. You can repeat any of these operations by typing ; (a semi-colon) or , (comma) to do so in the opposite direction.

Some of these thing I learned by playing Vim Adventures. It was a very fun and educational Web game. The only thing that pisses me off is that I thought I had a lifetime license to it, and after a year or so I was pay-walled. And I think it changed to a monthly subscription or something stupid like that. For some reason I cannot access the site right now, but it appears to be up according to an IRC bot. I can't explain why I can't get to it, but yeah. If you want to have a bit of fun and possibly learn a few tricks give that a try. But make sure you can cancel your subscription whenever you want/need to.. I'm still sore about having my license silently pulled when I was sure I had a permanent license for early access.

piskypom
Member #16,813
February 2018

Wow, ok. That is a bit mind numbing to look at. I love learning new concepts so I will look over this code.

I love the feeling of wrapping my ahead around something new and having that "aha!" moment, when it "clicks" as you mentioned. I appreciate you taking the time to put this together. Any new concept that can further improve my productivity or way of looking at things is welcome.

The foo bar is what is throwing me off. I need to see it in a system I already know and understand. To know how it can benefit what I'm currently doing. Or at least something that I'm already quite familiar with.

Give me some time and I'll eventually get it.

When you mentioned Vim in your post I knew we'd get along ;D. Vim is really such an odd approach and I wouldn't recommend it to just anybody. I've seen the responses of some friends and they just blow it off. "Thanks, but I'm fine with ___. I don't have time to learn something like that." You know what they say "Different strokes for different folks." When I heard that there was an editor that you could use without lifting your hands off the keyboard, I was intrigued. I installed it the moment I heard about the mouseless coding aspect. I always thought it was a pain to constantly lift my hand. Laziness breeds ingenuity.

+1 for qq and f/F t/T

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

The power of polymorphism comes from the common interface created by inheritance.

For example, consider the following C++11 code :

#SelectExpand
1#include <cstdio> 2 3#include <algorithm> 4 5class B { 6public : 7 virtual void DoStuff()=0; 8}; 9 10class D1 : public B { 11public : 12 void DoStuff() {printf("D1::DoStuff()\n");} 13}; 14class D2 : public B { 15public : 16 void DoStuff() {printf("D2::DoStuff()\n");} 17}; 18class D2A : public D2 { 19public : 20 void DoStuff() {printf("D2A::DoStuff()\n");} 21}; 22 23int main(int argc , char** argv) { 24 25 (void)argc; 26 (void)argv; 27 28 const int SIZE = 3; 29 B* base[SIZE] = {new D1() , new D2() , new D2A()}; 30 31 auto do_stuff = [](B* b) {b->DoStuff();}; 32 33 std::for_each(base , base + SIZE , do_stuff); 34 35 return 0; 36}

The output is :

D1::DoStuff()
D2::DoStuff()
D2A::DoStuff()

So now you can see that each Base* can be used to call virtual functions, which lets you redefine their behavior.

piskypom
Member #16,813
February 2018

I'm having trouble with line 31. I've not seen that syntax before. And how do you know to use a pointer to make the array? For example, why not just B base[SIZE] ?

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

piskypom said:

I'm having trouble with line 31. I've not seen that syntax before.

It's a lambda. The equivalent code would be :

for (int i = 0 ; i < SIZE ; ++i) {base[i]->DoStuff();}

Quote:

And how do you know to use a pointer to make the array? For example, why not just B base[SIZE] ?

Because then they would actually be B class objects. The powers comes from being able to store a D1* and a D2* and a D2A* all in the same type of pointer, which would be a B*. Otherwise, They're not actually derived class objects.

bamccaig
Member #7,536
July 2006
avatar

Higher-level languages such a Python and Java abstract the pointers away from us with implicit references. C++ gives you the power to control this so you end up with a dilemma where "pointers are hard so avoid them", but polymorphism requires them.. Pointers are not really that hard, but managing memory 100% perfectly certainly can be. C++ has a few tools to help, but it still requires extreme discipline and diligence to have any hope of getting it perfect. Anyway, a base class instance can't necessarily hold a derived instance because the derived type might have more members or methods, and the base class doesn't allocate enough memory for that. That's why pointers/references are needed for polymorphism to work. The pointer is the same size no matter what, and C++ is able to layout the memory in such a way that it knows where to find the base fields and methods, and can find a vtable to lookup the virtual methods.

piskypom
Member #16,813
February 2018

Despite my ineptitude, I actually understood everything you just explained. The first thing I learned in C++ was ... well, C. I was taught C-strings straight away. Seeing how the memory is moved around in these arrays helped alot. Especially after seeing the power after looking at the console spewing out garbage from unallocated memory. I still don't quite see the need for polymorphism for the coding style I use. But I'm not opposed. I just need some more experience.

Go to: