— Making a 2D split screen game — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction We can fly, we can shoot. What we need now is a place to do it all. Up until now, we've been doing these things in a dark void. Some sort of environment would be good. In this part, we're going to introduce just that, by loading the world data from a file, and drawing it. We'll also be able to interact with it, by flying into it and shooting bullets into it. Just like our models, our zones are constructed from triangles, and are loaded from files that detail the coordinates of all the points. Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./versus03 to run the code. You will see a window open like the one above, with the player's ship located just below the middle of the screen. Use the same controls as before. Note how if you fly into the walls or central point you will bounce off them. Bullets that are fired will also accurately strike the walls and middle triangle. Once you're finished, close the window to exit. Inspecting the code Loading, drawing, and processing our world is extremely simple, as we'll see. At this point, there isn't very much to it, although things will get a bit more complicated later on. For now, we can get everything setup, and lay the foundations. Starting with structs.h:
The only thing we need to do here is to update the Entity struct, to add in the familiar `touch` function pointer. This will be invoked whenever something touches an entity, whether it be the environment (the world), a bullet, another player, an item, etc. This is very similar to the `touch` functions we've seen in the past. Moving on now to world.c. This is where we're doing all our major world processing. There's a number of functions in this file, but nothing difficult to understand. Let's start with initWorld:
We're first setting up a linked list, to hold the list of Triangles that will make up our zone (as `head` and `tail`). We're also randomly choosing a colour with which to render our zone. We do this by choosing a random hue (0 - 360) and converting it into RGB. The hsvToRGB function can be found in util.c (it's a very common function, so we won't detail it here). With our RGB values determined, we set them into an SDL_Color called worldColor that we'll use later on. With our init step complete, we can next look at the loadWorld function. Before we get to that, we should take a look at the format of the zone data file itself: 9 0 0 0 900 50 0 50 0 0 900 50 900 50 0 50 30 1600 0 1600 0 50 30 1600 30 1600 850 50 900 1600 900 1600 30 1540 30 1600 850 50 850 50 900 1600 850 1540 30 1540 850 1600 850 800 370 700 530 900 530 Much like the models file, our world data is very simple. The first line details the number of triangles in the file (one per line, so this number can also be seen as the number lines that follow). The triangle data itself is a series of x and y values, 6 per line, that make up the three points of our triangle. There is no colour data to be found, unlike the Models. Now let's look at the loadWorld function itself:
The function accepts a parameter, zoneNum, that represents the number of the zone we want to load. This is used to construct the `filename`. We open the file, and scan the first line, to get the number of triangles stored (as numLines). We then loop through all the lines in the file, calling a function named spawnTriangle to create a Triangle (assigned to `t`). We then use another for-loop to read each triangle point (3 of them), and assign them to the `x` and `y` variables of said point. That's it..! That's all we need to do to load our world data. As you can see, it's very simple, and just like loading models (except without the colour information). Next up, we have drawWorld:
It should be clear what's going on here, but I'll briefly detail. We're looping through all the Triangles in our linked list, setting the SDL_Vertex data (`v`) using each one, and passing that data to our drawVertex function. We next come to spawnTriangle:
As you'd expect, this function creates a Triangle and adds it to our linked list. One thing of note is that when we create the Triangle, we're assigning its colors to those of worldColor, that we randomly set in our initWorld step. What we're next doing is darkening the RGB values of the first point by 15%. This produces a very subtle gradient in our world, per triangle, to stop it from looking completely flat and boring..! One could remove these three lines to have a flat look, if they desired, without breaking anything. Finally, we have getWorldTriangles:
This is a actually a temporary function we've made until we implement our spactial grid, later on. All it does is return the first Triangle in our linked list, so we can loop through them, for the purposes of collision detection. It will be removed in a later part. That's world.c complete. We can now move on to seeing how we're handling collision detection and response. We need our players and bullets to interact with the world, rather than ignoring it. So, first over to player.c, where we've updated initPlayer:
We're assigning a `touch` function to our player entity. Next up, we have the actual `touch` function:
For now, nothing special. If the player touches anything, we'll call `bounce`, to make repel them in the opposite direction to where they were going. That's all for player.c. Over next to entities.c, where we've updated doEntities:
As well as calling an entity's `tick`, we're now testing if they have a `touch` function set, and are calling touchWorld, if so:
The summary of this function is that we're looping through all the triangles in our world (with the help of getWorldTriangles). We're then testing to see if the bounding sphere of our entity is intersecting any of the lines in our Triangle (our for-loop will test lines 0-1, 1-2, 2-0, in that order). We call a function named lineCircleCollision (in util.c) to check this. If the check resolves to true, we'll call the entity's `touch` function (passing over NULL right now). Not complicated, at all. We're basically seeing if any of the lines of a given Triangle are intersecting our entity (via their bounding sphere). You're probably aware that this means that if our entity is placed directly inside a Triangle, without touching any lines, this test will fail. While true, I have never seen this happen to the player, even when their ship is moving extremely fast. We can therefore safely conclude that this check meet the needs of our game. While trying this part out, you will likley have become aware of the fact that the player's ship is allowed to enter portions of the world, without issue. This is, again, due to the bounding circle having a radius smaller than the ship itself. This is to provide some leeway, and is quite a common compromise when handling collision checks. Further, you might be wonder why we're not checking if the entity's model's triangles are intersecting with the world's triangles? While this gives us a highly accurate collision check, that looks great, it has a huge downside - and that is that the player can become stuck in the world all too easily. Rotating around, for example, can cause our model's triangles to enter the world and prevent the player from then being able to move. A solution to this would be to perform various checks before applying the rotation (a near-full physics simulation). However, this now has gameplay issues, in that the player can drive themselves into a corner from which they cannot escape (unless we give them the ability to reverse). All in all, a fully accurate collision check for the player would become far too frustrating, and make our game less fun to play. So, now you know. There's a method to the madness! As for the line-circle check itself - this is some complicated maths that can found in a numerous forms and places (internet, books, your old trigonometry homework ...). I won't pretend to understand it all. For the curious, the function is listed below:
That's it for our entites. We can now deal with bullets interacting with the world. Starting with doBullets:
For each bullet, we're now calling a new function named touchWorld, passing the bullet (`b`) over. The touchWorld function is defined as such:
Once again, we're fetching all the world's Triangles, and looping through them. This time, however, we're testing if the bullet's center point (its origin) has entered into any triangles, using a function called pointInTriangle. This gets us a much nicer and accurate collision test than checking the radius against the world (and in this case, we don't need to worry about the bullet getting stuck). If the bullet has hit the world, we'll set its `health` to 0, and return from the function. This function also now tests if the bullet has moved outside of the screen (not that such a thing is possible in this demo zone), but it's here anyway..!). For those interested, the pointInTriangle function can be found below:
That's our world loading and collision checks all done! We're almost done now, and just need to update zone.c with our new functions. Starting with initZone:
We're now calling initWorld and loadWorld (passing over 0, to make it load data/zones/00.dat). Lastly, we've updated `draw`:
We're calling drawWorld here, doing so before drawEntities and drawBullets. And that's that! We can load our world geometry and interact with it (and give it a random hue each time the game starts). Everything is progressing very nicely, and is extremely easy to work with. But there's something missing, something very important that was the whole purpose of this tutorial set ... Isn't this meant to be a two-player game? And if so, where is the second player? Well, in our next part we'll be introducing the second player, who can be controlled with their own scheme (either keyboard or joystick). This is where things will start to get even more exciting! 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 | |
Desktop site |