The 2024 Wheel Reinvention Jam just concluded. See the results.

Help handling data in a non OOP way

So, I'm still learning so this question might be dumb, but here it is:

Imagine the following code being inside main.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    // initialization
    renderer_t *renderer = create_renderer();
    world_t *world = create_world();
    enemies_t *enemies = spawn_enemies();
    player_t player;
    input_t input;

    // main loop
    while(1) {

        // handle events here

        update_and_render(renderer, world, enemies, player, input);
    }


So, my questions are:

- As my game gets bigger, the initialization part will eventually have way more work to do, but I'm guessing the only thing I could do is to create an initialize() function that initializes all variables I need as globals, and then I could use them in the update_and_render() part. Is there another option so that not all of my initialization code is inlined inside main()?

- Also, do I have to always initialize something out of the main loop and then pass it to update_and_render() so that it can be used by my game? Or is there another way to do this?


I'm just confused as what should I put where, because right now every time I need something I'm just creating a global, and I know that will get messy as time goes on. But to me, the only other option seems to be initialize everything inside main and pass it to update_and_render().
The code fragment you posted is not handling data in OOP way. Are you asking how to write code that doesn't look like your example? Or something else?

If you are worried that you'll have too many variables to pass around, you can simply put your renderer/world/enemeis/player/whatever else in one GameState structure you pass around. Exactly as Handmade Hero does.

You need to initialized data before using it. Where it happens - it's entirely up to you and your game. It could be in "create" function, it could be in game update loop, it could be in some asset loader. It very much depends on situation and what kind of data it is.


Edited by Mārtiņš Možeiko on
Following up on what mmozeiko said, I strongly recommend creating a single state struct and using it differently depending on any number of given circumstances in your game. My approach was the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct TitleScreenData {
    //...whatever a title screen would have
};

struct GameData {
    //entities, map, camera, etc.
};

enum StateType {
    STATE_TITLE,
    STATE_GAME,
    MAX_STATE
};

struct Gamestate {
    int type;
    union {
        TitleScreenData *title;
        GameData *game;
    };
};

Gamestate *new_title(/*stuff you need to initialize a title screen*/); //allocates title, etc.
Gamestate *new_game(/*stuff you need to initialize a game... for instance, a path to a player save file*/); //allocates game, etc.
Gamestate *update_gamestate(Gamestate *g);
void render_gamestate(Gamestate *g);


Of course you can imagine that update_gamestate and render_gamestate do different things depending on the type variable inside the passed gamestate. They'll use either the TitleScreenData or the GameData inside the union depending on what they need; your responsibility of the programmer is just to ensure that your code stays consistent (e.g. if the type is STATE_GAME, only use GameData...). This, of course, doesn't seem to be a difficult task.
I think I get what you're suggesting, but I have a technical question about your structure.

Wouldn't writing to Gamestate intending to change TitleScreenData make GameData completely unreadable? Why use an union here? My understanding is that in this case TitleScreenData* and GameData* use the same memory and you have to reassign the pointers every time you want to switch types.

I guess what I'm trying to say is, how would you do this? (in order)
- Update TitleScreenData
- Update GameData
- Read TitleScreenData

Edited by Italo on
A union says the following: "This block of memory can be interpreted to mean one of the following things." A state system like this one operates on the assumption that the program (or system, or whatever we're dealing with) can only be in one state at a time.

Firstly, because in the union I only store pointers to state-specific data, there's no memory being wasted (pointers are all equal in size, independent of what they're pointing to); the union serves as effectively a cast of raw memory.

Using the TitleScreenData instantiation "casts" the memory to be used as if it's an instantiation of TitleScreenData. Of course, if it isn't arranged like your program expects a TitleScreenData instantiation to be arranged, you'll run into some issues (and your program isn't operating safely). That's what the "type" integer is for; it is designed to determine which data is meant to be used. The programmer's job is just to ensure that they're only accessing, say, the TitleScreenData struct when type == STATE_TITLE.

The definition of the update_gamestate function might look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Gamestate *update_gamestate(Gamestate *g) {
    switch(g->type) {
        case STATE_TITLE: {
            //update the title screen, using the TitleScreenData struct
            break;
        }
        case STATE_GAME: {
            //update the game, using the GameData struct
            break;
        }
        //note: my strategy is to branch further into specific update_title and update_game
        //      functions; this isn't necessarily fastest as it causes more indirection but it helps
        //      with organization (game functions can be thrown into a separate file, and same with
        //      title functions)
        default: break;
    }
    return g;
}


It's, of course, expected that before update_gamestate is called, the state-specific memory has been allocated according to the state's type integer.

Your question seemed to also involve state changing; my strategy for that is the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Gamestate *current = NULL, *next = NULL;
current = new_title();

//...

//at the end of a game loop, maybe somewhere you set next to be the result of new_game()
if(next) {
    free_gamestate(current);
    current = next;
    next = NULL;
}


So, for example, in the title's update function, you might have the following code:

1
2
3
4
5
6
7
8
9
//in title update function
if(play_button_clicked) {
    next = new_game();
}

//at the end of this loop, the current state will be freed and reassigned to next
//this will begin updating and rendering using the /GameData/ instead of the
//TitleScreenData. This, of course, assumes that new_game() returns a valid Gamestate *
//that properly allocates and initializes a GameData struct.


Hopefully that cleared things up a bit.

Edited by Ryan Fleury on
@Delix: Your method is more a way to achieve polymorphism without virtual functions.

I think what mmozeiko was suggesting is more like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct game_state_t {
    renderer_t renderer;
    world_t world;
    enemies_t enemies;
    player_t player;
    input_t input;
};

void main( void ) {

    game_state_t state; // Allocate the state the way you want (global, stack, malloc... ).

    // Initialize where you want (e.g. instead of here, it could be the first time you enter update_and_render).
    create_renderer( &state.renderer );
    create_world( &state.world );
    spawn_enemies( &state.enemies );

    while ( 1 ) {
        update_and_render( &state );
    }
}

void update_and_render( game_state_t* state ) {
    simulate( &state->world );
    ...
    render( &state->renderer );
}
Effectively, yeah; it is, however, still a method of organizing data in a non-OOP way. It differs from OOP in that it doesn't abstract more than usual and requires straightforward procedural code to use.

The object-oriented paradigm wasn't wrong in claiming there's use in specialized types of something, I would just argue that it was wrong in its approach to solving it.

Edited by Ryan Fleury on
Ok, thanks for the help, you guys are awesome. Can I ask one more question?

I have 3 structures like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct
{
  double x;
  double y;
  v2 velocity;
  texture_t image;
  int w;
  int h;
} player_h;

typedef struct
{
  double x;
  double y;
  v2 velocity;
  texture_t image;
  int w;
  int h;
  behavior_t behavior;
  skill_t skills[5]; // array of skills
  int skills_count; // how many skills does he have
} enemy_t;

typedef struct
{
  double x;
  double y;
  v2 velocity;
  texture_t image;
  int w;
  int h;
} particle_t;


So, it seems like there's a lot of duplicated data above. How would you guys go about "fixing" that? Or should I just leave it as is and not care about this?
And, on the enemy_t structure, is that a dumb way of handling enemies with different skills/skill counts?
Here's one possible way of doing it:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enum Entity_Type { PLAYER_ENTITY, ENEMY_ENTITY, PARTICLE_ENTITY };
typedef struct {
  float x;
  float y;
  Vector2 velocity;
  Texture image;
  int w;
  int h;
  int type;
  union { 
    struct { // Enemy Data:
      Behavior behavior;
      Skill skills[5];
      int skills_count;
    };
    struct { // Player Data:
      // Player Data...
    };
  };
} Entity;

// You don't necessarily need these:
typedef Entity Player;
typedef Entity Enemy;
typedef Entity Particle;


The OOP way of doing this would be to have a base class entity which player, enemy, and particle would inherit from. This avoids that by having only one data structure for every entity. This structure makes it easy to add new entity types without having to write a lot of extra code. It's convenient because you can put all of your entities into a single array which you can iterate over for things like simulating and rendering. This also allows you to pass in different types of entities into the same procedures, for example if you have:
1
2
3
void simulate_gravity(Entity *e) {
  // Simulate Gravity...
}
You have the ability to pass in a player, enemy, or particle into this procedure.

The downside to this would be that you would end up allocating more data for your entities. In the cases where data is a big problem, you could split a type into its own structure. For example, if you have a lot of particles in your game at once, you may want to split the particle type into its own struct to avoid having to allocate as much extra memory. Another downside of this is that there is less type safety for each of your entity types. You would instead use the type variable in the struct to keep track of the type of the entity.

This isn't the only way of doing it, but this is the method I tend to use.
For this specific case you could do:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct
{
  double x;
  double y;
  v2 velocity;
  texture_t image;
  int w;
  int h;
} particle_t;

typedef struct
{
  particle_t particle;
} player_h;

typedef struct
{
  particle_t particle;
  behavior_t behavior;
  skill_t skills[5]; // array of skills
  int skills_count; // how many skills does he have
} enemy_t;


Since player_h and enemy_t have the attributes of a particle you can use composition.

Another option is to use discriminated unions (aka tagged union, variant record) which could use up more memory but it can be more pleasant to work with.
https://en.wikipedia.org/wiki/Tagged_union

Edit: EasyP describes this approach.
Edit2: In the 1970s & 1980s section of the wiki page there is an example of how these work in C/C++.

Edited by Caius on
That seems to be a nice way to handle something like this.

Let me just see if I understood what you did there with unions and unnamed structs. If you have a unnamed struct like that inside a union, I can access its members by name because the struct is removed by the compiler and what remains is a block of members that must be allocated side by side (as opposed to overlapping allocation if I hadn't used an unnamed struct)?

In this case, I'm assuming it should be an error if I use the same name for a member in two different struct inside the union?

Edited by Italo on
Yes, you can access the members inside the unnamed structs by name, which is why I find this method to be pretty convenient. The unnamed structs provide a way of grouping extra data based on the entity type. Each of these unnamed structs will share the same location in memory. You seem to have the right idea. And yes, you should get an error if you have a duplicate name in two different structs in the union.
Be careful using anonymous structs in the union, however; this can lead to name collisions.
What do you mean by name collisions?
nyeecola
Ok, thanks for the help, you guys are awesome. Can I ask one more question?

I have 3 structures like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct
{
  double x;
  double y;
  v2 velocity;
  texture_t image;
  int w;
  int h;
} player_h;

typedef struct
{
  double x;
  double y;
  v2 velocity;
  texture_t image;
  int w;
  int h;
  behavior_t behavior;
  skill_t skills[5]; // array of skills
  int skills_count; // how many skills does he have
} enemy_t;

typedef struct
{
  double x;
  double y;
  v2 velocity;
  texture_t image;
  int w;
  int h;
} particle_t;


So, it seems like there's a lot of duplicated data above. How would you guys go about "fixing" that? Or should I just leave it as is and not care about this?
And, on the enemy_t structure, is that a dumb way of handling enemies with different skills/skill counts?


Here you can easily apply compression-oriented programming.
Remember: every time you copy and paste some code, pay attention because that could it's the right moment to compress that code and pull it out.

In your example, the best thing to do is to "logically" group the common properties of the structs. All of them have x, y and velocity that are logically correlated: let's pull those out and write:

1
2
3
4
5
6
struct Position
{
    double x;
    double y;
    v2 velocity;
};


and now your code become:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct
{
  Position p;
  texture_t image;
  int w;
  int h;
} player_h;

typedef struct
{
  Position p;
  texture_t image;
  int w;
  int h;
  behavior_t behavior;
  skill_t skills[5]; // array of skills
  int skills_count; // how many skills does he have
} enemy_t;

typedef struct
{
  Position p;;
  texture_t image;
  int w;
  int h;
} particle_t;


You could do the same thing with w, h and image: those belongs to the same "logical" component (I guess?):

1
2
3
4
5
6
struct Texture
{
  texture_t image;
  int w;
  int h;
};



And now your code is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
typedef struct
{
  Position p;
  Texture tex;
} player_h;

typedef struct
{
  Position p;
  Texture tex;
  behavior_t behavior;
  skill_t skills[5]; // array of skills
  int skills_count; // how many skills does he have
} enemy_t;

typedef struct
{
  Position p;
  Texture tex;
} particle_t;



And there you are: it's just "the most natural and logical thing to do".
Leonardo