Structures... Is flat generally better? Sean Barrett's style? What about JB?

Hello everyone,

I would like to share my recent experience because I really felt completely broken today. And I would welcome any advice or comments.

So far, I've 'done' live-code editing and game-platform split (~ Day30), and now I skipped a little towards implementing fonts (~ Day160).
And I bravely decided to do it on my own and write strings on the screen without watching Casey - by reading Sean's library.

I failed abysmally. When I tried writing small prototypes to check my understanding, I got super overwhelmed and ended up staring blankly at the screen, not understanding anything. The problem was super easy to understand. It's just that my brain... it just couldn't hold the idea and what needs to be done, simultaneously. And by what needs to be done, I mean the situation where I started writing a function to draw two consecutive glyphs, for example, but all the things that would now need to be repaired elsewhere started filling my mental space up to a point where the idea just.. poof, disappeared.

I admit, my capacity to hold a lot of things in my head is pretty bad (maybe I shouldn't be a programmer) but I can actually handle difficult concepts.

Do you experience this as well? I am perplexed how Casey can start writing and editing new code, and then pretty much remember where the changes propagated - without breaking everything and ending up staring blankly at the screen.

The only way forward was obviously simplifying the code, so that I could 'handle' thinking about it... And I think that the thing that was stalling me were actually the structures. I felt a slight discomfort already because I used a lot of cumbersome things, such as:

 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
"handmade.h"

struct Win32_Bitmap 
{
    BITMAPINFO info; 
    int width;
    int height;
    int bytes_per_pixel;
    int memory_size;
    void *memory;
};
struct Game_Bitmap 
{
    int width;
    int height;
    int bytes_per_pixel;
    int memory_size;
    void *memory;
};
....
"win32_handmade.cpp"

void UpdateGameBitmapSubset() // on ResizeDB
{
    game_bitmap.memory = win32_bitmap.memory;
    game_bitmap.memory_size = win32_bitmap.memory_size;
    game_bitmap.width = win32_bitmap.width;
    game_bitmap.height = win32_bitmap.height;
}

DrawRectangleDLL(&game_bitmap, input, &game);  
WriteSineWaveDLL(&game_bitmap, &game, phase);


I think that it was solely my discomfort with the naming and the way the data was structured, that my brain was just stalling...
I ended up making two big structures, Win32 and Game, and decided that everything in Game is a pointer to the memory that is 'controlled' by the platform. Even ints... how bad is that?



 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
"handmade.h"
struct Win32
{
    BITMAPINFO screen_bitmap_info;
    Bitmap screen_bitmap;
    Bitmap letter_bitmap;
    Bitmap font_bitmap;

    uint64 memory_size;

    int mouse_x;
    int mouse_y;

    float phase;
    Game_Memory game_memory;
    HMODULE game_code;
};



struct Game         // game has access to things which get updated by the platform 
{
    Bitmap *screen_bitmap;
    Bitmap *letter_bitmap;
    Bitmap *font_bitmap;

    int *mouse_x;
    int *mouse_y;

    float *phase;
    Game_Memory *game_memory;
    HMODULE *game_code;


    // here is platform-dependent code exposed to the game code
    PlatformDebugFreeFileMemoryType         *PlatformDebugFreeFileMemory; 
    PlatformDebugReadEntireFileType         *PlatformDebugReadEntireFile; 
    PlatformDebugOverwriteCreateFileType    *PlatformDebugOverwriteCreateFile;
};
struct Bitmap 
{
    int width;
    int height;
    int bytes_per_pixel;
    int memory_size;
    void *memory;
};
struct RGB
{
    uint8 r;
    uint8 g;
    uint8 b;
};
struct DebugReadResult
{
    void *content;
    uint64 content_size;
};

"handmade.cpp"
...
void WriteSineWave(Game *game)
{
    Bitmap screen_bitmap = game->screen_bitmap;
    int width = screen_bitmap->width;
    int height = screen_bitmap->height;



I am much more at ease with this than with passing more than one obscure structures. When the platform calls game functions, it just passes &game as the argument.
Furthermore, the functions always start by extracting the things that it will be working with (renaming ->->) which improves readability for me.


When I thought about this, I noticed how easy to use the library was and I fell in love with Sean Berret's style of functions with 'many arguments'.

We are basically forced to write down all the variables that actually form a logical structure, before plugging them in. This might seem like a neat practice to follow but I think it goes deeper.
Basically, I think that Sean Berret is kind of 'screaming' that just because we think in structures, writing them down does no good. (It spoils readability which seems to matter immensely.) It becomes really evident, once you observe how much he uses concepts which are actually structures, e.g. 'bounding box', 'glyph metrics', but he never 'defines' them in code, i.e. never groups them inside a structure!

What I also noticed is that he seems to try to avoid returning structures. (In some cases he does but these structures are pretty large.)


So I am thinking that Sean's way solves the structure problem extremely elegantly:

You have comments and function names to prepare the mental structures/concepts, and the function names are immediately followed by arguments which remind you of their more precise definitions, which you may choose to read (and load into your brain if desired).

In the post title, I ask if flat is generally better. Right now, I think that the correct answer is the Sean's 100%. Work with any mental concepts/structures you want but don't write them down in code because it will be less readable.

So do you guys struggle with structures too? Do you like Sean's style? Is it too purist? Am I getting it wrong? How does Johnathan Blow approach this?

Edited by partypooper on
partypooper
I failed abysmally. When I tried writing small prototypes to check my understanding, I got super overwhelmed and ended up staring blankly at the screen, not understanding anything. The problem was super easy to understand. It's just that my brain... it just couldn't hold the idea and what needs to be done, simultaneously. And by what needs to be done, I mean the situation where I started writing a function to draw two consecutive glyphs, for example, but all the things that would now need to be repaired elsewhere started filling my mental space up to a point where the idea just.. poof, disappeared.


It generally takes me some time to get use to new libraries because I need to understand new concepts and sometime I don't understand some decisions until I face the problem in my code. A way I found to "free space" in my mind is to simply keep notes about what needs to be done. For example as you read example of using stb_truetype you can write in your code the step that need to be done to load a font, render a character and copy the functions names. It's also useful to keep a description of the concept you are not familiar with. And using keywords in your comments such as TODO, NOTE, IMPORTANT...

For fixing things that are broken when you modify code, I would separate them in two "types":

- First there is code that will just not compile anymore. I don't worry about that at all, because the compiler will point them at you so I don't have to remember them. You can also use the error message to make sure new code cover all your use cases.

- Second there is code that compile but doesn't work the same way as before and produce a wrong result. If I think it's necessary, I keep comment in the new code to explain the change that was made and what need to be done to work with the new system so that if I encounter an issue in the future I will know how to fix it. When I do big changes, I often miss things that need to be fixed. That is not a problem, I will just need to debug them when I see the problem. It can take some time but it's generally not significant so I don't worry about that.

Also sometimes just taking a break and start fresh on a problem helps.

partypooper
I am perplexed how Casey can start writing and editing new code, and then pretty much remember where the changes propagated - without breaking everything and ending up staring blankly at the screen.


It comes with years of experience and practice. It's not innate.

partypooper

I ended up making two big structures, Win32 and Game, and decided that everything in Game is a pointer to the memory that is 'controlled' by the platform. Even ints... how bad is that?


You could do this to avoid having to keep the structures code in sync:

 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
struct Game
{
    Bitmap screen_bitmap;
    Bitmap letter_bitmap;
    Bitmap font_bitmap;

    int mouse_x;
    int mouse_y;

    float phase;
    Game_Memory game_memory;
    HMODULE game_code;

    // here is platform-dependent code exposed to the game code
    PlatformDebugFreeFileMemoryType         *PlatformDebugFreeFileMemory; 
    PlatformDebugReadEntireFileType         *PlatformDebugReadEntireFile; 
    PlatformDebugOverwriteCreateFileType    *PlatformDebugOverwriteCreateFile;
};

struct Win32
{
    BITMAPINFO screen_bitmap_info;
    Game game;
};

void fn( Game* game ) { ... }

// Call
fn( &Win32.game );


There is in my opinion a value into only passing only the required structure/data to a function. It makes it clear on what data the function will operate and makes it simpler to modify the function because you don't have to worry about other data.

I don't agree that having a long list of arguments is always good. There is a difference between the application code and the code for a library.

In a library you want to make it easy for users to integrate it in their code. And one part of that is to avoid forcing people to use your types, because the user codebase may have an equivalent type already and you don't want them to need to create a temp object each time they want to use your lib. Having an API that allows to pass basic types instead of structures makes it easier to integrate (but doesn't solve all issues). Also while the lib API might not expose structures, internally it probably uses structures.

Returning a structure will most likely make a extra copy. Instead of returning a big structure, you can pass a pointer to a struct that will receive the result.

But I don't want, in my code, to always have to use 4 different variables to represent a bounding box. It's longer to type, it's more error prone (it's easy to mix x, y and min, max), you can't return a new object, you can't easily create an array of them...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// This
float bb_min_x = 0, bb_max_x = 10, bb_min_y = 0, bb_max_y = 10;
grow( &bb_min_x, &bb_max_x, &bb_min_y, &bb_max_y, 5, 2 );

float bb2_min_x, bb2_max_x, bb2_min_y, bb2_max_y;
grow2( &bb_min_x, &bb_max_x, &bb_min_y, &bb_max_y, 5, 2, &bb2_min_x, &bb2_max_x, &bb2_min_y, &bb2_max_y );

// Is less clear to me that
bounding_box_t bb = { 0, 10, 0, 10 };
grow( &bb, 5, 2 );

bounding_box_t bb2;
bb2 = grow2( &bb, 5, 2 );
// or
grow2( &bb, 5, 2, &bb2 );

Edited by Simon Anciaux on Reason: typo