PC Games
• Orb Tutorials
• 2D shoot 'em up Latest Updates
SDL2 Versus game tutorial
Download keys for SDL2 tutorials on itch.io
The Legend of Edgar 1.37
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
Tags • android (3) • battle-for-the-solar-system (10) • blob-wars (10) • brexit (1) • code (6) • edgar (9) • games (43) • lasagne-monsters (1) • making-of (5) • match3 (1) • numberblocksonline (1) • orb (2) • site (1) • tanx (4) • three-guys (3) • three-guys-apocalypse (3) • tutorials (17) • water-closet (4) Books The Third Side (Battle for the Solar System, #2) The White Knights have had their wings clipped. Shot down and stranded on a planet in independent space, the five pilots find themselves sitting directly in the path of the Pandoran war machine as it prepares to advance The Mission. But if they can somehow survive and find a way home, they might just discover something far more worrisome than that which destroyed an empire. |
— Making a 2D split screen game — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction It's time to introduce the second major part of our game - the split screen view. As we'll soon see, this is far, far easier than one would at first have thought; it just involves two "cameras", one for each player, and SDL's ability to render to textures. The former we've seen many times before, but the render to texture is something new. Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./versus05 to run the code. You will see a window open like the one above, with each player on either side of our zone. Use the default controls (or, at your option, copy the config.json file from a previous tutorial, to use that - remember to exit the game before copying the replacement file). Play the game as normal. Notice how the camera view stays centered on the releveant player, and how it is also constrained to the bounds of the zone itself. Once you're finished, close the window to exit. Inspecting the code As stated before, adding in our split screen is very straightforward, and doesn't require a huge amount of effort. We need to make a number of tweaks to our code, mostly all around rendering, but as you can already see, this part is far smaller than one might expect. Let's start first with the changes to structs:
We've updated the Entity struct here, to change the `draw` function pointer. It now accepts an SDL_FPoint as the second parameter, that we're calling `camera`. This will hold the data about the camera position we want to render with. Next, we've updated Zone:
We've added two new variables here. `cameras` is an array of SDL_FPoints, that will hold our camera positions for each player. `bounds` is a variable that will hold the size of our world. We'll be using the SDL_Rect's `x`, `y`, `w`, and `h` as the corners. Since we're talking about the Zone bounds, let's move over to zone.c, to see how we're using it. We've updated just two functions in this file. Starting with loadWorld:
After we've loaded our world's triangles, we're going to loop through each one, and determine the overall maximum and minimum point values, with the help of our MIN and MAX macros (found in defs.h). Zone's `bound`'s `x` and `y` will be the smallest x and y found while checking our triangles, while the `w` and `h` will contains the largest `x` and `y` values. Notice how we first set the bound's `x` and `y` to a very large value, so our MIN test doesn't start at 0. Next, let's turn to drawWorld:
Here, we've updated the function to accept an SDL_FPoint as a parameter. All we're doing with it is adjusting our Vertex's position by `camera`, by subtracting `camera`'s `x` and `y` from the SDL_Vertex's `position`'s `x` and `y`. You will find that all the other draw function updates do the same. Moving on now to player.c. Again, we've made some small changes to this file, starting with `tick`:
At the end of the function, after we've moved our player, we're going to grab a pointer to the Player's `camera` in Zone (indexing using the Player's `num`), so that we can set its values. Something important to note here: normally, we would horizontally centre the player on the screen, by using half of the screen's width (SCREEN_WIDTH / 2). However, since we're in split screen, we need to halve this value again. Therefore, we're subtracting just a quarter of SCREEN_WIDTH. Additionally, when it comes to the maximum horizontal position of the camera, we're limiting to the width of the zone, less half of the screen width. Again, if this were full screen, this would be the full screen width. The `y` limits of our camera are as one might expect, making use of the zone's `y` and `h` bounds, less half the screen height or the full height, respectively. Now over to `draw`. The calculation here will look familiar:
Just as with drawing the world triangles, we are subtracting the `camera`'s `x` and `y` from the entity's position (as drawPosition), and passing this value to drawModel. This adjusts the entity's rendering position according to the camera view passed into the function. That's it for player.c. Next over to entities.c, where we've made a predictable change to drawEntities:
This function has been updated to accept an SDL_FPoint that acts as our camera position. We're passing this over to each entity when we call its `draw` function. Moving across to bullets.c, we can see we've done likewise with drawBullets:
When it comes to drawing our bullets, we're adjusting the rendering position (as drawPosition) before calling drawModel. Finally, let's move over to zone.c, where all the really interesting stuff happens. Starting with initZone:
We've introduced a new static variable here called playerViewportTexture. playerViewportTexture is an SDL_Texture, that, as the name suggests, will act as the viewport for our camera. It is a texture that can be used as a render target by SDL's APIs. What this means is that we can choose to draw to it, instead of the main screen. Once done, we can then draw this texture to the main display. For us, this has great benefits, such as clipping of object drawn, and the ability to position this texture wherever we wish (such as side by side). When creating the texture, note how we're making it half of our defined screen width (since this texture will occupy one half of the screen). The height remains the same, as expected. The texture is created with a target type of SDL_TEXTUREACCESS_TARGET, means we're allowed to draw directly to it. SDL_PIXELFORMAT_RGBA8888 is chosen as the format that is both commonly supported by native APIs and hardware, and meets our requirements. More information about the SDL_CreateTexture can be found here: https://wiki.libsdl.org/SDL2/SDL_CreateTexture. With that done, we come to the final (and most important) change to our code - the `draw` function:
You will immediately notice that we're using a for-loop, where before we were simply calling drawEntities, drawBullets, etc. just once. Let's walk through the changes to this function. As noted, we're now using a for-loop. We use this to loop through all the cameras in Zone (again, just two, one per player). We first call SDL_SetRenderTarget, passing over playerViewportTexture, to tell SDL to use playerViewportTexture as our render target, so all our subsequent drawing operations are sent to this texture. Next, we grab a reference to a camera (an SDL_FPoint) from Zone, indexed by `i`. We then call drawEntities, drawBullets, etc. passing over the SDL_FPoint. If you remember, in player.c we're setting the position of each Player's camera in the `tick` function. We're now using that camera data when it comes to drawing, so that we draw the scene correctly for the targetted player. drawEntities, drawBullets, etc. are all passed this camera (`c`). With all our drawing complete, we call flushGeometry, to ensure we no nothing left in our batch, then call SDL_SetRenderTarget again, this time passing over NULL, to return to our default / main render target (the screen). Lastly, we draw playerViewportTexture to the screen (via `blit`). It will be horizontally positioned at either the left-side of the screen or the middle of the screen, depending on whether this is player 1 or 2 that we're working with. This results in the views being positioned side by side. The last thing we do is draw a horizontal dividing line down the middle of the screen, via drawRect (being 1 pixel in width), to create a visual separation between the two views. Done! Wow, this is great! We have the makings of a split screen, two player game. We can control our ships separately, and render the scenes at different points of view. From here on out, we can start to introduce more of our core gameplay elements, knowing we've accomplished the most important aspects*. In the next part, we'll handle the player dying. (* - well, almost; the more astute amongst the readers will become aware of some minor trouble brewing) Purchase The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle: From itch.io |