Structuring programs to be modular

Preface:
I’m continuing my game dev journey with zig and raylib, but starting to hit walls with how to structure my code. Right now this is the hardest part of self learning programming, as general tutorials/code snippets are not helping me anymore, when I need to connect these small codes together in a structured way. I guess this is the hard part of programming mainly.
So I want to take a step back and think about how should I structure and implement my code, to be more modular so later I can change the raylib backend to a lower API.

From my research - which can be completely wrong - I found that interfaces can help abstract my code, but this is more of an OOP concept (Meaning it’s usually used in languages that are made for OOP programming, and zig doesn’t have syntactical sugar for that) (Feel free to correct me if I’m wrong here). Though implementing interfaces are not impossible, so I implemented an interface system for this, which works fine, but the code looks extremely messy. At least for me. A friend of my (C++ background) suggested to just use function pointers as for him it seems like interfaces are a forced approach in zig. My knowledge and experience stops here. Then I remembered:

zig zen:
* Communicate intent precisely
...
* Only one obvious way to do things
...

So what is the one obvious way to implement something like this? What should I look for, or try?

4 Likes

Take a look at how std.mem.Allocator is implemented. And then how other allocators such as std.heap.ArenaAllocator and std.heap.page_allocator are implemented.

This is how you do interfaces the Zig way. Essentially what your friend suggested is correct. You use a vtable to define what functions you need to run your game, and then use raylib to implement these functions.

Another way would be to use an anonymous module that can be configured at build time in your build.zig. E.g.

exe.root_module.addAnonymousImport("renderer", .{
  .root_source_file = b.path("renderers/raylib.zig"),
});
1 Like

One useful tip here is that, whenever you find yourself wondering how to do something, the best way to figure out is to try distill some sort of a small toy problem, solve that, and then apply the solution to the real case.

So if you real problem is “I want to be able to swap backend of my game from raylib to a lower-level API”, the toy problem would look like this:

  1. Using ray lib, draw a triangle that moves when a key is pressed
  2. Do the same using that other API
  3. Figure out how to switch between the two versions, without duplicating the code.

When your toy program is about a hundred lines, it becomes much easier to experiment with various approaches and seeing what works and what doesn’t

12 Likes

Hello,
I’ll tell you my approach, it’s not dogma, but it’s worth what it’s worth.
When I’m developing a project, I granulate to identify any bugs, but it’s not infallible.

After that, it’s a matter of choice: either you leave the granulation to the user, or you create a namespace where you gather all your modules together, but this requires excellent documentation, on the other hand it simplifies things for the user, who will see the whole library available under one name.

Translated with DeepL.com (free version)

1 Like

I think what you need is not an interface in the OOP sense, that is only needed if you want to support multiple rendering backends at the same time.
But if I understand you correctly, you only want to replace the rendering backend and throw away the old one.

So what you need is a “programmer interface”/API that abstracts all the backend functionality in a backend-independent way.
E.g. you could just make one big file with an init/deinit function which put all the raylib state you need into a global variable. Then you can implement whatever functions you need, e.g. drawImage, drawText, …, think of it as if you make your own library abstraction over raylib.
Then when the time comes to switch, you basically just make a second file where you implement all those functions with the new backend and then you just switch the files. You don’t need a runtime interface for this. Of course during the migration process you will likely notice that not all your abstractions work well with the new backend, but this will happen no matter what.


In conclusion:
The one obvious way to structure your code is just use functions to abstract away the details (e.g. what backend you use) and put them into different files (to make API boundaries more clear).

8 Likes

Tbh, I wouldn’t bother with static or dynamic polymorphism until you absolutely need it. It’s one of those dangerous patterns that lure beginners into a complexity trap.

Sometimes you’ll need to ‘inject’ user-provided code into a subsystem / module (e.g. dynamic polymorphism), and the best approach for this both in Zig and C is a simple struct with function pointers.

I would never design a whole code base around this idea though (or rather: no longer since weaning off the OOP mind virus), instead use function pointer structs sparingly and not as a ‘virtual method table replacement’, and when you feel the need for a class hierarchie and virtual methods, trace back and find a better solution :wink:

3 Likes

PS: specifically for wrapping raylib so that it is exchangeable with something else later:

Most importantly: define the subset of raylib that you actually need, and wrap that feature subset into your own medium-level module which is sort-of specialized for the type of game you want to build. Maybe different medium-level modules for rendering, audio, input and asset-loading/streaming.

Maybe put those into a subdirectory engine/raylib/, e.g.

engine/
    raylib/
        render.zig
        audio.zig
        input.zig
        raylib.zig // => re-export render.zig, audio.zig and input.zig under a module 'raylib'

…later you could add different implementations with the same module API (e.g. the same public functions and types):

engine/
    sdl/
        render.zig
        audio.zig
        input.zig
        sdl.zig

Then above you might have an engine.zig module which conditionally imports one or the other backend:

engine/engine.zig:

const build_options = @import("build_options"); // => defined in build.zig
const engine = switch (build_options.engine) {
    .raylib => @import("raylib/raylib.zig"),
    .sdl => @import("sdl/sdl.zig"),
};

…and any higher level code just imports engine.zig. This is basically static polymorphism via modules and is as efficient as it gets.

You might also consider splitting your project into a handful highlevel modules in the build system (e.g. in my example this would make sense for the engine/engine.zig file), this lets you import those modules from anywhere in the project regardless of the directory structure (IIRC Zig doesn’t allow @import() via a parent directory:

3 Likes