The best and most original games usually come from quick prototyping with placeholder graphics and the idea that you are just doing it for yourself, for fun. This will unleash more creativity by not censoring ideas that sound too silly at first and allow you to try it quickly. Build reusable code that can easily by made into something else with some changed settings and imagination.
All games start out as small hacks going straight for the goal just to test things, then needs higher abstractions as the duplicated patterns pile up. Don't obsess over abstractions before they are needed, but don't get stuck writing the same boilerplate code over and over again.
Common steps:
- Outlining coherent 2D/3D coordinate systems for your sanity
- Visual representation of the world
- Player to navigate with while the camera follows
- Animations, particles, light and weapons
- Physics and basic opponents
- Level editor
- A goal to reach, loading the next level
- Saving/loading game progress (use integers when possible)
- Menu/overlay/view system (allowing top down world view to enter side-scroller level)
- Scripted events and story
If you hit a roadblock with memory leaks and crashes when the code gets bigger and more advanced, one can also use C-style C++ to get some extra power with templates and automatic reference counting when needed. C is easier to learn from being small, but C++ can offer libraries with beginner friendly safety abstractions for quicker prototyping and less crashes.
The biggest obstacle to stability in C/C++ is optimizations that work on raw pointers where you see the start, but not the size. I use a C++ class wrapped around a raw C pointer that only remembers the allocation size during debugging. This allow doing low level pointer optimizations for release mode, with the same safety as a high level language in debug mode. External memory debuggers can detect when going outside of the allocation requested from the operating system, but this won't work when using a custom allocator and might not be triggered until after the whole application has produced incorrect results, sent corrupted pointers to GPU drivers, overwritten your files with garbage data and crashed the whole operating system beyond recovery.
https://github.com/Dawoodoz/DFPSR/blob/master/Source/DFPSR/base/SafePointer.h
File serialization is one of the most common places to find bugs in released products, because it's both much more complex and called a lot less often. The consequence of a failed save is also a lot worse because it won't go away from restarting the game. Instead of copy pasting the same file access code for each file format to parse, one can create a wrapper around raw memory allocations for knowing the allocation's size at all time. Giving easy reference counted sharing of data and reusable methods for cloning, saving/loading, archive packing/unpacking, compressing and encrypting. Then you can easily regression test serialization by generating random objects, serializing them to memory buffers, parsing back to object, and comparing if they are equal, billions of times in a loop until all bugs are gone.
https://github.com/Dawoodoz/DFPSR/blob/master/Source/DFPSR/api/bufferAPI.h
Reading source code from old ID software games can show how commercial games were written from scratch in C, before game-makers took over the market. The Quake 3 codebase is quite clean and minimalistic, despite being a high budget game.
https://github.com/id-Software