$ time make
rm -rf build
mkdir build
cp -r assets build/assets
gcc -std=c11 -Wall -Wno-unused-result -fno-common -DSOGL_MAJOR_VERSION=3 -DSOGL_MINOR_VERSION=3 -D_POSIX_C_SOURCE=199309L -o build/space-shooter -g -DSPACE_SHOOTER_DEBUG src/shared/*.c src/game/*.c src/platform/linux/*.c -lX11 -ldl -lGL -lm -lpthread -lasound
real 0m0.437s
user 0m0.288s
sys 0m0.079s
that's what I like to see...
Conversely, I recently wrote a simple grafana plugin (my first time using typescript and "modern" web tooling) and I don't know what the hell yarn does but I can compile and play this game several times before it finishes.
Cool. Also nice to see a well written ARCHITECTURE.md. I like this bit about dealing with memory:
> Almost all memory allocations in space-shooter.c are static, with dynamic allocations only used to load image and sound assets when the game initializes. This leads to a nice "programmer peace of mind" benefit that once the game initializes, I no longer have to worry about errors related to allocating or freeing memory.
It's something I also do myself in C projects - in fact in small programs I hardly ever find myself doing dynamic allocations at all. Functions that generate data don't allocate the memory themselves but receive a reference to an output location. That principle extends to larger programs. (It's not novel either, most system level libraries do this)
Thanks! This was a revelation for me on a few levels:
1. That memory management doesn't have to be scary with a little forethought, at least for programs where you can set a reasonable upper bound on the resource requirements.
2. That it's possible to structure at least a subset of programs in such a way that error states can only be entered during initialization, and that makes the rest of the program much easier to reason about.
very cool project. i wonder if the libraries used in this project also offer the guarantee of static allocations only / dyn allocations only at startup. i’ve been using the JUCE toolkit for a project the past 10 years and it has allocations all over the place. need 16 bytes? malloc. need to concat that string? a few mallocs for handfuls of bytes. i am stuck with it, so i have had to override malloc and friends and build my own fixed block size allocator. very annoying to find out you are only doing static allocations but then all the libs you use together amount to 3000 malloc calls per second. i even found out that a call like glBufferSubData will call malloc or related functions. so doing that on each frame seems to be a bad idea as well.
> i even found out that a call like glBufferSubData will call malloc or related functions. so doing that on each frame seems to be a bad idea as well.
That's interesting and not what I'd expect at all! Is there documentation you could point me to or did you find that out with some tooling? And is there a better way to update attribute buffers for instanced draw calls?
I did not look up any documentation to verify this but since I did override all allocation functions I could keep statistics of how many calls I was getting for each function, and when I commented out the glBufferSubData calls I could see my stats drop. The contribution was somewhere around 300 calls/s I think... this is with the panfrost driver with gallium/mesa on an ARM platform. My plan is to use persistent buffers which are memory mapped once. Hopefully that will get rid of these allocation calls.
This is one of the main advantages of using a lower level library like Vulkan or DX12. Higher level APIs like OpenGL and DX11 do a lot of work for you, but that work can result in extra allocations that you can't control, starting driver threads, extra resource transitions, etc
Thanks for the tip! I'll try to figure out if something similar is happening on either of the machines I'm using. That's a bummer that an in-place update function would be allocating behind the scenes...
I'm guessing the data is copied to a temporary buffer. The target GL buffer can still be in flight for previous operations.
If I remember correctly, that safety can be disabled with the right incantation of flags on the buffer.
Always a pleasure to look at someone's self-contained small game written in C, especially when the source is relatively well organized.
During the pandemic lock-downs a musician buddy pulled me into a 24-hour IRC-hosted "wild" compo, which usually sees mostly ANSI/music submissions. I took some boilerplate code from another C game I had shipped and hacked something together in an all-nighter we called SARS:
It uses GLAD, SDL2+OpenGL, and autotools in terms of third-party dependencies. There are some git submodules but they're in-house developed vendored stuff I try to reuse in the interests of saving time and not repeating bugs/fixes across projects.
Being a 24-hour hack it's super rushed and messy in places. But I think it may be interesting to skim relative to space-shooter.c just to see how differently one can structure these things, since it too is quite small and pure C with a smattering of GLSL.
The title led me to think it might be a single file, but still this is good stuff.
You could probably make it a single file if you used the wonderfully obscure XPM image format for your sprites and assets. It is both a C source fragment and an image format in one! https://en.wikipedia.org/wiki/X_PixMap
On Linux you can use "ld -o myobj.o -r -b binary myfile.dat" and it will generate a linkable object file that contains your file data as a byte array. However, other platforms do it other ways, so it's generally easier to use "xxd" from the vim package to generate a C source file with a literal array, similar to the way that XPM works. That will work on all platforms.
I totally recommend to go for it. It's not that scary, just a few concepts to learn. For example on Windows, you roughly need to know about HANDLE, HWND, WNDCLASSEX, RegisterWindowClass(), CreateWindow(), how to write a window proc to handle events coming from the OS, how to write a simple message loop to pump those events. To put something on the screen, look up the drawing context HDC, the RECT structure, and use FillRect() to draw colored axis-aligned quads.
Optionally, later you move to a custom memory-backed backbuffer allocated using CreateDIBSection(), so you can just set each pixel using the CPU as a uint32_t RGBA value. That allows you to go wild, you can proceed to write your own 3D game engine with nothing to distract you - it's you, the CPU, and the backbuffer memory. (It will be running at software rasterizer speeds, of course - but it should be easy to get very good performance at say 640x480).
It shouldn't take you more than a few hours to maybe 2 days to get the ball rolling, depending on your prerequisites. I initially found the Win32 API to be a bit arcane with its overboarding use of preprocessor and of typedef'ed types (even "pointer-to" typedefs like LPCSTR instead of simply "const char *"). But beyond these superficialities, I find that large parts of it are fairly well designed, and anyway the code to interface with the OS can all be kept in a central place.
Once you're a bit accustomed to these things, maybe afterwards you'll look back and wonder how you could put up with the piles of abstractions all these fluff libraries put on top. And personally, while this approach is not suited to quickly hack up a GUI in a day, I find it's a great feeling to be in control of everything, and this will show in the quality of the product as well.
Thanks! Nothing against SDL. It's a great library, and I referenced its source code a lot while writing the platform layers. But I did find it incredibly satisfying to write everything myself. If you do ever decide to take a crack at it, I highly recommend Handmade Hero as a starting point: https://handmadehero.org/
Nice work you have here, I will go ahead and ask for the curious ones what is your background and what would your top recommended books, videos, and additional educational resources that you used to learn C and game development in C?
Also, have you thought about making your own indie game studio to do this full time or on the side if you are not already doing so?
Not OP but I'm currently writing a game in C: just throw yourself into it. The language itself is minimal enough that you probably won't need much guidance other than looking up library functions and with modern tools like the various sanitizers in clang and valgrind it's hard to go too wrong.
Loving the fact that OOP here ends with the structures and no heavy java-style class bloat. The code is very well ordered, and to my surprise - not that long! Which speaks of proper architecture. Good work, good example!
$ space-shooter
FATAL ERROR: Unable to initialize renderer.
PS: figured it out, I had copied the binary to a separate folder but didn't copy build/assets too; doing that fixed the issue. Submitted PR to add note to Readme to mention this in case anyone else faces this issue.
PPS: another issue is that once you copy it to another folder and create a symlink to it, it crashes again with the same error i.e. I doesn't resolve to the real path to get the assets location.
Thanks! My first child was born last year, and I started a new job 3 months earlier, so time was definitely not an abundant resource! But between the stress of all that and the pandemic, I found squeezing in an hour or two for this project here and there was one of the few things keeping me sane, and that's what got me through it.
Unfortunately, the Mac port would likely involve calling into (or out from) a little Objective-C, unless you used the ancient Carbon APIs for graphics and window management.
So it’s not possible to interface with Metal or the Accelerate framework on macOS using pure C? I’ve found some outdated C wrappers for metal but nothing up to date, and wondered if something in the later versions assumed Swift or Objective C library consumption…
Theoretically, Metal can be called from C by going directly through the Objective-C runtime API, but this requires either macro magic (like: https://github.com/garettbass/oc/), or code-generated headers (which was most likely used in the official Metal-cpp wrapper: https://developer.apple.com/metal/cpp/)
But it's not a big deal to move all the ObjC code into a separate .m source file and expose a smaller and higher level C API.
Metal is an Objective-C API. It is possible to call arbitrary Objective-C APIs from pure C using the Objective-C runtime functions, and Apple themselves recently released a Metal API wrapper written in C++ that works this way. [1] I wouldn't recommend that approach, though. With Objective-C being an almost-pure superset of C, it's a lot easier to just build your code in Objective-C mode and make native Objective-C calls where necessary.
These packages are only needed for building the project, not for running it (and X11, GL and ALSA can be considered 'system libraries', because there's no way to create a X11+GL+ALSA application only with syscalls, you'll have to go through the DLLs APIs).
I know it goes against the HH ethos but... you should consider using SDL2 in the future for projects like this. You'll get greater (and better tested) cross-platform compatibility, support for more peripherals, better image support, etc, and not have to write most of this on your own.
From the description of the project, I would dare a guess that the intent of this project was to go as self-contained as reasonably possible:
> […] written in standard C11 using only system libraries (with system libraries defined as anything included in the C standard library or supported operating systems).
I'm not missing the point, I'm just saying that in general, outside of specifically having the goal of not using any third party libraries, SDL is a good idea for a project like this.
Libraries like that might make it easier to get started, but they tend to limit what you can do. For example, I recently created a Desktop GUI app that could take inputs from a networking socket. How do you do that cleanly? There is often a way around limitations, for example by creating a separate thread, or by polling every so many milliseconds (which is ugly and a waste of resources). In my case, interfacing directly with Win32 without a 3rd party layer in between, it was easy to create an Event Object for the socket and am calling MsgWaitForMultipleObjects() in my message loop. Not sure what's a good solution to do this with SDL, but why should I even bother...
Also, it seems to create a separate listener thread just to pump the messages, which I'm not sure I like.
I could imagine SDL supports new event types by requiring the user to create _yet_ another thread, which waits for e.g. network events, then submits them to the SDL main event queue.
IMHO SDL mostly makes sense on Linux because it hides a lot of really ugly window system and GLX setup code.
On Windows (with Win32+DXGI+D3D11) and macOS (with Cocoa+Metal+MetalKit), things like setting up a window, 3D device and swap chain is just a few lines of relatively straightforward code, so SDL is by far not as useful there as on Linux.
...which is also quite trivial with OS-native code on Win32 and macOS. Besides, the whole point of the OP's project is to not use separate dependencies.
Conversely, I recently wrote a simple grafana plugin (my first time using typescript and "modern" web tooling) and I don't know what the hell yarn does but I can compile and play this game several times before it finishes.