├── .gitmodules ├── .gitpod.yml ├── CMakeLists.txt ├── Dockerfile ├── LICENSE.txt ├── README.md ├── doc ├── 1.png ├── 2.gif ├── 3.gif ├── 4.png ├── 5.gif ├── 6.gif ├── 7.gif └── capture.gif ├── external └── CMakeLists.txt ├── resources ├── fall.bmp ├── fall.png ├── flight.bmp ├── flight.png ├── ground.bmp ├── ground.png ├── landing.bmp ├── landing.png ├── numbers.bmp ├── numbers.png ├── rest.bmp ├── rest.png ├── takeoff.bmp ├── takeoff.png ├── walk.bmp └── walk.png └── src ├── fps_counter.h ├── main.cpp ├── map.h ├── player.h └── sprite.h /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/SDL"] 2 | path = external/SDL 3 | url = https://github.com/libsdl-org/SDL 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: Dockerfile 3 | ports: 4 | - port: 6080 5 | onOpen: notify 6 | - port: 5900 7 | onOpen: ignore 8 | tasks: 9 | - command: > 10 | mkdir --parents build && 11 | cd build && 12 | cmake .. && 13 | make && 14 | ./sdl2-demo 15 | cd .. 16 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required( VERSION 3.7.0 ) 2 | project( sdl2-demo ) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") 8 | file(GLOB SOURCES 9 | "${SRC_DIR}/*.h" 10 | "${SRC_DIR}/*.cpp" 11 | ) 12 | add_definitions(-DRESOURCES_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/resources/\") 13 | 14 | find_package( SDL2 ) 15 | if( ${SDL2_FOUND} ) 16 | message(STATUS "Found SDL2") 17 | include_directories(${SDL2_INCLUDE_DIRS}) 18 | add_executable(${PROJECT_NAME} ${SOURCES}) 19 | target_link_libraries(${PROJECT_NAME} ${SDL2_LIBRARIES}) 20 | else() 21 | message(STATUS "Could not locate SDL2, using the submodule") 22 | add_subdirectory(external) 23 | add_executable(${PROJECT_NAME} ${SOURCES}) 24 | target_include_directories(${PROJECT_NAME} PRIVATE "external/SDL/include") 25 | target_link_libraries( 26 | ${PROJECT_NAME} PRIVATE 27 | SDL2main 28 | SDL2-static 29 | ) 30 | endif() 31 | 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full-vnc 2 | 3 | USER root 4 | # add your tools here 5 | RUN apt-get update && apt-get install -y \ 6 | libsdl2-dev 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A blank for a SDL2-based platformer game 2 | Attention, this repository does not contain (and never will) a playable game. The goal is to show basic principles to my students, so it is only a demo repo. Here is an example of what is inside: 3 | 4 | ![](https://raw.githubusercontent.com/ssloy/sdl2-demo/main/doc/capture.gif) 5 | 6 | **[Check the wiki](https://github.com/ssloy/sdl2-demo/wiki) for the detailed description of how the project is built.** 7 | 8 | At the moment of this writing the repository contains less than 300 lines of code: 9 | ```sh 10 | ssloy@khronos:~/sdl2-demo/src$ cat *.cpp *.h | wc -l 11 | 296 12 | ``` 13 | 14 | 15 | # Compilation (tested on linux and windows, macos is yet to try) 16 | ```sh 17 | git clone --recurse-submodules https://github.com/ssloy/sdl2-demo.git && 18 | cd sdl2-demo && 19 | mkdir build && 20 | cd build && 21 | cmake .. && 22 | cmake --build . -j && 23 | ./sdl2-demo 24 | ``` 25 | 26 | You can open the project in Gitpod, a free online dev environment for GitHub: 27 | 28 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ssloy/sdl2-demo) 29 | 30 | 31 | On open, the editor will compile & run the program as well as open the resulting image in the editor's preview. 32 | Just change the code in the editor and rerun the script (use the terminal's history) to see updated images. 33 | Note how awesome Gitpod is, it allows to run SDL2 games directly in the browser! 34 | 35 | -------------------------------------------------------------------------------- /doc/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/1.png -------------------------------------------------------------------------------- /doc/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/2.gif -------------------------------------------------------------------------------- /doc/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/3.gif -------------------------------------------------------------------------------- /doc/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/4.png -------------------------------------------------------------------------------- /doc/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/5.gif -------------------------------------------------------------------------------- /doc/6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/6.gif -------------------------------------------------------------------------------- /doc/7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/7.gif -------------------------------------------------------------------------------- /doc/capture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/doc/capture.gif -------------------------------------------------------------------------------- /external/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set( SDL_STATIC ON CACHE BOOL "" FORCE ) 2 | set( SDL_SHARED OFF CACHE BOOL "" FORCE ) 3 | add_subdirectory( SDL ) 4 | -------------------------------------------------------------------------------- /resources/fall.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/fall.bmp -------------------------------------------------------------------------------- /resources/fall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/fall.png -------------------------------------------------------------------------------- /resources/flight.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/flight.bmp -------------------------------------------------------------------------------- /resources/flight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/flight.png -------------------------------------------------------------------------------- /resources/ground.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/ground.bmp -------------------------------------------------------------------------------- /resources/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/ground.png -------------------------------------------------------------------------------- /resources/landing.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/landing.bmp -------------------------------------------------------------------------------- /resources/landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/landing.png -------------------------------------------------------------------------------- /resources/numbers.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/numbers.bmp -------------------------------------------------------------------------------- /resources/numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/numbers.png -------------------------------------------------------------------------------- /resources/rest.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/rest.bmp -------------------------------------------------------------------------------- /resources/rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/rest.png -------------------------------------------------------------------------------- /resources/takeoff.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/takeoff.bmp -------------------------------------------------------------------------------- /resources/takeoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/takeoff.png -------------------------------------------------------------------------------- /resources/walk.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/walk.bmp -------------------------------------------------------------------------------- /resources/walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/sdl2-demo/fc0340509e9a51b3a996839fe32307681dd00288/resources/walk.png -------------------------------------------------------------------------------- /src/fps_counter.h: -------------------------------------------------------------------------------- 1 | #include "sprite.h" 2 | 3 | struct FPS_Counter { 4 | FPS_Counter(SDL_Renderer *renderer) : renderer(renderer), numbers(renderer, "numbers.bmp", 24) {} 5 | 6 | void draw() { 7 | fps_cur++; 8 | double dt = std::chrono::duration(Clock::now() - timestamp).count(); 9 | if (dt>=.3) { // every 300 ms update current FPS reading 10 | fps_prev = fps_cur/dt; 11 | fps_cur = 0; 12 | timestamp = Clock::now(); 13 | } 14 | SDL_Rect dst = {4, 16, numbers.width, numbers.height}; // first character will be drawn here 15 | for (const char c : std::to_string(fps_prev)) { // extract individual digits of fps_prev 16 | SDL_Rect src = numbers.rect(c-'0'); // crude conversion of numeric characters to int: '7'-'0'=7 17 | SDL_RenderCopy(renderer, numbers.texture, &src, &dst); // draw current digit 18 | dst.x += numbers.width + 4; // draw characters left-to-right, +4 for letter spacing (TODO: add padding directly to the .bmp file) 19 | } 20 | } 21 | 22 | int fps_cur = 0; // the FPS readings are updated once in a while; fps_cur is the number of draw() calls since the last reading 23 | int fps_prev = 0; // and here is the last fps reading 24 | TimeStamp timestamp = Clock::now(); // last time fps_prev was updated 25 | SDL_Renderer *renderer; // draw here 26 | const Sprite numbers; // "font" file 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #define SDL_MAIN_HANDLED 6 | #include 7 | #include "fps_counter.h" 8 | #include "map.h" 9 | #include "player.h" 10 | 11 | void main_loop(SDL_Renderer *renderer) { 12 | FPS_Counter fps_counter(renderer); 13 | Map map(renderer); 14 | Player player(renderer); 15 | TimeStamp timestamp = Clock::now(); 16 | while (1) { // main game loop 17 | SDL_Event event; // handle window closing 18 | if (SDL_PollEvent(&event) && (SDL_QUIT==event.type || (SDL_KEYDOWN==event.type && SDLK_ESCAPE==event.key.keysym.sym))) 19 | break; // quit 20 | player.handle_keyboard(); // no need for the event variable, direct keyboard state polling 21 | 22 | const auto dt = Clock::now() - timestamp; 23 | if (dt(dt).count(), map); // gravity, movements, collision detection etc 30 | 31 | SDL_RenderClear(renderer); // re-draw the window 32 | fps_counter.draw(); 33 | player.draw(); 34 | map.draw(); 35 | SDL_RenderPresent(renderer); 36 | } 37 | } // N.B. fps_counter, map and player objects call their destructors here, thus destroying allocated textures, it must be done prior to destroying the renderer 38 | 39 | int main() { 40 | SDL_SetMainReady(); // tell SDL that we handle main() function ourselves, comes with the SDL_MAIN_HANDLED macro 41 | if (SDL_Init(SDL_INIT_VIDEO)) { 42 | std::cerr << "Failed to initialize SDL: " << SDL_GetError() << std::endl; 43 | return -1; 44 | } 45 | 46 | SDL_Window *window = nullptr; 47 | SDL_Renderer *renderer = nullptr; 48 | if (SDL_CreateWindowAndRenderer(1024, 768, SDL_WINDOW_SHOWN | SDL_WINDOW_INPUT_FOCUS, &window, &renderer)) { 49 | std::cerr << "Failed to create window and renderer: " << SDL_GetError() << std::endl; 50 | return -1; 51 | } 52 | SDL_SetWindowTitle(window, "SDL2 game blank"); 53 | SDL_SetRenderDrawColor(renderer, 210, 255, 179, 255); 54 | 55 | main_loop(renderer); // all interesting things happen here 56 | 57 | SDL_DestroyRenderer(renderer); 58 | SDL_DestroyWindow(window); 59 | SDL_Quit(); 60 | return 0; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/map.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct Map { 4 | Map(SDL_Renderer *renderer) : renderer(renderer), textures(renderer, "ground.bmp", 128) { 5 | assert(sizeof(level) == w*h+1); // +1 for the null terminated string 6 | int window_w, window_h; 7 | if (!SDL_GetRendererOutputSize(renderer, &window_w, &window_h)) { 8 | tile_w = window_w/w; 9 | tile_h = window_h/h; 10 | } else 11 | std::cerr << "Failed to get renderer size: " << SDL_GetError() << std::endl; 12 | } 13 | 14 | void draw() { // draw the level in the renderer window 15 | for (int j=0; j=0 && j>=0 && i=0 && j>=0 && i 2 | #include 3 | 4 | struct Player { 5 | enum States { REST=0, TAKEOFF=1, FLIGHT=2, LANDING=3, WALK=4, FALL=5 }; 6 | 7 | Player(SDL_Renderer *renderer) : 8 | renderer(renderer), 9 | sprites{Animation(renderer, "rest.bmp", 256, 1.0, true ), 10 | Animation(renderer, "takeoff.bmp", 256, 0.3, false), 11 | Animation(renderer, "flight.bmp", 256, 1.3, false), 12 | Animation(renderer, "landing.bmp", 256, 0.3, false), 13 | Animation(renderer, "walk.bmp", 256, 1.0, true ), 14 | Animation(renderer, "fall.bmp", 256, 1.0, true )} { 15 | } 16 | 17 | void set_state(int s) { 18 | timestamp = Clock::now(); 19 | state = s; 20 | if (state!=FLIGHT && state!=WALK) 21 | vx = 0; 22 | else if (state==WALK) 23 | vx = backwards ? -150 : 150; 24 | else if (state==FLIGHT) { 25 | vy = jumpvy; 26 | vx = backwards ? -jumpvx : jumpvx; 27 | } 28 | } 29 | 30 | void handle_keyboard() { 31 | const Uint8 *kbstate = SDL_GetKeyboardState(NULL); 32 | if (state==WALK && !kbstate[SDL_SCANCODE_RIGHT] && !kbstate[SDL_SCANCODE_LEFT]) 33 | set_state(REST); 34 | if ((state==REST || state==WALK) && kbstate[SDL_SCANCODE_UP]) { 35 | if (kbstate[SDL_SCANCODE_LEFT] || kbstate[SDL_SCANCODE_RIGHT]) { 36 | jumpvx = 200; // long jump 37 | jumpvy = -200; 38 | } else { 39 | jumpvx = 50; // high jump 40 | jumpvy = -300; 41 | } 42 | set_state(TAKEOFF); 43 | } 44 | if (state==REST && (kbstate[SDL_SCANCODE_LEFT] || kbstate[SDL_SCANCODE_RIGHT])) { 45 | backwards = kbstate[SDL_SCANCODE_LEFT]; 46 | set_state(WALK); 47 | } 48 | } 49 | 50 | void update_state(const double dt, const Map &map) { 51 | if (state==TAKEOFF && sprites[state].animation_ended(timestamp)) 52 | set_state(FLIGHT); // takeoff -> flight 53 | if (state==LANDING && sprites[state].animation_ended(timestamp)) 54 | set_state(REST); // landing -> rest 55 | if (state!=FLIGHT && map.is_empty(x/map.tile_w, y/map.tile_h + 1)) 56 | set_state(FALL); // put free falling sprite if no ground under the feet 57 | 58 | x += dt*vx; // prior to collision detection 59 | if (!map.is_empty(x/map.tile_w, y/map.tile_h)) { // horizontal collision detection 60 | int snap = std::round(x/map.tile_w)*map.tile_w; // snap the coorinate to the boundary of last free tile 61 | x = snap + (snap>x ? 1 : -1); // be careful to snap to the left or to the right side of the free tile 62 | vx = 0; // stop 63 | } 64 | 65 | y += dt*vy; // prior to collision detection 66 | vy += dt*300; // gravity 67 | if (!map.is_empty(x/map.tile_w, y/map.tile_h)) { // vertical collision detection 68 | int snap = std::round(y/map.tile_h)*map.tile_h; // snap the coorinate to the boundary of last free tile 69 | y = snap + (snap>y ? 1 : -1); // be careful to snap to the top or the bottom side of the free tile 70 | vy = 0; // stop 71 | if (state==FLIGHT || state==FALL) 72 | set_state(LANDING); 73 | } 74 | } 75 | 76 | void draw() { 77 | SDL_Rect src = sprites[state].rect(timestamp); 78 | SDL_Rect dest = { int(x)-sprite_w/2, int(y)-sprite_h, sprite_w, sprite_h }; 79 | SDL_RenderCopyEx(renderer, sprites[state].texture, &src, &dest, 0, nullptr, backwards ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE); 80 | } 81 | 82 | double x = 150, y = 200; // coordinates of the character 83 | double vx = 0, vy = 0; // speed 84 | bool backwards = false; // facing left or right 85 | double jumpvx = 0, jumpvy = 0; // will be used to differentiate high jump from a long jump 86 | 87 | int state = REST; // current sprite 88 | TimeStamp timestamp = Clock::now(); 89 | SDL_Renderer *renderer; // draw here 90 | 91 | const int sprite_w = 256; // size of the sprite on the screen 92 | const int sprite_h = 128; 93 | const std::array sprites; // sprite sequences to be drawn 94 | }; 95 | 96 | -------------------------------------------------------------------------------- /src/sprite.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct Sprite { 4 | Sprite(SDL_Renderer *renderer, const std::string filename, const int width) : width(width) { 5 | SDL_Surface *surface = SDL_LoadBMP((std::string(RESOURCES_DIR) + filename).c_str()); 6 | if (!surface) { 7 | std::cerr << "Error in SDL_LoadBMP: " << SDL_GetError() << std::endl; 8 | return; 9 | } 10 | if (!(surface->w%width) && surface->w/width) { // image width must be a multiple of sprite width 11 | height = surface->h; 12 | nframes = surface->w/width; 13 | texture = SDL_CreateTextureFromSurface(renderer, surface); 14 | } else 15 | std::cerr << "Incorrect sprite size" << std::endl; 16 | SDL_FreeSurface(surface); 17 | } 18 | 19 | SDL_Rect rect(const int idx) const { // choose the sprite number idx from the texture 20 | return { idx*width, 0, width, height }; 21 | } 22 | 23 | ~Sprite() { // do not forget to free the memory! 24 | if (texture) SDL_DestroyTexture(texture); 25 | } 26 | 27 | SDL_Texture *texture = nullptr; // the image is to be stored here 28 | int width = 0; // single sprite width (texture width = width * nframes) 29 | int height = 0; // sprite height 30 | int nframes = 0; // number of frames in the animation sequence 31 | }; 32 | 33 | using Clock = std::chrono::high_resolution_clock; 34 | using TimeStamp = std::chrono::time_point; 35 | 36 | struct Animation : public Sprite { 37 | Animation(SDL_Renderer *renderer, const std::string filename, const int width, const double duration, const bool repeat) : 38 | Sprite(renderer, filename, width), duration(duration), repeat(repeat) {} 39 | 40 | bool animation_ended(const TimeStamp timestamp) const { // is the animation sequence still playing? 41 | double elapsed = std::chrono::duration(Clock::now() - timestamp).count(); // seconds from timestamp to now 42 | return !repeat && elapsed >= duration; 43 | } 44 | 45 | int frame(const TimeStamp timestamp) const { // compute the frame number at current time for the the animation started at timestamp 46 | double elapsed = std::chrono::duration(Clock::now() - timestamp).count(); // seconds from timestamp to now 47 | int idx = static_cast(nframes*elapsed/duration); 48 | return repeat ? idx % nframes : std::min(idx, nframes-1); 49 | } 50 | 51 | SDL_Rect rect(const TimeStamp timestamp) const { // choose the right frame from the texture 52 | return { frame(timestamp)*width, 0, width, height }; 53 | } 54 | 55 | const double duration = 1; // duration of the animation sequence in seconds 56 | const bool repeat = false; // should we repeat the animation? 57 | }; 58 | 59 | --------------------------------------------------------------------------------