Hacker News new | past | comments | ask | show | jobs | submit login
Space-shooter.c: cross-platform, top-down 2D space shooter written in C (github.com/tsherif)
210 points by ingve on Dec 11, 2021 | hide | past | favorite | 73 comments



  $ 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.


Whenever I come back to C from Rust and this happens, it freaks me out. Like, is that it? Did it finish already?


It also happens to me in C++, because I always use binary libraries.

Still looking forward to the day cargo will support them.

Having said that, the more AOT compiled languages that join the Turbo Pascal/Modula-2/Oberon/Delphi/Eiffel/Go compile times club the merrier.


Try Haskell ROFL


use esbuild or something which uses esbuild (e.g. vite). Fast compilation on code change is a must.


Esbuild by itself is still an order of magnitude faster than any traditional bundler using it.


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.


in that case you probably need a barrier? which is what you need with persistent mapping too?


I practice something similar, I've statically allocated the physical RAM modules when I built my computer. O:)


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:

https://git.pengaru.com/cgit/sars/.git/tree/

https://www.pouet.net/prod.php?which=85496

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.


Sweet, thanks for sharing! I'll definitely take a look.


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


What are upsides of using this format instead of a more generic array of pixels (something like output of imagemagick to .h files or PPM files)?

BTW I know one can use resource files (.rc) on Windows to embed various resources, but is there an analogous mechanism for linux binaries?


The examples in the wiki article show the difference pretty well. XPM lets you sort-of see what the image is just by looking at the source file.

E.G. compare this

  static char blarg_bits[] = {
  0x13, 0x00, 0x15, 0x00, 0x93, 0xcd, 0x55, 0xa5, 0x93, 0xc5, 0x00, 0x80,
  0x00, 0x60 };
with this

  static char * blarg_xpm[] = {
  "16 7 2 1",
  "* c #000000",
  ". c #ffffff",
  "**..*...........",
  "*.*.*...........",
  "**..*..**.**..**",
  "*.*.*.*.*.*..*.*",
  "**..*..**.*...**",
  "...............*",
  ".............**."
  };


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.


That's an interesting idea. Probably won't change the format for this project, but something I might consider in future ones. Thanks for the tip!


Kudos for not using something like SDL2. I'd be very reluctant to do so because C is so barebone, but I admire anyone who can pull this off.


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.


Now that is something I can agree on. :)


:-)


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/


Oh wow, it's all done using no 3rd party libraries too. Gonna bookmark this just to browse through later


For the reference, Tyrian source code:

https://github.com/andyvand/OpenTyrian

AAA PC shoot’em up for PC from 90s.


The scroll-speed and the graphics remind me of the magnificent Xenon II Megablast.

https://www.youtube.com/watch?v=v9nD9DQwd80


Bitmap Brothers had such unique art style! G.O.D.S.!


And the amazing music by Bomb the Bass:

https://www.youtube.com/watch?v=bWbJeUEKzc8


Hadn't seen this before, but that's a flattering comparison. Thanks!


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?


Thanks!

If you go through the architecture doc (which I'm almost done writing), you'll find links to most of the references I used: https://github.com/tsherif/space-shooter.c/blob/master/ARCHI...

Top one, though, would be Handmade Hero: https://handmadehero.org/


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.


These would probably be fun to change. :-)

// Player constants

https://github.com/tsherif/space-shooter.c/blob/1b97e85cc7f2...


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!


Thanks! I definitely tried to focus on only using as much abstraction as was actually helpful in getting it to work the way I wanted it to.


You mean 1990's C++.


Hi, just wanted to say that this project is awesome and inspiring. Thanks.


Thank you!


Pop!_OS 21.10 - builds ok but can't run:

    $ 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.


I love this. My ambition is to become less busy (i.e., retire) to do stuff like that; fun code, with no obligations.


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.


Congratulations for first child and new job :)

(reached level 4, but I can do better)


Thank you!


Can't build on mac, anybody figured that out?


For now, it's Windows/Linux only. I hope to write a Mac platform layer eventually. I just don't currently have a Mac machine to work on...


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.


Accelerate is a pure 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.

[1] https://news.ycombinator.com/item?id=29289761


Ack! Right I hadn't thought of that. I'll have to decide if I'm ok with breaking the "written in standard C" constraint on this project...


There's no platform code for macOS yet. (It's a goal of the project to not use cross-platform abstractions)


Does Mac not ship with X11 anymore?



You can still install XQuartz, though.


A cross-platform, top-down 2D space shooter written in C using only system libraries.

   sudo apt install linux-libc-dev libx11-dev mesa-common-dev libasound2-dev
(x) doubt


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.


In a sense, I did use SDL. The source code of SDL, along with GLFW and Sokol, were my primary references when writing the platform layers.


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 think you're missing the point.


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...


SDL can handle events and message passing just fine, but you would have to write most of that yourself either way, since it isn't a framework.


How do you mean it "can handle events"? I barely know SDL, but it does not look like it is super straightforward to integrate a socket:

    https://github.com/libsdl-org/SDL/blob/main/src/video/windows/SDL_windowsevents.c#L1430
    https://github.com/libsdl-org/SDL-1.2/blob/main/src/events/SDL_events.c#L403
    https://github.com/SDL-mirror/SDL/blob/master/src/events/SDL_events.c#L769
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.


SDL does a lot more than open windows. It gives _cross-platform_ graphics, audio, and input handling.


...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.


"OS-native" implies not cross-platform.

??




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: