Allegro.cc - Online Community

Allegro.cc Forums » Game Design & Concepts » Why I write games in C

This thread is locked; no one can reply to it. rss feed Print
 1   2   3 
Why I write games in C
Neil Roy
Member #2,229
April 2002
avatar

bamccaig said:

There's no real winner between them.

Oh yeah, C is absolutely the winner. It is faster as it is closer to machine code as you can get without writing assembly. It is less complex which makes it easier for the compiler to optimize. C++ is a higher level language and is harder for the compiler to optimize, especially with volatile functions and the like, it is impossible for a compiler to optimize those. The extra layers in C++ only add to the problems, they don't help any. And in recent years I am seeing a movement away from the very basics of C++ classes like the constructor and destructor, and instead I am seeing init() functions etc... in classes which is stupidity, may as well use C if you're going to do that. There is much about C++ classes that was touted as a great thing like inheritance, polymorphism etc... that are starting to be seen as bad things. Instead we are seeing more things like vectors used (which is one thing I like about C++, but that I have replicated in C easily, what I needed from them anyhow) and other stuff that leads to long, convoluted horrible code that is difficult to follow.

I am seeing videos out now where newer developers are moving away from OOP and only using C++ for the vectors and such, but otherwise their code looks like C. And they could move to just C if Microsoft supported newer C versions better. Thankfully we have GNU compilers (MinGW) which does support the 2011 standard of C.

I simply do not see high level languages being better. The only exceptions are ones like JavaScript perhaps that lend themselves well to online development.

I have completed several games in C, they all work, they run smoothly, not a problem with them. I tried to do it in C++ and the headaches using it were enormous and I didn't like the mess my code had become. I can make game objects similar to C++ classes in C. I can put my code in separate files, with their own headers and use structs for everything I need. I will never be convinced that C++ is the way to go. I think it probably turns more people off of programming than any other language. C is really easy to learn in comparison. I moved from BASIC to C and they were very similar in a lot of respects. C++? Forget it.

Chris Katko
Member #1,881
January 2002
avatar

Neil Roy said:

I simply do not see high level languages being better.

Try D. :P

You can write native assembler in D the same way you can in C, C++. But performance is useless for most hobbyists now. Even if there was a 10x slowdown (or 100x in some cases!) those are still COEFFICENTS as opposed to order (quadratic, cubic, exponential) algorithm complexity. So by time you FINISH your program, the CPU's will still exist 2x-4x faster than you started with.

What's really more important these days... is simply coding efficiency. Getting a program that does what you want it to do ("almost" regardless of speed) is the most important problem.

I mean, how many games would we all have on our belts already if we could simply "imagine" a game in our mind and have the code written for us?

Now clearly, speed matters. Except it only matters... when it matters. In games where we're actually limited by huge numbers of objects, complex physics, AI, and graphics, and so on. Networking is still very much a limiting factor.

Modern high-level languages make it easier to CORRECTLY and reliably employ multithreading which is where ALL computing systems are heading (and have been for over a decade). I remember one professional (Tim Sweeney?) talking about functional languages and how most multi-threading these days is pathetic. Coders will say "the code works" but they can't prove it, and they can't prove it beyond a shadow of a doubt. That proof comes in DAMN important when you're doing things like security proofing, distributed computing, and more. But even in games, crashing to desktop is a complete failure of the programming ecosystem. Programs shouldn't crash. There should never be a case in your program where something happens that you didn't expect and plan for. We're supposed to be engineers, not technicians welding random pieces of scrap together. We're supposed to KNOW and be able to prove that when things run, they will always run.

Things like pure functions and immutability drastically increase the ability for a compiler to prove your code works as intended, and ALWAYS works as intended. The definition of a pure function is one that can't mutate data... and has NO SIDE EFFECTS. (No mutations of global state.) And simply restricting yourself to that "as much as possible" (obviously not in tight loops in say, a particle system) means >70/80/90% of the codebase can be proven to not break random stuff, or break in random ways.

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs

Eric Johnson
Member #14,841
January 2013
avatar

Somewhat related to C versus C++... Having spent so much time in C++, there are a few things that I'm not sure how to accomplish in C. For example, how would you go about spawning multiple enemies of the same type in C if you don't have classes?

If I am writing a game in C++, for example, and I wanted to spawn a multitude of bats to attack the player, I would do so by creating a bat class, and then by instantiating said class as many times as I need for each bat. How would I accomplish this in C though? With an array of structs for each bat?

bamccaig
Member #7,536
July 2006
avatar

There's no such thing as a software "engineer". There is never "proof" that the program is correct. As a matter of fact, if it's non-trivial you can almost guarantee that it's incorrect (i.e., not perfect), but hopefully it's close enough to do its job. Hell, even code that hasn't changed much in 20 years still has bugs in it.

Append:

If I am writing a game in C++, for example, and I wanted to spawn a multitude of bats to attack the player, I would do so by creating a bat class, and then by instantiating said class as many times as I need for each bat. How would I accomplish this in C though? With an array of structs for each bat?

A class is basically just syntactic sugar over a struct and objects. The same could be accomplished using a struct and various functions that operate on the struct.

struct t_bat_object {
    // Whatever fields.
};

typedef struct t_bat_object t_bat_object;

void bat_method_x(t_bat_object * self, type1 arg1, type2 arg2) {
    // Manipulate "object" pointed to by self.
}

To achieve something fancy like "polymorphism" where "bat" is also an "animal" you'd have to use composition to define the data for the t_bat_object (which is partially composed of the data for a t_animal_object) and then define basically a "vtable" (i.e., virtual function table) for your methods so that they could be "overridden" by derived types. The implementation of which is ugly and messy since structs must be typed to dereference fields, and function pointers must be typed to invoke them, and their parameters need to be abstracted to allow different shapes of functions to be defined... I spent about 40 minutes trying to demonstrate a solution before I realized it was far too simple to achieve the goal and gave up. :P Google is your friend. Once you read that you'll probably swear off C, at least where true polymorphism is necessary. :D

Chris Katko
Member #1,881
January 2002
avatar

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs

princeofspace
Member #11,874
April 2010
avatar

Typically, I write game entities as systems, not objects. That means that instead of

auto& m = new_game_monster(x, y, etc...);
game_state->add(m);

I can just call

const unsigned int i = make_monster(x, y);

Now, the monster system does the plumbing instead of a generic game "state" class.

My preference is to use indexers instead of pointers. Instead of making a generic system for updating all objects, I get the control of updating individual systems, and doing it all at once. I like doing this because there's times when I want a specific monster or sprite to execute code in a certain order -- which can be hard to do with a big vector full of generic "sprite" objects.

move_monsters();
ai_monsters();
draw_monsters();

or, more typically,

#SelectExpand
1 2/* control system */ 3ai_monsters(); 4ai_powerups(); 5control_interface_players(keyboard_struct); 6 7/* movement system */ 8move_monsters(); 9move_powerups(); 10move_players(); 11 12/* draw system */ 13draw_monsters(render_context_struct, camera_struct); 14draw_powerups(render_context_struct, camera_struct); 15draw_players(render_context_struct, camera_struct);

Obviously, this is just a quick mockup, but you get the idea.

Audric
Member #907
January 2001

Just to bounce on bamccaig's answer :
The following pattern of C coding is used everywhere. Even Allegro uses it.

#SelectExpand
1typedef struct { 2 int x; 3 int y; 4 char *name; 5} BAT; 6 7BAT* create_bat(int x, int y, const char *name) 8{ 9 BAT* bat = (BAT *)malloc(sizeof(BAT)); 10 bat->x = x; 11 bat->y = y; 12 bat->name = strdup(name); 13 14 return bat; 15} 16 17void destroy_bat(BAT *bat) 18{ 19 free(bat->name); 20 free(bat); 21} 22 23// Usage : to allocate 24BAT* new_bat = create_bat(3, 4, "Bob"); 25 26// To delete : 27destroy_bat(new_bat);

Peter Hull
Member #1,136
March 2001

...and to follow on from Audric, Allegro is also an example of how to do polymorphism in C, e.g. a uniform 'bitmap' interface even if the implementation is different.
https://github.com/liballeg/allegro5/blob/master/src/bitmap.c#L127
(it's the call to a function stored in vt)

There is something good about C, I find it encourages just getting down to it. With C++ I find myself agonising about how to organising classes, what should be private, where to put const, references vs. pointers vs. pass-by-value and so on.

Lately I've had some success with using 'C++ in a C way'; I stick to functions and plain-ish structs, BUT I make use of the standard container and string libraries. This takes care of a load of commonly used functionality and I haven't noticed any drop in performance, not for the stuff I'm doing, anyway.

bamccaig
Member #7,536
July 2006
avatar

Polymorphism is actually relatively easy to achieve in C. You just need a structure with function pointers that can point to different functions for different objects.

The more difficult thing is probably inheritance. You'd have a different vtable for each type, with a different shape. So if your t_animal_object * is actually a t_bat_object *, how do you find the animal vtable in your bat object not knowing it's a bat object? You'd have to have a generic way. I'm thinking that a dynamic structure like a dictionary or something might be the key, but I haven't given it much thought.

#SelectExpand
1typedef struct t_vtable_dictonary { 2 // Magic. 3} t_vtable_dictonary; 4 5void * vtable_dictionary_get(t_vtable_dictionary * dict, char * key) { 6 // Magic. 7} 8 9void vtable_dictionary_set(t_vtable_dictionary * dict, char * key, void * vtable) { 10 // Magic. 11} 12 13const char * const ANIMAL_OBJECT_VTABLE_KEY = "animal"; 14 15typedef struct t_animal_vtable { 16 void (*eat)(t_animal_object *, char *); 17}; 18 19typdef struct t_animal_object { 20 // Always the first element.
21 t_vtable_dictionary * vtable_dict;
22} t_animal_object; 23 24typedef struct t_bat_object {
25 t_vtable_dictionary * vtable_dict;
26 t_animal_object animal; 27} t_bat_object; 28 29void bat_animal_eat(t_animal_object * self, char * food) { 30 // Eat it! 31} 32 33void foo(t_animal_object * animal) {
34 t_vtable_dictionary * dict = (t_vtable_dictionary *)animal;
35 t_animal_vtable * vtable = (t_animal_vtable *) 36 vtable_dictionary_get(dict, ANIMAL_OBJECT_VTABLE_KEY); 37 38 assert(vtable); 39 40 vtable->eat(animal, "blood"); 41} 42 43int main(int argc, char * argv[]){ 44 // Normally you'd put most of this stuff in a "constructor" or "initialization" 45 // function, but for brevity it's here... 46 t_bat_object * bat = malloc(sizeof(t_bat_object)); 47 t_animal_vtable * vtable = malloc(sizeof(t_vanimal_vtable)); 48 t_vtable_dictionary * dict = malloc(sizeof(t_vtable_dictionary)); 49 50 vtable->eat = bat_animal_eat; 51 bat->vtable_dict = dict; 52 53 vtable_dictionary_set(dict, ANIMAL_OBJECT_VTABLE_KEY, vtable); 54 55 foo((t_animal_object *)bat); 56 57 return 0; 58}

Of course, you can imagine how easily this will fail since you're forcing the cast and if the programmer passes in a wrong object it'll crash hard. There's basically no type safety. It would fail in most languages, but in C++ it could determine the type was wrong at run-time (if not compile-time!), and in most dynamic languages it would also detect the type mismatch at runtime and tell you. Your C program is just going to get killed by the kernel at best, and overwrite the wrong memory at worst, and let you poke around in a debugger or with printf() until you figure out your mistake.

Audric
Member #907
January 2001

I'm a bit wary of trying to get close to C++ polymorphism in C (and certainly don't advise it to new C coders :) ). It may be a clever use of the language, but if you mess up, I fear it's very difficult to locate where. I'd rather have to maintain code like the following - You can say it's verbose, maybe repetitive, but I think I can more easily spot errors by reading the code.

#SelectExpand
1typedef struct { 2int sub_type_id; // 0 = cat, 1 = dog; // FIXME: implement enum 3void * sub_type; 4} ANIMAL; 5 6void speak(ANIMAL *animal) 7{ 8 switch (animal->sub_type_id) 9 { 10 case 0: // cat 11 cat * cat = (CAT)animal->sub_type; 12 cat_mew(cat); 13 break; 14 15 case 1: // dog 16 dog * dog = (DOG)animal->sub_type; 17 dog_bark(dog); 18 break; 19 ... 20 } 21}

Lately I've had some success with using 'C++ in a C way'; I stick to functions and plain-ish structs, BUT I make use of the standard container and string libraries. This takes care of a load of commonly used functionality and I haven't noticed any drop in performance, not for the stuff I'm doing, anyway.

I want to say, it makes your code easy to understand and modify by the biggest possible number of people.

Oscar Giner
Member #2,207
April 2002
avatar

bamccaig said:

There's no such thing as a software "engineer". There is never "proof" that the program is correct. As a matter of fact, if it's non-trivial you can almost guarantee that it's incorrect (i.e., not perfect), but hopefully it's close enough to do its job. Hell, even code that hasn't changed much in 20 years still has bugs in it.

That you haven't studied software engineering doesn't mean it doesn't exist ;). And yes, there're mathematical methods to prove a piece of code is correct. It's costly, so it's not very viable to do for a full big project, but you can at least proof some key parts.

princeofspace
Member #11,874
April 2010
avatar

I'm wary of polymorphism as a whole, because lots of programmers use it in a way that hinders readability.

For example: most game objects will have an Update method called once (or more) per frame. This is a sort of nebulous way to handle game object maitenance, because all these Update calls do different things; you never know EXACTLY what Update does.

If an Update method calls func1, func2, func3, it improves readability to simply call func1, func2, func3, and skip Update. I can't remember where I read this -- maybe somebody else does -- but if ALL objects call Update, then NO objects need to call Update.

Neil Roy
Member #2,229
April 2002
avatar

Lately I've had some success with using 'C++ in a C way'

When I have used C++, this is how I end up using it. I have seen a new movement of people doing exactly this, one series I have been following of an indy developer who does just this. He's not doing OOP anymore, but using procedural programming instead. Episode 4 he talks about that more...

video

I honestly have considered this myself, but I really like C, and I love doing it myself. I recently wrote functions that do something similar to vector stacks. It felt better to do it myself in C and I understand what is going on because... I wrote it. And it didn't take long to implement.

If I was writing code to spawn many monsters, I would use my personal vector functions to push new monsters onto the stack that I spawn, then just... handle them in order, pulling them off as they die. It's a no brainer. Use a linked list which are simple to do. And you could have a monster creation function which handles all this for you. You tell it to spawn a new monster, it creates one, allocates the memory for it, adds it to your linked list or stack and when their life runs out, they are removed from the stack and memory is cleared for it. It's no more complicated than C++ classes to be honest.

Polymorphism = bad. It is really difficult, if not impossible in many cases for the compiler to optimize, so it leads to slower code. You can do similar things with structs and unions to be honest.

bamccaig
Member #7,536
July 2006
avatar

I'm wary of polymorphism as a whole, because lots of programmers use it in a way that hinders readability.

For example: most game objects will have an Update method called once (or more) per frame. This is a sort of nebulous way to handle game object maitenance, because all these Update calls do different things; you never know EXACTLY what Update does.

If an Update method calls func1, func2, func3, it improves readability to simply call func1, func2, func3, and skip Update. I can't remember where I read this -- maybe somebody else does -- but if ALL objects call Update, then NO objects need to call Update.

This isn't polymorphism's fault. This is OOP's fault, or rather a misunderstanding in what it means. People that jump on the OO bandwagon immediately want their objects to have behaviors to satisfy their fantasy that the objects simulate something real world. They fail to think deeply enough and arrive at the conclusion that "Mario" "walks". In reality, Mario's brain would send electrical impulses to his leg muscles causing them to contract, causing his legs to move, and the friction with his shoes and the ground would cause his body to come into motion. He only controls the electrical impulse. The rest of the action is carried about by biology and physics, things he does not control.

Instead of saying: mario.update(); princess.update(); bowser.update();, you should just be saying engine.update();. The engine is a process that understands how all things in the world interact. It understands that when Mario's brain sends that signal to his legs the resulting action is movement. It also understands that when there's a wall in front of Mario the signal doesn't work because Mario collides with the wall. You need an object with a view of the world and an understanding about how objects interact to manipulate the world. A proper game world doesn't let Mario decide if he collides or not. If he's trying to save the princess it would be a lot easier if he could just skip past all this running and jumping and set Bowser's dead bit instead and then warp the princess and himself to a romantic bedroom or something. He can dream. Mario doesn't control the world. He is bound by the rules of the world, and these rules should be imposed on him from a higher-level object.

This kind of thinking is the result of not taking "OO" far enough. Ultimately, most game objects are pretty stupid, and the code should reflect that. They're really mostly just state. What brings that state to life is the engine. I've found that's generally a better way to structure code. Don't mix "service" i.e., behavior objects with "state" i.e., data objects. Your state objects should be pretty stupid. Your service objects should have little to no real state. State objects and other service objects are passed into a service object and the service object transforms the state objects using the service objects and its own behaviors. The result is that the state changes.

When you structure code like this the program structure makes a lot more sense, and it's a lot more flexible, and a lot more powerful.

Chris Katko
Member #1,881
January 2002
avatar

If an Update method calls func1, func2, func3, it improves readability to simply call func1, func2, func3, and skip Update.

I don't agree with that at all. You're still calling the same functions. What's the problem with grouping them in easily memorizable hierarchies like update() and draw()?

You want to know what functions monster_type calls verses a player_type? Go to the class.

I mean. func1, func2, and func3 are all going to call things too. How many layers are you going to strip off before you end up with:

#SelectExpand
1int main() 2{ 3obj1.x(); 4obj1.y(); 5obj1.z(); 6obj1.w(); 7obj1.w2(); 8obj1.w3(); 9 10obj2.x(); 11obj2.y(); 12obj2.z(); 13obj2.w(); 14obj2.w2(); 15obj2.w3(); 16}

A program can have call stacks that are too wide but it can also have call stacks that are too tall. There's a balancing act. But the same decisions need to be solved regardless of the hierarchy. Game logic needs to be done, and it doesn't matter where it gets called--except for the human trying to understand it.

[edit]

Neil Roy said:

If I was writing code to spawn many monsters, I would use my personal vector functions to push new monsters onto the stack that I spawn, then just... handle them in order, pulling them off as they die.

So... you're using RAII but without the simplicity of C++ RAII?

[edit]

bamccaig said:

This is OOP's fault, or rather a misunderstanding in what it means. People that jump on the OO bandwagon immediately want their objects to have behaviors to satisfy their fantasy that the objects simulate something real world.

That's exactly what Mike Acton talks about. Programming schools use EXAMPLES for classes because it's easier to learn. But then people spend a lifetime trying to UNDO the damage of school on their way of thinking. It's the same thing with math and physics. 99% of math we're taught in school is "wrong" but it's easier to teach the approximations. Then college (and definitely grad school) is all about unraveling those approximations and understanding when and where they fall apart.

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs

princeofspace
Member #11,874
April 2010
avatar

Adele Goldberg famously said of smalltalk that "everything happens somewhere else." This has widely been taken as a criticism of OOP in general; sooner or later, you have to quit stringing through virtuals and WRITE THE CODE that's going to make your program work.

Polymorphism itself may not be the culprit, or even OOP, but it's certainly the way most programmers seem to approach software design.

Neil Roy
Member #2,229
April 2002
avatar

Thankfully, I never jumped on the OOP bandwagon. :)

Also, if each object is calling the same function, than you have more problems as calling the same function many times (especially if you have a lot of objects) means you are thrashing the stack like crazy for all those function calls rather than just one. And who knows what that is doing to the cache.

princeofspace
Member #11,874
April 2010
avatar

A program can have call stacks that are too wide but it can also have call stacks that are too tall. There's a balancing act. But the same decisions need to be solved regardless of the hierarchy. Game logic needs to be done, and it doesn't matter where it gets called--except for the human trying to understand it.

Separate out stage update (where the business logic happens) from stage draw, then stage update will look like this:

#SelectExpand
1stage_func1(void) 2{ 3 /* call all sprites that need func1 */ 4} 5 6 7stage_func2(void) 8{ 9 /* call all sprites that need func2 */ 10} 11 12 13stage_func3(void) 14{ 15 /* call all sprites that need func3 */ 16} 17 18 19/* 20 * Update stage once per frame. 21 */ 22stage_update(void) 23{ 24 stage_func1(); 25 stage_func2(); 26 stage_func3(); 27}

This method avoids branching, which as we all know, we should ALWAYS avoid when possible. It also ensures leaf functions will never go too deep. If you want to know what sort of functionality a sprite has, it's here, presented as a sort of list.

 1   2   3 


Go to: