Waaaay back in the dark ages of junior high school, I lost many hours to this classic 2D platformer from John Romero. My tribute to these good times is this tutorial for beginner programmers.
The ten-part video series recreates the game using C and SDL with a few interesting limitations: I’ve kept a static design using basic features of the C language. No function pointers, memory allocators, or customer data structures or operations. We fit around 1,000 lines in to a single file. Total run time of the videos is just under two hours.
Click on the picture to go to the video. Click on the title to go to the project page.
Part 1: Playthrough & Plan
|A playthrough of the original Dangerous Dave, pointing out gameplay and features. Brief discussion about tools and development strategy.
|Video runtime: 12:49|
Part 2: Extracting the Tileset
Part 3: Extracting Level Data
|The original binary contains the level information that we’ll need. We’ll built a utility to extract these data in to separate data files. Again, many thanks to levellass and Malvineous at shikadi.net.
|Video runtime: 5:15|
Part 4: The Game Loop
Part 5: Adding Dave
|We’ll add Dave to the world and give him basic movement abilities: Left, right, jumping, and picking up items.
|Video runtime: 17:34|
Part 6: Building Dave’s Capabilities
|Dave needs more features to get him through the game. We’ll start by finishing levels, then adding features like screen scrolling, weapons, and the jetpack.
|Video runtime: 16:27|
Part 7: Adding Monsters
|Beyond level two, each level will challenge Dave with monsters. We’ll get the monsters in to the world and get them moving and shooting.
|Video runtime: 16:27|
Part 8: Game Animations
|Let’s make the game more visually complete by adding animations. We’ll implement a game tick clock and use it to offset tile drawing.
|Video runtime: 7:13|
Part 9: User Interface
|The original Dangerous Dave has a status bar and several banners. We’ll bring those in to our implementation.
|Video runtime: 8:13|
Part 10: Loose Ends
|We need to add a few more things to make the game winnable. We’ll allow Dave to climb trees and stars. Then we need to add level wrapping so Dave can fall through the floor.
|Video runtime: 10:13|
The goal is a reasonably complete (and winnable) version of Dangerous Dave with the minimum amount of design. I've skipped many best-practices for scalability and maintainable design in order to keep this accssible for beginners. The code comes in at around 1000 lines in which we define 30 procedures. Below, we'll review the namespace of the project, which includes C standard library functions, SDL functions, and game state variables. I'll break down the game loop as it developed over the course of the project.
Standard C Functions
Here are the 7 functions you need to know or learn to understand this project:
|fclose||Closes a file previously opened using fopen|
|fgetc||Reads the next byte from a file|
|fopen||Opens a file|
|free||Releases memory previously allocated by malloc|
|malloc||Allocates a block of memory|
|sprintf||Outputs a string to a variable. This function is unsafe.|
|strcat||Concatenates two strings. This function is unsafe.|
SDL is a development library that connects program logic to output devices. We'll use 19 functions (of the 536 SDL2 defines) to bring Dangerous Dave to life.
|SDL_CreateTextureFromSurface||Creates an SDL_Texture from an SDL_Surface. We manipulate surfaces before conversion|
|SDL_CreateWindowAndRenderer||Sets up our output devices (the screen) during initialization|
|SDL_Delay||Stops game processing for a time. Blocking (non-busy) waiting|
|SDL_FreeSurface||Releases resources used by SDL_Surface|
|SDL_GetKeyboadState||Reads the keyboard state directly. Returns a state array|
|SDL_GetTicks||Records the current time stamp for the app|
|SDL_Init||Starts SDL within the application|
|SDL_LoadBMP||Reads a bitmap file and returns a surface|
|SDL_Log||Sends a string to the SDL log|
|SDL_MapRGB||Converts an input RGB value to the desired format|
|SDL_PollEvent||Checks if there is an SDL_Event waiting|
|SDL_Quit||Ends the SDL context|
|SDL_RenderClear||Clears the back buffer with the current color|
|SDL_RenderCopy||Pushes a texture to the renderer (similar to blitting surfaces)|
|SDL_RenderFillRect||Draws a filled rectangle|
|SDL_RenderPresent||Swaps back buffer to the front|
|SDL_RenderSetScale||Scales the scaling for renderer output|
|SDL_SetColorKey||Sets the transparency for a surface|
|SDL_SetRenderDrawColor||Sets the current drawing color|
We define 30 procedures that make up the heart of our game. All procedures are composed from the above functions and various flow control options in C (loops, conditions, etc). Our procedures are:
|add_score||Adds points to score and checks for extra lives earned|
|apply_gravity||Applies gravity to Dave|
|check_collision||Updates collision point status for Dave|
|check_input||Reads keyboard state and sets update flags|
|clear_input||Removes keyboard state flags|
|draw_dave||Renders Dave to the screen|
|draw_dave_bullet||Renders Dave's bullets to the screen|
|draw_monster_bullet||Renders enemy shots to the screen|
|draw_monsters||Renders enemies to the screen|
|draw_ui||Renders UI elements, like score.|
|draw_world||Renders the world environment|
|fire_monsters||Fires enemy weapons|
|init_assets||Loads level data and tiles|
|init_game||Sets up game state|
|is_clear||Checks if a level square is empty and passable|
|is_visible||Checks if a level square is visible|
|move_dave||Moves Dave depending on input and state|
|move_monsters||Moves monsters along their path|
|pickup_item||Picks up an item for Dave|
|render||Main render routine|
|restart_level||Moves Dave back to starting position|
|scroll_screen||Moves the screen if necessary|
|start_level||Sets up level start state|
|update_dbullet||Moves Dave's bullet|
|update_ebullet||Moves enemy bullets|
|update_frame||Allows animation through sprite frames|
|update_game||Main game loop update routine|
|update_level||Checks level-wide game state events|
|verify_input||Checks keyboard input for validity|
Most of our development time is spent building features and plugging them in to our game loop. The first development video includes us setting up the fixed 30 FPS game loop with empty functions for checking input, updating game, and rendering. Ultimately, we build towards the final result which looks like the diagram below:
The final piece to our design puzzle is the game state. This is what we update every game loop iteration using our procedures above. Since we're using C, I've stashed the game state in one large struct and two small structs. We pass the struct around in each step of the loop and update as needed. The elements of our game state structs include:
|Game State Variable||Purpose|
|can_climb||Flag set if Dave is standing on a climbable tile (trees, stars, etc)|
|check_door||Flag triggered if Dave is standing on the level exit door|
|check_pickup_x||World grid X position to trigger a pickup|
|check_pickup_y||World grid X position to trigger a pickup|
|collision_point||A set of nine points that hold a collision state (clear or not)|
|current_level||The level Dave is on|
|dave_[some_action]||A flag set after an action has verified and triggers it in the game loop|
|dave_dead_timer||A countdown timer when Dave dies. Controls animation an level reset|
|dave_px||Dave's pixel X position in the world|
|dave_py||Dave's pixel Y position in the world|
|dave_tick||Timer used to control Dave's animations|
|dave_x||Dave's grid X position|
|dave_y||Dave's grid Y position|
|dbullet_dir||Direction of Dave's bullet|
|dbullet_px||Pixel X position of Dave's bullet|
|dbullet_py||Pixel Y position of Dave's bullet|
|dead_timer||A dead timer used for each monster. Controls animation|
|ebullet_dir||Direction of monster bullets|
|ebullet_px||Pixel X position of monster bullet|
|ebullet_py||Pixel Y position of monster bullet|
|gun||Flag set if Dave has a weapon|
|jetpack||Value set if Dave has the jetpack. Double's as the fuel level|
|jetpack_delay||Timer used to prevent repeated toggling of the jetpack|
|jump_timer||Used to control Dave's jump characteristics|
|last_dir||The direction Dave is facing|
|lives||Current lives left|
|monster_px||Monster pixel X position|
|monster_py||Monster pixel Y position|
|monster_x||Monster world grid X position|
|monster_y||Monster world grid Y position|
|next_px||Monster movement waypoint pixel X position|
|next_py||Monster movement waypoint pixel X position|
|on_ground||Flag set if Dave is on the ground|
|path_index||Monster's current position on it's predefined path|
|quit||Game loop quit flag|
|score||Player's current score|
|scroll_x||Amount of screen scroll required|
|tick||Global game timer. Seeds tile animations|
|trophy||Flag set if Dave has the level trophy|
|try_[some_action]||Flag set if player has pushed a key for an action|
|type||Monster type. Doubles as tileset index|
|view_x||World grid X position of the screen|
|view_y||World grid Y position of the screen|
Put everything together and the result is: Dangerous Dave!
How long did it really take?
Two days, about 4 hours each. Day one was asset extraction and putting together the world…roughly the first 4 videos. Day two covered the game play. Setting up the videos, github, and this website took about a week. Overall, this entire project took about 10 days. Short enough to keep the fun alive.
Is this really a beginner programming project?
Conceptually, yes this is very much a beginner project. The biggest hurdle might be using C. I’m not sure how common C programming is among beginners these days. I’d also debate the value for a beginner to start with C…OOP designs reign supreme in the 21st century.
What features are missing from this version?
A few things are missing. I didn’t try to work on the PC speaker sound effects. Also, the original game had level transitions that I didn’t implement. High scores are not tracked between game runs. Finally, the main menu and victory splash screen are not here. I didn’t feel that these features contribute enough to the core game to include. If there is enough interest, I’ll consider adding another hour of video to fit them in.