├── README.md ├── img ├── 0_0_dot_product.jpg ├── 0_1_cross_product.jpg ├── 1_1_rectangle_vs_triangle_1.jpg ├── 1_2_circle_vs_triangle_1.jpg ├── 1_2_circle_vs_triangle_2.jpg ├── 1_2_circle_vs_triangle_3.jpg ├── 1_2_circle_vs_triangle_4.jpg ├── 2_1_incrementally_smaller_steps.jpg ├── 3_1_circle_vs_triangle_normal_1.jpg ├── 3_1_circle_vs_triangle_normal_2.jpg ├── 3_1_circle_vs_triangle_normal_3.jpg ├── 3_2_rectangle_vs_triangle_normal_1.jpg ├── 3_2_rectangle_vs_triangle_normal_2.jpg ├── 3_2_rectangle_vs_triangle_normal_3.jpg ├── 4_0_slide_bounce.jpg ├── 4_0_smoothness_1.jpg ├── 4_0_smoothness_2.jpg ├── 4_1_sticking_to_the_ground_1.jpg ├── demo.gif ├── no_sticking.gif └── sticking.gif └── src ├── build.bat └── main.cpp /README.md: -------------------------------------------------------------------------------- 1 | # How to program a simple vectorial 2D platformer 2 | 3 | In this tutorial, I will show you how to program a vectorial 2D platform game from the ground up. The code is in straightforward, procedural C++, but all the concepts are language-agnostic. The tutorial focuses on the fundamentals of collision detection and the details that make this simple platformer engine work. 4 | 5 | The prerequisites for this tutorial are C/C++ knowledge and high school level math. The public project only targets Windows, but I made a Wasm version that I [put up on a website](https://pered.itch.io/platformer-tutorial-demo). 6 | 7 | TLDR: [See the central function in the source code](https://github.com/Pere001/2d-platformer-tutorial-2023/blob/main/src/main.cpp#L717). [Play a web version of the final demo](https://pered.itch.io/platformer-tutorial-demo). 8 | 9 | __Table of Contents__ 10 | 11 | * [Intro](#intro) 12 | * [The Problem](#the-problem) 13 | * [Overview of my Method](#overview-of-my-method) 14 | * [Part 0 - Basic Math](#part-0---basic-math) 15 | * [Trigonometric Functions](#trigonometric-functions) 16 | * [Dot and Cross Products](#dot-and-cross-products) 17 | * [Part 1 - Collision Functions](#part-1---collision-functions) 18 | * [Computing Normals](#computing-normals) 19 | * [Rectangle vs. Triangle](#rectangle-vs-triangle) 20 | * [Circle vs. Triangle](#circle-vs-triangle) 21 | * [Part 2 - Move in Incrementally Smaller Steps](#part-2---move-in-incrementally-smaller-steps) 22 | * [Part 3 - Collision Normals](#part-3---collision-normals) 23 | * [Colision Normal: Circle vs. Triangle](#collision-normal-circle-vs-triangle) 24 | * [Collision Normal: Rectangle vs. Triangle](#collision-normal-rectangle-vs-triangle) 25 | * [Part 4 - Using the Normal](#part-4---using-the-normal) 26 | * [Sticking to the Ground](#sticking-to-the-ground) 27 | * [Part 5 - Optimization](#part-5---optimization) 28 | * [Closing Words](#closing-words) 29 | * [Further Reading](#further-reading) 30 | 31 | 32 | # Intro 33 | 34 | ![Demo](/img/demo.gif) 35 | 36 | ## The Problem 37 | 38 | Every platformer game has to handle movement and collisions. Therefore, we must design a system that can query the physical situation of an entity (e.g. grounded state, raycasts, etc.) and apply movement in a way that conforms predictably to the terrain. 39 | 40 | This can be a daunting task because you need to solve many open problems in a highly coordinated manner. 41 | 42 | For example, the entity state might be useful in the process of resolving the collisions, which might in turn change the entity state and inform the gameplay code, whose needs will determine the kind of information we need to query about the surroundings of an entity, which will then determine what entity and terrain state we need to store, etc. Each part imposes constraints and demmands upon the others, so it's hard to know where to start. 43 | 44 | I can imagine three kinds of approaches to designing the core of a platformer: 45 | 46 | 1. Depend on a physics engine. A physics engine is a complex system that follows the equations of Newtonian mechanics to systematically apply realistic movement to objects that might have different properties and constraints. It's what you need for games like _Angry Birds_. If your platformer features objects with this kind of realistic physics, you could make your player character a physical object as well, and control it indirectly through the interface of the physics engine (e.g. applying forces). This is probably what they do in games like _Limbo_ and _Little Big Planet_. 47 | 48 | In this tutorial, we won't cover physics engines. We will focus on the nonrealistic types of platformers. These can still emulate some effects from real life physics (for example, all platformers have some interpretation of gravity), but they're not structured to solve complex physical interactions of many solid objects. 49 | 50 | 2. What I would call "the railroad method". If your entity is grounded, keep track of the point it's grounded on, associated with an edge of a specific wall. This requires the different edges of a continuous floor to be linked by some metadata. In this method, it's OK if the entity slightly intersects some walls while it's grounded, because its walking movement is bound to the chain of edges like on a railroad. 51 | 52 | I think this is the kind of method they use in games such as _Rayman Origins_ and _Braid_. This is probably more complex than the method we'll use and also more specialized, since it requires some coupling with the player movement mechanics and some specific formatting of the terrain data. 53 | 54 | 3. My solution to the problem is a guarantee of separation. We ensure no entity ever intersects any wall. We check for collision before attempting to move an entity to a new place. If the target position collides, we make smaller and smaller safe steps on the segment of the speed until the steps are so small that it looks like the entity is touching a wall. However, it is always slightly separated. 55 | 56 | To avoid all these iterations, we could find the tangent position directly, add a little margin to it, and move the entity there. But, for this tutorial, we'll just use the stupid iterative approach to keep things simple. With this method, there's no "terrain metadata" at all. We simply use the raw geometry and the assumption of separation to find normals that we'll use to guide the entity's movement. 57 | 58 | I'm sure there are other possible options, and games could also use a mixed approach. 59 | 60 | ## Overview of my Method 61 | 62 | Here's a summary of the method (don't worry if you don't totally get it right now). We try to move an entity by its speed. If it intersects any wall, we instead try to move it by a fraction of the speed. Through "convergent" trial and error tests, we find the highest fraction of the speed that we can add to the entity's position without intersecting any wall, and we move the entity there. 63 | 64 | Then, we use some math to find the first wall to collide and its collision normal. Finally, we use the normal to update the speed, with the possibility of applying bounce or slide. 65 | 66 | Additionally, there's a trick we do to avoid being stuck because of precision issues when moving parallel and very close to a wall's edge. When we're testing positions we will also test if "shifting" the entity by a subpixel amount perpendicular to the speed allows it to go farther, and if so move to that farthest, shifted position. 67 | 68 | This method has a downside. It's rather costly because it does many iterations of collision checks against all candidate walls. On top of that, the trick that avoids getting stuck when moving tangent to edges potentially triples the number of iterations. 69 | 70 | That said, this method is simple and modular. You can easily improve or simplify different aspects to fit your needs. You can add support for a different shape of entity or wall by supplying the respective intersection function and the code for the collision normal. This approach is also robust to very rough terrain data. Your walls can be paper-thin. They can even intersect one another. You will still get the desired behavior. 71 | 72 | With the math described in this tutorial, you can improve the movement code so it doesn't do the repeated iterations of intersection checks to solve collisions. Instead, you would directly find the farthest position that doesn't collide by calculating the time of impact and advancing only by that time. (Honestly, I'd do that if I were remaking this tutorial from scratch, now that I know better. But it's also kind of interesting to experiment with these more wacky ideas, and it does keep the tutorial simpler.) 73 | 74 | The structure of this tutorial follows the steps I'd recommend as the implementation arc: 75 | 76 | 0. [Basic Math](#part-0---basic-math): Of course, you will need your math functions, but most important is an excellent intuition of the dot and cross products. 77 | 1. [Collision Functions](#part-1---collision-functions): We will start by making the collision functions. After this, you can test them by placing a bunch of walls and dragging a shape with the mouse that changes color when it intersects a wall. 78 | 2. [Move in Incrementally Smaller Steps](#part-2---move-in-incrementally-smaller-steps): Next, we will implement the iterative testing I described, to move an entity up to the edge of a wall without intersecting it. You can test this by creating an entity that is movable by keyboard input. 79 | 3. [Collision Normals](#part-3---collision-normals): We will calculate the collision normals. You can test this by drawing a line in the direction of the last normal. 80 | 4. [Using the Normal](#part-4---using-the-normal): We will use the normal to update the player's speed, and we will finally have a moving player. 81 | 5. [Optimization](#part-5---optimization): We will finish by talking about optimizations and improvements you can make to this project. 82 | 83 | For entities, I chose to implement a circle and a rectangle. For walls, I chose triangles. With this approach, it is easy to add more entity shapes, and it is trivial to change the walls from triangles to general convex polygons. You just have to change the hard-coded `3` in a bunch of `for` loops. 84 | 85 | Without further ado, let's get on with the tutorial. 86 | 87 | 88 | # Part 0 - Basic Math 89 | 90 | Before we start, let's clarify the mathematical functions and conventions we will use. Directions will be implicitly in counterclockwise radians starting from the right. We will define a 2D vector struct (`v2`) and overload all the operators corresponding to its mathematical operations (I assume you understand basic vector math). 91 | 92 | Our coordinate system will be positive y up. We will be using `float`s for our positions, but note that you could also use `int`s with very little change to the code, as I do in my own game. 93 | 94 | ```c++ 95 | struct v2{ 96 | float x; 97 | float y; 98 | }; 99 | ``` 100 | 101 | The rest of this section is mainly intended for beginners. 102 | 103 | ## Trigonometric Functions 104 | 105 | Here are the basic trigonometric functions we will use (in pseudocode). 106 | 107 | - `Length(v) = SquareRoot(v.x*v.x + v.y*v.y)` 108 | - `LengthSqr(v) = v.x*v.x + v.y*v.y` It’s common to use the length squared as an optimization. When comparing distances with <, >, <= or >= the result is the same as comparing those distances squared, so sometimes we can save ourselves the square root. 109 | - `AngleOf(v) = Atan2(v.y, v.x)` (Since atan2 is undefined for zero vectors, we will also check if v.x and v.y are 0 and return 0 in that case.) 110 | - `V2FromLengthDir(length, dir) = {Cos(dir)*length, Sin(dir)*length}` Constructs a `v2` from a length and an angle. 111 | - `Normalize(v) = V2FromLengthDir(1, AngleOf(v))` I like this option better than the common alternative `v/Length(v)` because it allows the input to be a zero vector (since it avoids the division by zero). This will reduce the amount of edge cases needed in our code. 112 | - `Rotate90Degrees(v) = {-v.y, v.x}` 113 | - `RotateMinus90Degrees(v) = {v.y, -v.x}` 114 | 115 | ## Dot and Cross Products 116 | 117 | - `Dot(a, b) = a.x*b.x + a.y*b.y` 118 | - `Cross(a, b) = a.x*b.y - a.y*b.x` 119 | 120 | The dot product between 2 vectors, if one of them is unit-length (normalized), is a projection of the non-unit-length vector into the direction of the unit-length one. That is, what (signed) distance along the axis of the unit-length vector the other vector travels. 121 | 122 | ![Dot product](/img/0_0_dot_product.jpg) 123 | 124 | The cross product helps us compare the angle between two vectors. If the two vectors are parallel, the cross product will be 0. If both vectors are normalized, and they are perpendicular to each other, the cross product will be 1 (if the angle from a to b is 90°) or -1 (if the angle is 270°). 125 | 126 | Most importantly, if the second vector is less than pi radians (180°) counterclockwise from the first, the cross product will be positive, and if it’s over pi radians it wil be negative. 127 | 128 | ![Cross product](/img/0_1_cross_product.jpg) 129 | 130 | [Check out this Desmos graphic to see this interactively](https://www.desmos.com/calculator/foxjolfgkq). I admit I often come to play with it when I need to refresh my understanding of the two products. It is crucial that you have a basic intuition about the dot and cross products if you are to follow the rest of this tutorial. 131 | 132 | I must note that in mathematics the "cross product" is technically only defined for 3D vectors. In 3D it’s a vector perpendicular to vectors a and b, and its formula is: `a × b = (a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x)`. 133 | 134 | As you can see, the Z component matches the definition of cross product that we use. That’s because in 2D it’s common to use the term “cross product”, as we do, to refer to the Z component of the actual cross product of your 2D vectors extended to 3D. In some code bases they also call this "the determinant”, which is an operation you can do on a matrix that also happens to match our 2D cross product in the case of 2x2 matrices. So yeah, we'll call it the cross product as many people do, but be aware that might not be perfect math language. 135 | 136 | 137 | # Part 1 - Collision Functions 138 | 139 | ## Computing Normals 140 | 141 | We always store the vertices of our walls in the same directional order, in our case counterclockwise (CCW), with the normals pointing outwards. We will precompute the normals and store them with the wall data to avoid computing them every time we need them (which will be often). 142 | 143 | ```c++ 144 | struct wall{ 145 | v2 p[3]; // Vertices. 146 | v2 normals[3]; // normals[0] is the normal of the edge from p[0] to p[1], and so forth. 147 | }; 148 | ``` 149 | 150 | To calculate the normal of an edge we just do: 151 | 152 | ```c++ 153 | v2 edgeDir = Normalize(p1 - p0); 154 | v2 n = {edgeDir.y, -edgeDir.x}; // Rotates edgeDir -90 degrees CCW 155 | ``` 156 | 157 | To ensure three points are CCW we can simply check the cross product of the first and second edges: 158 | 159 | ```c++ 160 | bool PointsAreCW(v2 p0, v2 p1, v2 p2){ 161 | return (Cross(p1 - p0, p2 - p1) < 0); 162 | } 163 | ``` 164 | 165 | Just for completion, if you had a general polygon instead of a triangle, you could use this slightly more complex method. Sum the angle of the direction of each edge relative to the direction of the previous edge. If the points make a full circle CCW, the sum will be `2*PI` and if they make a full circle CW the sum will be `-2*PI`. 166 | 167 | ```c++ 168 | bool PointsAreCW(v2 *points, int num){ 169 | float angleSum = 0.f; 170 | v2 prevPoint = points[num - 1]; 171 | float prevAngle = AngleOf(prevPoint - points[num - 2]); 172 | for(int i = 0; i < num; i++){ 173 | float newAngle = AngleOf(points[i] - prevPoint); 174 | angleSum += AngleDifference(newAngle, prevAngle); 175 | prevAngle = newAngle; 176 | prevPoint = points[i]; 177 | } 178 | return (angleSum < 0); 179 | } 180 | ``` 181 | 182 | You could flip the order of the points of a wall if this returned true. Or you could just load the walls from valid data. 183 | 184 | ## Rectangle vs Triangle 185 | 186 | To check whether a rectangle and a triangle intersect, we will use the Separating Axis Theorem (SAT), which is probably the most common method for finding collisions between convex polygons. If we can find one line that separates all of the rectangle's points from all of the triangle's, we will know they do not intersect. 187 | 188 | The SAT says that if two convex polygons are separated, one of the lines that separate them will be in the direction of one of the edges of our polygons. This means we only have to check as many directions as edges there are in both polygons. 189 | 190 | To test for separation in the direction of a given edge, we can project all the points of the other shape, relative to a point on the edge, onto the normal of that edge. If all the projections are greater than zero, it means a line in the direction of the edge can separate the walls. We will do this for every edge. If at least one edge finds separation, the shapes do not intersect. If no edges find separation, the shapes do intersect. 191 | 192 | I don’t count tangential shapes as colliding, but you could. In our case I don’t think it matters. 193 | 194 | ```c++ 195 | bool RectangleWallCollision(v2 rMin, v2 rMax, wall *w){ 196 | // Check if rectangle and triangle are separated by an axis-aligned line. 197 | // (The bitwise '&' operator here does the same as the boolean '&&', but it's faster because it avoids some branches) 198 | if ( (w->p[0].x >= rMax.x & w->p[1].x >= rMax.x & w->p[2].x >= rMax.x) 199 | || (w->p[0].y >= rMax.y & w->p[1].y >= rMax.y & w->p[2].y >= rMax.y) 200 | || (w->p[0].x <= rMin.x & w->p[1].x <= rMin.x & w->p[2].x <= rMin.x) 201 | || (w->p[0].y <= rMin.y & w->p[1].y <= rMin.y & w->p[2].y <= rMin.y)) 202 | return false; 203 | 204 | // Check if rectangle and triangle are separated by one of triangle's edges. 205 | for(int i = 0; i < 3; i++){ 206 | v2 p = w->p[i]; 207 | v2 n = w->normals[i]; 208 | 209 | float rectProjected[4]; 210 | rectProjected[0] = Dot(rMin - p, n); 211 | rectProjected[1] = Dot(V2(rMin.x, rMax.y) - p, n); 212 | rectProjected[2] = Dot(rMax - p, n); 213 | rectProjected[3] = Dot(V2(rMax.x, rMin.y) - p, n); 214 | 215 | float rectProjectedMin = Min(Min(Min(rectProjected[0], rectProjected[1]), rectProjected[2]), rectProjected[3]); 216 | 217 | if (rectProjectedMin >= 0){ 218 | return false; 219 | } 220 | } 221 | return true; // No separating axis found 222 | } 223 | ``` 224 | 225 | If you got lost, I suggest thinking by making some drawings, or just check out my diagram: 226 | 227 | ![Rect vs tri](/img/1_1_rectangle_vs_triangle_1.jpg) 228 | 229 | This illustrates the check of an edge that finds separation. We consider all points relative to a vertex of the edge we are checking. All the projections of the triangle vertices onto the normal of that edge will be <= 0, because the normal points outwards, and no point on the triangle can be in front of the normal. If the minimum of the projections of the rectangle is >= 0, it means there is separation. 230 | 231 | ## Circle vs Triangle 232 | 233 | This one is fun. First, we will project the center of the circle onto the normals of the triangle’s edges. This is similar to what we did with the rectangle, and it gives us the signed distance of the center to the line of each of the edges. If that distance is greater than r in all of them, there is no collision. 234 | 235 | But this is not enough. Were we to rely solely on this test, we would see false positives near the triangle's vertices. In essence, we would be checking a collision between the circle's center and a larger triangle with each edge at distance r from our original triangle. This is not equivalent to the collision between our circle and the original triangle. 236 | 237 | ![Circle vs tri](/img/1_2_circle_vs_triangle_1.jpg) 238 | 239 | A collision of the center point against the larger triangle with rounded corners IS equivalent. 240 | 241 | ![Circle vs tri](/img/1_2_circle_vs_triangle_2.jpg) 242 | 243 | So we’ll break the collision down into 3 shapes: 244 | 245 | ![Circle vs tri](/img/1_2_circle_vs_triangle_3.jpg) 246 | 247 | 1. If the center is inside the (original) triangle, that’s a collision. 248 | 2. If the center is inside any of the rectangles created by extruding the triangle’s edges by r (circle’s radius), that’s a collision. 249 | 3. If the center is inside a circle of radius r with center on any of the triangle vertices, that’s a collision. 250 | 251 | Otherwise there’s no collision. 252 | 253 | ```c++ 254 | bool CircleWallCollision(v2 c, float r, wall *w){ 255 | // This loop checks the collision of the center with the triangle and the edge rectangles. 256 | bool centerOnTriangle = true; 257 | for(int i = 0; i < 3; i++){ 258 | float proj = Dot(w->normals[i], c - w->p[i]); // Center projected onto normal 259 | 260 | if (proj > r) // (Optional early-out: circle is too far outwards to collide) 261 | return false; 262 | 263 | if (proj < 0) // The center is inward from this edge: it might be inside the triangle or not, 264 | continue; // and inside other edge rects or not, but this will be found by the other iterations. 265 | 266 | // Check if center is inside the edge rects. 267 | v2 edgeDir = Rotate90Degrees(w->normals[i]); 268 | float edgeProj0 = Dot(edgeDir, c - w->p[i]); // Center projected onto edge direction, relative to current vertex. 269 | float edgeProj1 = Dot(edgeDir, c - w->p[(i + 1) % 3]); // Center projected onto edge direction, relative to next vertex. 270 | if (edgeProj0 > 0 && edgeProj1 < 0){ 271 | // The projected center falls between the two vertices of the edge: the center is inside the edge rect. 272 | return true; 273 | } 274 | 275 | centerOnTriangle = false; 276 | } 277 | 278 | if (!centerOnTriangle){ 279 | // Center wasn't inside the triangle or the edge rects 280 | // Check if a vertex is inside the circle. 281 | for(int i = 0; i < 3; i++){ 282 | float distanceSqr = LengthSqr(w->p[i] - c); 283 | if (distanceSqr < r*r) 284 | return true; 285 | } 286 | } 287 | return centerOnTriangle; 288 | } 289 | ``` 290 | The first loop does this, with respect to the position of the center: 291 | 292 | ![Circle vs tri](/img/1_2_circle_vs_triangle_4.jpg) 293 | 294 | The second loop just returns true if the center is inside those vertex circles (which is the same as checking if the vertices are inside the original circle). 295 | 296 | 297 | # Part 2 - Move in Incrementally Smaller Steps 298 | 299 | Now that we have the collision functions, let’s apply some velocity. We attempt to move the entity to the new position (old position + speed), and if we don’t collide, we’re done. If there is a collision at the new position, we'll check multiple points on the segment between the old and new positions. We will then keep the farthest point that doesn't collide with any wall. If all of them collide, we will keep the old position. 300 | 301 | How do we decide which intermediate points to check? We could move in equally small steps toward the new position until we collide, but there is a more efficient way. We will cut the step size in half at every iteration. This way, for the same level of precision, we will only have to do O(log(n)) checks instead of O(n) (see [big O notation](https://en.wikipedia.org/wiki/Big_O_notation)). 302 | 303 | ![Incrementally smaller steps](/img/2_1_incrementally_smaller_steps.jpg) 304 | 305 | First, the step size is the length of speed. We advance toward pos + speed by the step size. If we collide, we half the step size and move backwards. If that collides, we half the step and move backwards again. If we don’t collide, we half the step and move forwards. Etc. We’ll stop when we reach an arbitrary limit on the step size or on the number of iterations. 0.05 pixels for the minimum step length and 15 maximum steps seems reasonable enough. You can tune these for different kinds of entities to save some performance. 306 | 307 | Here is the first version of the `MoveCircle()` function: 308 | 309 | ```c++ 310 | v2 MoveCircle(v2 pos, float r, v2 speed){ 311 | v2 newPos = pos; 312 | float stepSize = 1.0f; 313 | float speedFactor = 1.0f; 314 | for(int steps = 0; steps < 15; steps++){ 315 | v2 p = pos + speedFactor*speed; 316 | stepSize *= 0.5f; 317 | if (CircleWallCollision(p, r)){ // (This calls CircleWallCollision on all existing walls) 318 | speedFactor -= stepSize; 319 | }else{ 320 | speedFactor += stepSize; 321 | newPos = p; 322 | if (steps == 0) 323 | break; // No collisions found on the first step. 324 | } 325 | if (stepSize*Length(speed) <= .05f) 326 | break; // Limit step size. 327 | } 328 | return newPos; 329 | } 330 | ``` 331 | 332 | I kept this code simple because we will expand upon it later. 333 | - After moving the circle we will want to compute the collision normals if it collided. 334 | - We will add small perpendicular shifts to facilitate moving tangent to edges. This solves the problem of entities getting stuck when sliding along edges. Because of precision issues, if you move a shape along an edge, it will find collision at some positions and not others. 335 | 336 | Our solution shifts the entity by a subpixel amount perpendicular to its speed when a collision is found. If the shifted position doesn't collide, we will move there and count that as a non-collision (so, the next step will be forward rather than backward). You can see the implementation of this in [the final version of this function](https://github.com/Pere001/2d-platformer-tutorial-2023/blob/main/src/main.cpp#L574). 337 | - There will be an equivalent function for the rectangle, `MoveRectangle()`. 338 | - To avoid tunnelling (fast entities going through thin walls) we could have a maximum step length by adding an outer loop. 339 | 340 | 341 | # Part 3 - Collision Normals 342 | 343 | If we can't move the entity by the whole speed, we will want to find the collision normal of the first wall the entity collides with. Sometimes this is the normal of one of the wall’s edges, and sometimes it will be something else (imagine the entity colliding against the protruding edge of a wall, like a basket ball hitting the corner of a table). 344 | 345 | We need the collision normal because it helps us calculate the bouncing or sliding reaction we will see in the next section, along with other possible mechanics. 346 | 347 | As a recap, these are the data we'll use to calculate the collision normal after having moved the entity and detected a collision: 348 | - Entity speed. 349 | - New entity position, or the position of the last non-colliding step. It shouldn't collide with any wall. 350 | - Last colliding position, or the position of the entity at the last colliding step. This should be farther than the new position along the direction of the speed, and infinitesimally close to it. 351 | - The walls. We will use the ones that intersect the entity in its last colliding position. 352 | 353 | ## Collision Normal: Circle vs Triangle 354 | 355 | The following is a simple method to get the circle's collision normal. Find the closest point on the closest edge of the closest wall (to our new position). The collision normal will be the direction from that point to the center of the circle. 356 | 357 | ![Circle vs triangle normal](/img/3_1_circle_vs_triangle_normal_1.jpg) 358 | 359 | The blue arrow is the circle’s speed. The black segment is the closest edge. The black dot is the closest point on that edge. The black arrow is the resulting collision normal. 360 | 361 | This distance-based method works very well because the circle has previously been moved very close to the wall it will hit. However, it's still possible that the edge that is closest is not the one it would hit first. We will solve this by taking the velocity's direction into account. 362 | 363 | Instead of comparing the edges by their distance to the circle, we will compare them by the time it takes the circle to hit them if it keeps moving by its speed. 364 | To compute this time, we will simply divide the distance by the dot product of the velocity and the direction from the circle to the point. This will prioritize edges that the circle is moving towards. The units also make sense, because if you divide distance by speed, you get time. 365 | 366 | But it's probably better understood geometrically: 367 | 368 | ![Circle vs triangle normal](/img/3_1_circle_vs_triangle_normal_2.jpg) 369 | 370 | Now, if we take the lowest time, we correctly find the wall that collides first. The lines representing "time" are actually showing time\*speed, or the distance travelled until collision. 371 | 372 | Here's a more detailed proof: 373 | 374 | ![Circle vs triangle normal](/img/3_1_circle_vs_triangle_normal_3.jpg) 375 | 376 | As you can see, the dot product `Dot(speed, -n)` projects the speed onto `-n` to give us the amount that `speed` is moving towards our edge. That is, the distance the circle will move along the direction of the negative normal each second. Or the distance our circle will move towards our edge each second. 377 | 378 | We want to know how much time it will take to travel the distance `d` along the direction of the normal. Since both are along the same direction, we can divide the distance we want to travel by the projected speed, and we will get the time it takes. It's not that complicated when you break it down. 379 | 380 | Note that `n` is not really taken from the edge's normal. To find it, you compute the direction from the closest point on the edge, to the center of the circle. This allows us to handle the cases where the circle collides a pointy vertex, using exactly the same code as when it collides the surface of an edge. 381 | 382 | Here’s the code. Imagine it’s inside the previous `MoveCircle()` function, after the loop. `walls` is an array of all existing walls, sized by `numWalls`. Imagine we then return the `bestNormal` along with the new position and whether it collided. You can also see [the whole function on GitHub](https://github.com/Pere001/2d-platformer-tutorial-2023/blob/main/src/main.cpp#L574). 383 | ```c++ 384 | v2 bestNormal = -Normalize(speed); // Default value in case we don't find any. 385 | float bestTime = MAX_FLOAT; // Lowest time until collision, to find the edge that would collide first. 386 | for(int wallIndex = 0; wallIndex < numWalls; wallIndex++){ 387 | wall *w = &walls[wallIndex]; 388 | if (!CircleWallCollision(lastCollisionPos, r, w)) 389 | continue; 390 | 391 | for(int i = 0; i < 3; i++){ 392 | if (Dot(w->normals[i], speed) >= 0) 393 | continue; // Ignore edges facing away 394 | 395 | v2 t0 = w->p[i]; 396 | v2 t1 = w->p[(i + 1) % 3]; 397 | 398 | // Let's find the closest point on the edge. 399 | v2 edgeDir = Rotate90Degrees(w->normals[i]); 400 | float cProjected = Dot(edgeDir, newPos - t0); // Center projected onto the edge. 401 | float t1Projected = Dot(edgeDir, t1 - t0); // Next vertex projected onto the edge. 402 | v2 collisionPoint = t0 + edgeDir*Clamp(cProjected, 0, t1Projected); // Closest point on the edge 403 | 404 | v2 n = Normalize(newPos - collisionPoint); // Collision normal of this edge. 405 | float distance = Dot(newPos - collisionPoint, n) - r; // (We use Dot() to avoid the squareroot of Length()). 406 | float nDotSpeed = Dot(n, speed); 407 | if (!nDotSpeed) 408 | continue; // Avoid division by 0. 409 | float time = distance/-nDotSpeed; // Time to hit wall 410 | if (time < bestTime && Dot(n, speed) <= 0){ // Ignore normals that face away from speed (technically possible with shifts) 411 | bestTime = time; 412 | bestNormal = n; 413 | } 414 | } 415 | } 416 | ``` 417 | 418 | ## Collision Normal: Rectangle vs Triangle 419 | 420 | For the rectangle's collision normal, we will borrow some techniques we used with the circle. We will iterate each wall that collides the rectangle in its last colliding position to find the wall that collides first. 421 | 422 | For each wall, we will iterate every edge of that wall and every edge of the rectangle, treating them the same. For every edge, we will project onto its normal all the points of the other shape. Of these projected points, we'll take the lowest. This will be the "projected distance" of this edge in respect to the other shape. From this "projected distance", we will calculate the "time until impact" of the edge by dividing by the dot product between the speed and the normal (the same way we did with the circle). As I have said, we will do this with all edges of both shapes. The edge with the highest "time until impact" will provide the "time until impact" of a given wall and its collision normal. Why the highest? The best way to understand this is with some visual examples: 423 | 424 | ![Circle vs triangle normal](/img/3_2_rectangle_vs_triangle_normal_1.jpg) 425 | 426 | The grey lines show the "projected distance" of edges B and C. The black lines represent the "time until collision" (a, b, and c) of edges A, B, and C. It is clear to the eye that the correct time until impact is a, which is the highest. It's also clear that the collision normal will be the normal of edge A. 427 | 428 | So, taking the highest "time until impact", we will find the "time until impact" for a wall and its collision normal. We will do this for all candidate walls and the winner will be the wall with the lowest time. The collision normal of this wall will be the final collision normal. If the collision normal was taken from an edge of the rectangle, we will negate it, because we want it from the point of view of the rectangle. 429 | 430 | If we stop here, this will give us the expected normal in most situations. However, it might cause problems with geometry such as the following: 431 | 432 | ![Circle vs triangle normal](/img/3_2_rectangle_vs_triangle_normal_3.jpg) 433 | 434 | In this example, edge B is clearly in front of edge C, covering it fully, so we would expect the normal of B to win over the normal of C. The collision normal of A is the same as B's, so their respective times, a and b, are equal. Therefor, the upper left wall will select either A or B as the winner edge, but either way the collision normal will be the left direction. 435 | 436 | The lower right wall, however, will pick either A or C as the winner edge, since c also happens to be the same as a and b. Its collision normal can be either the left direction or the normal of C. Between the two walls, any of them can win, because we know their "time until impact" will be the same. So, if the lower right wall wins, and it had selected C as its colliding edge, the normal of C will mistakenly be chosen as the final collision normal. This could cause unexpected behaviour, like our player bouncing upwards when it runs into this wall. 437 | 438 | To solve this, we must understand which situations can cause this problem to happen. It wouldn't happen if the rectangle was slightly higher up, because the line of C would be farther, c would be greater, and we would correctly collide with edge B. It also wouldn't happen if the rectangle was a bit lower down, because the line of C would be closer, c would be lower than a, and the lower right wall would select A as the colliding edge. 439 | 440 | It can only happen if we hit such a point with perfect alignment. Most of the time this happens when there is a floor at the level of the point that leads the rectangle's lower corner into it. So, our solution will need to tackle this case. 441 | 442 | When two walls have the same "time until impact", we will break the tie by choosing the wall with a negated normal more aligned with the velocity of the rectangle. 443 | 444 | - When the rectangle is walking horizontally to the right, like the example above, the edge with the normal that faces leftmost will win the tie. This will choose the correct edge, B, and it also handles the cases where you're moving uphill. 445 | 446 | - In the cases where the rectangle is moving downhill, C wins this preference. However, the rectangle won't get to hit that point of conflict if the floor is connected to it, because it will be stopped by its own hitbox hitting B on a higher position. 447 | 448 | Long story short, this simple way to break the tie (just a dot product) solves the problem in the disproportionately most likely situation for it to occur. The unaddressed cases very improbable. 449 | 450 | For the glitch to arise now, a rectangle would have to jump into a wall that contains such a conflictive vertex in the perfect trajectory that properly aligned a corner of the rectangle with the conflictive vertex, and then the wrong edge would have to win its 1/4 chance of being selected. You could do something more complex to address these implausible cases. In practice, this will be more than enough for most games. 451 | 452 | The equality check for whether there's a tie will be done with an epsilon (i.e. check that the two values are __almost__ the same), since the two values will be computed by different routes, so even if geometrically they should be the same, they might be slightly different. 453 | 454 | That's all we need to do to find the correct normal. Here's the code (this goes at the end of [`MoveRectangle()`](https://github.com/Pere001/2d-platformer-tutorial-2023/blob/main/src/main.cpp#L717)): 455 | 456 | ```c++ 457 | v2 newR0 = newPos - halfDim; 458 | v2 newR1 = newPos + halfDim; 459 | v2 newRectPoints[4] = {newR0, V2(newR0.x, newR1.y), newR1, V2(newR1.x, newR0.y)}; 460 | 461 | // We find the two edges (one horizontal and one vertical) that face the direction of the speed, 462 | // and these arrays hold their normal and a point of the edge. 463 | v2 rectEdgePoints[2] = { {(speed.x < 0 ? newR0.x : newR1.x), newPos.y}, 464 | {newPos.x, (speed.y < 0 ? newR0.y : newR1.y)} }; 465 | v2 rectEdgeNormals[2] = { {(speed.x < 0 ? -1.f : 1.f), 0}, 466 | {0, (speed.y < 0 ? -1.f : 1.f)}}; 467 | 468 | v2 bestNormal = -Normalize(speed); // Final collision normal (Default value in case we don't find any) 469 | float bestTime = MAX_FLOAT; // Lowest time until collision, to find the edge that would collide first. 470 | 471 | for(int wallIndex = 0; wallIndex < numWalls; wallIndex++){ 472 | wall *w = &walls[wallIndex]; 473 | if (!RectangleWallCollision(lastCollisionPos - halfDim, lastCollisionPos + halfDim, w)) 474 | continue; 475 | 476 | v2 localNormal = bestNormal; // Best normal of the current wall. (default value "just in case") 477 | float localBestTime = MIN_FLOAT; // Highest time to hit an edge 478 | 479 | // Find the edge with the highest time until impact 480 | 481 | // Project rectangle onto triangle normals 482 | for(int i = 0; i < 3; i++){ 483 | v2 n = w->normals[i]; 484 | v2 p = w->p[i]; 485 | float nDotSpeed = Dot(n, speed); 486 | if (nDotSpeed < 0){ // Ignore edges facing away. 487 | float d = Min(Min(Min(Dot(newRectPoints[0] - p, n), // Projected distance to rectangle 488 | Dot(newRectPoints[1] - p, n)), 489 | Dot(newRectPoints[2] - p, n)), 490 | Dot(newRectPoints[3] - p, n)); 491 | float t = d/-nDotSpeed; // Time to hit edge 492 | if (t > localBestTime){ 493 | localBestTime = t; 494 | localNormal = n; 495 | } 496 | } 497 | } 498 | 499 | // Project triangle onto rectangle normals (we only need to check the two edges facing the direction of the speed) 500 | for(int i = 0; i < 2; i++){ 501 | v2 n = rectEdgeNormals[i]; 502 | float nDotSpeed = Dot(n, speed); 503 | if (nDotSpeed){ // Gota do this to avoid potential division by 0. 504 | float d = Min(Min(Dot(w->p[0] - rectEdgePoints[i], n), // Projected distance to triangle 505 | Dot(w->p[1] - rectEdgePoints[i], n)), 506 | Dot(w->p[2] - rectEdgePoints[i], n)); 507 | float t = d/nDotSpeed; // Time to hit edge 508 | if (t > localBestTime){ 509 | localBestTime = t; 510 | localNormal = -n; 511 | } 512 | } 513 | } 514 | 515 | // Compare the best normal in this wall with the best of previous walls. 516 | float epsilon = 0.000001f; 517 | if (localBestTime < bestTime || 518 | (localBestTime - bestTime < epsilon && Dot(localNormal, speed) < Dot(bestNormal, speed))) // If there's a tie we'll take the normal most aligned with the speed. 519 | { 520 | bestNormal = localNormal; 521 | bestTime = localBestTime; 522 | } 523 | } 524 | 525 | ``` 526 | 527 | 528 | # Part 4 - Using the Normal 529 | 530 | Now that the core of the engine is done, we can use the collision normal to update the movement of the player and other entities. There are many ways we could use this information to customize the movement: limits on walkable steepness, sticking to the ground or not, bouncing or not, the way speed is accumulated when going down curved ramps, change of acceleration depending on slope, etc. 531 | 532 | We also have all the degrees of freedom of platformers in general: acceleration, speed, maximum velocity, types of jump archs, jump cancelling, "coyote jump", stepping over small obstacles, friction, etc. 533 | 534 | I will show how to implement smooth, sliding movement; sticking to the ground; and bouncing. I'll keep the code simple, but note that you could add a bunch of quirks to affect the feel of the movement. I'll be showing the update code for the rectangle, but I've done a similar thing for the circle as well. 535 | 536 | First, we get the grounded state and ground normal by calling `MoveRectangle()` as if we were trying to move 1 pixel downwards. 537 | 538 | For reference, here's the prototype of that function: `v2 MoveRectangle(v2 pos, v2 halfDim, v2 speed, bool *outCollided, v2 *outCollisionNormal)`. 539 | 540 | We discard the resulting position so the rectangle doesn't actually move yet. We will use the ground normal later on. It helps us determine the direction of the acceleration caused by horizontal input. For this reason, if we're not grounded, we will set the ground normal to point up, so that when we are in the air the horizontal input just causes purely horizontal acceleration. The grounded state will be used to determine if the player can jump or not. 541 | 542 | ```c++ 543 | bool grounded; 544 | v2 groundNormal; 545 | // We just call this to get the grounded state 546 | MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2(0, -1.f), &grounded, &groundNormal); 547 | if (!grounded){ 548 | groundNormal = V2(0, 1.f); 549 | } 550 | ``` 551 | 552 | Next, we query the input to accelerate the entity "horizontally". It's not really horizontal because we apply the acceleration in the direction of the ground so that the entity will "stick" to the ground. 553 | 554 | ```c++ 555 | // Read input and set speed. 556 | // Move in direction of ground to avoid "stepping" down slopes. 557 | float ax = 0; // "Horizontal" acceleration 558 | v2 groundDir = RotateMinus90Degrees(groundNormal); 559 | if (globalInput.keyboard.arrowRight.isDown){ 560 | ax = .7f; // Accelerate right 561 | }else if (globalInput.keyboard.arrowLeft.isDown){ 562 | ax = -.7f; // Accelerate left 563 | }else{ 564 | ax = Dot(gameState.rectSpeed, groundDir)*-.1f; // Decelerate. 565 | } 566 | gameState.rectSpeed += ax*groundDir; 567 | ``` 568 | 569 | Then, we apply vertical acceleration. We will only apply gravity if the player is not grounded. If the player is grounded, we will let it jump. If it doesn't jump and it is grounded on a steep enough incline, we will also add some gravity, so it will naturally slide down the slope. This steepness threshold is arbritrary, and you can tweak it to fit your game (as all the other magic numbers you see around here). Finally, we limit the speed length so that the player doesn't accelerate too much. 570 | 571 | ```c++ 572 | float minSpeedY = -30.f; 573 | if (grounded){ 574 | if (globalInput.keyboard.arrowUp.isDown){ 575 | gameState.rectSpeed.y = 10.f; // Jump 576 | }else{ 577 | float diff = Abs(AngleDifference(PI/2, AngleOf(groundNormal))); 578 | if (diff > PI/4 && gameState.rectSpeed.y > minSpeedY) 579 | gameState.rectSpeed.y -= .3f; 580 | } 581 | }else if (gameState.rectSpeed.y > minSpeedY){ 582 | gameState.rectSpeed.y -= .4f; // Gravity 583 | } 584 | 585 | // Limit speed 586 | float maxSpeed = 12.f; 587 | if (Length(gameState.rectSpeed) > maxSpeed) 588 | gameState.rectSpeed *= maxSpeed/Length(gameState.rectSpeed); 589 | ``` 590 | 591 | Now that we have updated the speed, we will call `MoveRectangle()` to update the position of the entity. If there was a collision, we will use the collision normal to reflect the speed, causing a bounce. We can also project the speed onto the collision edge, causing a slide. The following diagram shows how slide or bounce is achieved by updating the speed (s) based on the collision normal (n). 592 | 593 | ![Slide and bounce](/img/4_0_slide_bounce.jpg) 594 | 595 | The decision of whether to slide or bounce is made from a handful of arbitrary heuristics tuned to my taste: namely, the projection of the speed onto the collision normal, and the direction of the collision normal (i.e. steepness). 596 | 597 | This is enough to make the entity react properly to the terrain, but it will make the entity's movement stuttery every time there is a collision, because in these cases the entity moves less than the speed. The following diagram depicts the position of an entity in consecutive frames as it walks on some irregular ground: 598 | 599 | ![Smoothness](/img/4_0_smoothness_1.jpg) 600 | 601 | You can see that where the slope begins, the entity doesn't advance by the whole speed. To solve this, we will do multiple iterations. We will keep track of how much of the original speed we have advanced. If we slid and didn't advance enough, we will do another iteration, now advancing in the new direction, until we've consumed all the speed, or an iteration limit is reached: 602 | 603 | ![Smoothness](/img/4_0_smoothness_2.jpg) 604 | 605 | So that's what this `for` loop does: 606 | 607 | ```c++ 608 | float speedLength = Length(gameState.rectSpeed); 609 | float toMove = speedLength; 610 | for(int i = 0; i < 6; i++){ // Artificial iteration limit 611 | bool collided; 612 | v2 collisionNormal; 613 | v2 oldPos = gameState.rectPos; 614 | gameState.rectPos = MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2FromLengthDir(toMove, AngleOf(gameState.rectSpeed)), &collided, &collisionNormal); 615 | toMove -= Length(gameState.rectPos - oldPos); 616 | 617 | if (!collided) 618 | break; 619 | 620 | v2 effectiveSpeed = V2FromLengthDir(speedLength, AngleOf(gameState.rectSpeed)); 621 | if (Dot(effectiveSpeed, collisionNormal) < -9.f && Dot(collisionNormal, V2(0, 1)) < .4f) { 622 | // Hit a wall hard: bounce! 623 | gameState.rectSpeed = (gameState.rectSpeed - 2*Dot(gameState.rectSpeed, collisionNormal)*collisionNormal)*.4f; 624 | }else{ 625 | v2 prevSpeed = gameState.rectSpeed; 626 | gameState.rectSpeed = gameState.rectSpeed - Dot(gameState.rectSpeed, collisionNormal)*collisionNormal; 627 | 628 | if (Abs(collisionNormal.y) > .8f) // The edge is very horizontal 629 | if (Abs(Dot(Normalize(prevSpeed), collisionNormal)) > .5f) // and we're going quite perpendicular to the edge 630 | break; // Don't slide. 631 | 632 | if (Abs(Dot(Normalize(prevSpeed), collisionNormal)) > .9f) // Going very perpendicular to the edge 633 | break; // Don't slide. 634 | } 635 | if (toMove < .1f) 636 | break; 637 | } 638 | ``` 639 | 640 | And there we have it, a working platformer: 641 | 642 | ![No sticking](/img/no_sticking.gif) 643 | 644 | As you can see, when we're on the slope, the rectangle moves in the direction of the floor. However, the rectangle doesn't stick to the ground when there's a decreasing change in slope, and we fly off. This might be the behaviour you want, but just for completion, I'll show a quick way to implement truly sticking to the ground. 645 | 646 | ## Sticking to the Ground 647 | 648 | After moving the entity, we will check its grounded state again. If it was grounded before moving, and it's not grounded after moving, it has flied off. That is, assuming it didn't bounce and that it didn't jump. So, we will add the simple variables `jumped` and `bounced` that we will set to true in the parts of the previous code that handle jumping and bouncing respectively. The entity could also have stopped being grounded because it slid into a vertical edge. We don't want to pull it down in that case, so we will also check the direction of the speed, and if it's highly vertical we won't count that as having flied off. 649 | 650 | Now that we have a way to detect whether the entity has flied off, we will try to pull it down if it has. We will define a maximum change in slope (i.e. steepness) we can correct, in our case 3. Then we will find the maximum distance we will allow pulling it down, based on that slope and the distance it travelled horizontally. We will use `MoveRectangle()` yet again to attempt to move the entity downward by this max distance. If we detect a collision, it will mean the entity has moved down into a grounded position. We can take this as the new position. If there was no collision, it means no ground was found, so we can leave the entity as it was, letting it fly off. 651 | 652 | Depending on the terrain this might be enough. But in situations such as the following, it will snap to the ground when it shouldn't: 653 | 654 | ![Sticking to the ground 1](/img/4_1_sticking_to_the_ground_1.jpg) 655 | 656 | In this case, the entity shouldn't stick to the ground because there is an abrupt change in the floor line. We will detect these abrupt changes by iterating from the old position (1 in the diagram) to the new (2) by small steps, and each step finding the distance to the ground by calling `MoveRectangle()` as we did before. If the difference between the ground distance in two consecutive steps is greater than the maximum slope we allow times the step size, we will not pull the entity down. For this loop, we will need the old position (1), so before the movement code we will store the position of the entity into a variable `oldPos`. 657 | 658 | This is the code responsible for sticking to the ground, after the movement code: 659 | 660 | ```c++ 661 | // Get the grounded state after moving 662 | bool newGrounded; 663 | v2 newGroundNormal; 664 | MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2(0, -1.f), &newGrounded, &newGroundNormal); 665 | v2 speedDir = Normalize(gameState.rectSpeed); 666 | if (!bounced && !jumped && grounded && !newGrounded && Abs(speedDir.y) < .95f){ // If stopped being grounded, and not travelling extremely vertically... 667 | // We'll try to move the entity down. 668 | float maxDistance = 40.f; // The max distance we'll move down (independent of speed) 669 | float maxSlope = 3.f; // This further restricts maxDistance based on the horizontal speed. 670 | float deltaX = Abs(gameState.rectPos.x - oldPos.x); 671 | float finalMaxDistance = Min(maxDistance, deltaX*maxSlope); 672 | 673 | bool foundGround; 674 | v2 newPos = MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2(0, -finalMaxDistance), &foundGround, &newGroundNormal); 675 | if (foundGround && deltaX){ 676 | float prevY = oldPos.y; 677 | bool tooMuchSlope = false; 678 | int steps = Ceil(deltaX/2); 679 | for(int i = 0; i < steps; i ++){ 680 | float t = (float)(i + 1)/(float)steps; 681 | v2 p = {Lerp(oldPos.x, gameState.rectPos.x, t), 682 | Lerp(oldPos.y, gameState.rectPos.y, t)}; 683 | finalMaxDistance = Min(maxDistance, t*deltaX*maxSlope); 684 | v2 intermediatePos = MoveRectangle(p, gameState.rectDim/2, V2(0, -finalMaxDistance), &foundGround, &newGroundNormal); 685 | 686 | float dy = intermediatePos.y - prevY; 687 | float stepSize = (gameState.rectPos.x - oldPos.x)/(float)steps; 688 | float slope = Abs(dy)/stepSize; 689 | if (slope > maxSlope){ 690 | tooMuchSlope = true; 691 | break; 692 | } 693 | prevY = intermediatePos.y; 694 | } 695 | if (!tooMuchSlope){ 696 | gameState.rectPos = newPos; 697 | gameState.rectSpeed = gameState.rectSpeed - Dot(gameState.rectSpeed, newGroundNormal)*newGroundNormal; 698 | } 699 | } 700 | } 701 | ``` 702 | 703 | You can see how the entity now sticks to the ground: 704 | 705 | ![Sticking](/img/sticking.gif) 706 | 707 | Note that this is an inefficient manner of sticking to the ground, because of the amount of iteration. But if you don't have a ton of entities running this and constantly flying off the ground, it will work fine. 708 | 709 | Also note that this can cause a small irregularity in the speed, because by shifting the entity down, the distance travelled in that frame changes. However, this is probably imperceptible enough that you don't need to worry about it. 710 | 711 | 712 | # Part 5 - Optimization 713 | 714 | We won't be doing any optimization in this article, but I will explain what optimizations you could do if you wanted to make a real game out of this. 715 | 716 | First of all, you want to avoid iterating all existing walls when you check for collision. Instead of checking all walls, `MoveCircle()`/`MoveRectanlge()` would take in a list of walls to check against. You could have all your terrain organized into a grid of rectangular chunks, big enough so that no entity can step on walls from 3 different chunks in the same axis in the same frame. 717 | 718 | Each wall could be assigned to a chunk. Before calling the `MoveCircle()`/`MoveRectangle()` functions, you make a rectangular bounding box fully containing the entity in all the potential final positions after being moved (you can take the rectangle defined by the speed vector and expand it to account for the entity's dimension, the potential shift, and a tiny margin for precision issues). 719 | 720 | Next, you iterate all walls in the 4 chunks closest to the bounding box and cache those walls that intersect it. After that, you can call `MoveCircle()`/`MoveRectangle()` passing the list of cached walls. If you have a loop that calls the Move functions multiple times for the same entity, as we do in our player movement code, an additional broader caching step might further reduce the number of collision checks. 721 | 722 | Here are some other improvements you might want to make to `MoveCircle()`/`MoveRectangle()`: 723 | - Allow passing the minimum step size and maximum number of iterations as parameters, so you can configure different entities with different precision levels. Also, the ground checks don't need intermediate iterations, although ideally that would have its own code instead of reusing the Move functions. 724 | - Allow passing parameters that can configure the shift amount and let you disable the shifts, again, so you can configure entities differently. 725 | - You could return whether it shifted, if you ever need that information in your movement code for some reason. 726 | - As I mentioned multiple times already, the iterative method for moving up close to a wall could be replaced by a slightly more sophisticated method that would directly find the farthest free position. 727 | 728 | 729 | # Closing Words 730 | 731 | I hope this tutorial made you more capable of solving these kinds of collision and movement challenges by yourself. You're free to use the code of this project for your own games. 732 | 733 | ## Further Reading 734 | I recommend this 2012 article, [_The guide to implementing 2D platformers_](http://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/) by Rodrigo Monteiro, which concisely explains how the simpler types of 2D platformers can be implemented, with an emphasis on tile-based platformers. 735 | 736 | The [Sonic Physics Guide](https://info.sonicretro.org/Sonic_Physics_Guide) explains the mechanics of the original Sonic games in a lot of detail. It's a good resource, but I think the approach these games took is outdated. The amount of optimizations that today would offer insignificant gains has a huge cost on the flexibility of the engine. 737 | 738 | --- 739 | 740 | Author: Pere Dolcet 741 | 742 | Editor: Ted Bendixson 743 | 744 | (02/2023) 745 | -------------------------------------------------------------------------------- /img/0_0_dot_product.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/0_0_dot_product.jpg -------------------------------------------------------------------------------- /img/0_1_cross_product.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/0_1_cross_product.jpg -------------------------------------------------------------------------------- /img/1_1_rectangle_vs_triangle_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/1_1_rectangle_vs_triangle_1.jpg -------------------------------------------------------------------------------- /img/1_2_circle_vs_triangle_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/1_2_circle_vs_triangle_1.jpg -------------------------------------------------------------------------------- /img/1_2_circle_vs_triangle_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/1_2_circle_vs_triangle_2.jpg -------------------------------------------------------------------------------- /img/1_2_circle_vs_triangle_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/1_2_circle_vs_triangle_3.jpg -------------------------------------------------------------------------------- /img/1_2_circle_vs_triangle_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/1_2_circle_vs_triangle_4.jpg -------------------------------------------------------------------------------- /img/2_1_incrementally_smaller_steps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/2_1_incrementally_smaller_steps.jpg -------------------------------------------------------------------------------- /img/3_1_circle_vs_triangle_normal_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/3_1_circle_vs_triangle_normal_1.jpg -------------------------------------------------------------------------------- /img/3_1_circle_vs_triangle_normal_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/3_1_circle_vs_triangle_normal_2.jpg -------------------------------------------------------------------------------- /img/3_1_circle_vs_triangle_normal_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/3_1_circle_vs_triangle_normal_3.jpg -------------------------------------------------------------------------------- /img/3_2_rectangle_vs_triangle_normal_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/3_2_rectangle_vs_triangle_normal_1.jpg -------------------------------------------------------------------------------- /img/3_2_rectangle_vs_triangle_normal_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/3_2_rectangle_vs_triangle_normal_2.jpg -------------------------------------------------------------------------------- /img/3_2_rectangle_vs_triangle_normal_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/3_2_rectangle_vs_triangle_normal_3.jpg -------------------------------------------------------------------------------- /img/4_0_slide_bounce.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/4_0_slide_bounce.jpg -------------------------------------------------------------------------------- /img/4_0_smoothness_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/4_0_smoothness_1.jpg -------------------------------------------------------------------------------- /img/4_0_smoothness_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/4_0_smoothness_2.jpg -------------------------------------------------------------------------------- /img/4_1_sticking_to_the_ground_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/4_1_sticking_to_the_ground_1.jpg -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/demo.gif -------------------------------------------------------------------------------- /img/no_sticking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/no_sticking.gif -------------------------------------------------------------------------------- /img/sticking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pere001/2d-platformer-tutorial-2023/9a600ab901aa2e093ab0f2aa6b3433508dd76af1/img/sticking.gif -------------------------------------------------------------------------------- /src/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Run this script to compile! 4 | 5 | :: But first, make sure to substitute this path for wherever your vcvarsall.bat is, which will depend on the version of your MSVC compiler. 6 | call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" x64 7 | 8 | IF NOT EXIST .\build mkdir .\build 9 | pushd .\build 10 | 11 | cl ..\main.cpp -Femain.exe -Z7 -nologo -link User32.lib Opengl32.lib Gdi32.lib 12 | 13 | popd 14 | 15 | pause 16 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #define CREATE_CONSOLE false // Set this to true to get a console you can use for debugging with DebugPrint(). 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | // 14 | // Types 15 | // 16 | #include 17 | 18 | 19 | // 20 | // Define utilities 21 | // 22 | #define Assert(x) { if (!(x)) { *(int *)0 = 0; } } 23 | #define AssertRange(x0, x1, x2) Assert(((x0) <= (x1)) && ((x1) <= (x2))) 24 | #define ArrayCount(arr) (sizeof(arr) / sizeof(arr[0])) 25 | 26 | 27 | 28 | //============================================================================== 29 | // 30 | // Maths 31 | // 32 | //============================================================================== 33 | 34 | #define MAX_FLOAT 340282346638528859811704183484516925440.f 35 | #define MIN_FLOAT (-340282346638528859811704183484516925440.f) 36 | #define PI 3.141592653589793238f 37 | #define SQUARE(x) ((x)*(x)) 38 | 39 | #include 40 | inline float Cos(float x){ 41 | float result = cos(x); 42 | return result; 43 | } 44 | inline float Sin(float x){ 45 | float result = sin(x); 46 | return result; 47 | } 48 | inline float SquareRoot(float x){ 49 | float result = sqrt(x); 50 | return result; 51 | } 52 | 53 | // Interpolation 54 | inline float Lerp(float a, float b, float t){ 55 | float result = (1.f - t)*a + b*t; 56 | return result; 57 | } 58 | 59 | // Modulo 60 | inline float FMod(float x, float modBy){ 61 | Assert(modBy >= 0.f); 62 | float result = fmod(x, modBy); 63 | return result; 64 | } 65 | 66 | // Round towards -infinity 67 | inline float Floor(float x){ 68 | float result = floorf(x); 69 | return result; 70 | } 71 | 72 | // Round towards +infinity 73 | inline float Ceil(float x){ 74 | float result = ceilf(x); 75 | return result; 76 | } 77 | 78 | inline float Round(float x){ 79 | float result = roundf(x); 80 | return result; 81 | } 82 | 83 | // Fractional part 84 | inline float Frac(float x){ 85 | float result = x - (float)(int)x; 86 | return result; 87 | } 88 | 89 | // Max 90 | inline float Max(float a, float b){ 91 | if (a >= b) 92 | return a; 93 | return b; 94 | } 95 | inline int MaxInt(int a, int b){ 96 | if (a >= b) 97 | return a; 98 | return b; 99 | } 100 | 101 | // Min 102 | inline float Min(float a, float b){ 103 | if (a <= b) 104 | return a; 105 | return b; 106 | } 107 | inline int MinInt(int a, int b){ 108 | if (a <= b) 109 | return a; 110 | return b; 111 | } 112 | 113 | // Clamp 114 | inline float Clamp(float value, float min, float max){ 115 | if (value > max) return max; 116 | if (value < min) return min; 117 | return value; 118 | } 119 | inline float Clamp01(float value){ 120 | return Clamp(value, 0, 1.f); 121 | } 122 | inline int ClampInt(int value, int min, int max){ 123 | if (value > max) return max; 124 | if (value < min) return min; 125 | return value; 126 | } 127 | 128 | // Absolute value 129 | inline float Abs(float value){ 130 | if (value < 0) 131 | return -value; 132 | return value; 133 | } 134 | inline int AbsInt(int value){ 135 | if (value < 0) 136 | return -value; 137 | return value; 138 | } 139 | 140 | 141 | // Return range: [-pi, pi] 142 | inline float NormalizeAngle(float a){ 143 | a = fmod(a, 2*PI); 144 | if (a < -PI) 145 | a += 2*PI; 146 | else if (a > PI) 147 | a -= 2*PI; 148 | return a; 149 | } 150 | // Return range. [-pi, pi] 151 | float AngleDifference(float to, float from){ 152 | float result = NormalizeAngle(to) - NormalizeAngle(from); 153 | if (result > PI) 154 | result -= 2*PI; 155 | else if (result <= -PI) 156 | result += 2*PI; 157 | return result; 158 | } 159 | 160 | 161 | // 162 | // V2 163 | // 164 | struct v2{ 165 | union { 166 | struct{ 167 | float x; 168 | float y; 169 | }; 170 | float asArray[2]; // TODO Get rid of this to simplify if possible. 171 | }; 172 | }; 173 | 174 | inline v2 V2(float x, float y){ 175 | v2 result = {x, y}; 176 | return result; 177 | } 178 | inline v2 V2(float xy){ 179 | v2 result = {xy, xy}; 180 | return result; 181 | } 182 | 183 | inline v2 operator+(v2 a, v2 b){ 184 | v2 result = {a.x + b.x, a.y + b.y}; 185 | return result; 186 | } 187 | 188 | inline v2 operator-(v2 a, v2 b){ 189 | v2 result = {a.x - b.x, a.y - b.y}; 190 | return result; 191 | } 192 | 193 | inline v2 operator-(v2 a){ 194 | v2 result = {-a.x, -a.y}; 195 | return result; 196 | } 197 | 198 | inline v2 operator/(v2 a, float scalar){ 199 | v2 result = {a.x/scalar, a.y/scalar}; 200 | return result; 201 | } 202 | inline v2 operator*(v2 a, float scalar){ 203 | v2 result = {a.x*scalar, a.y*scalar}; 204 | return result; 205 | } 206 | inline v2 operator/(float scalar, v2 a){ 207 | v2 result = {scalar/a.x, scalar/a.y}; 208 | return result; 209 | } 210 | inline v2 operator*(float scalar, v2 a){ 211 | v2 result = {a.x*scalar, a.y*scalar}; 212 | return result; 213 | } 214 | 215 | inline void operator+=(v2 &a, v2 b){ 216 | a = a + b; 217 | } 218 | inline void operator*=(v2 &a, float scalar){ 219 | a = a * scalar; 220 | } 221 | inline void operator-=(v2 &a, v2 b){ 222 | a = a - b; 223 | } 224 | inline void operator/=(v2 &a, float scalar){ 225 | a = a / scalar; 226 | } 227 | inline bool operator==(v2 a, v2 b){ 228 | bool result = (a.x == b.x) && (a.y == b.y); 229 | return result; 230 | } 231 | inline bool operator!=(v2 a, v2 b){ 232 | bool result = (a.x != b.x) || (a.y != b.y); 233 | return result; 234 | } 235 | 236 | inline float Dot(v2 a, v2 b){ 237 | float result = a.x*b.x + a.y*b.y; 238 | return result; 239 | } 240 | 241 | inline float Cross(v2 a, v2 b){ 242 | float result = a.x*b.y - a.y*b.x; 243 | return result; 244 | } 245 | 246 | inline v2 Hadamard(v2 a, v2 b){ 247 | v2 result = {a.x*b.x, a.y*b.y}; 248 | return result; 249 | } 250 | 251 | inline float Length(v2 a){ 252 | float result = SquareRoot(a.x*a.x + a.y*a.y); 253 | return result; 254 | } 255 | inline float LengthSqr(v2 a){ 256 | float result = a.x*a.x + a.y*a.y; 257 | return result; 258 | } 259 | 260 | inline float AngleOf(v2 a){ 261 | float result = 0; 262 | if (a.x || a.y) 263 | result = atan2(a.y, a.x); 264 | return result; 265 | } 266 | 267 | inline v2 V2FromLengthDir(float length, float direction){ 268 | v2 result = {Cos(direction)*length, Sin(direction)*length}; 269 | return result; 270 | } 271 | // Counterclockwise 272 | inline v2 Rotate90Degrees(v2 p){ 273 | v2 result = {-p.y, p.x}; 274 | return result; 275 | } 276 | // Counterclockwise 277 | inline v2 RotateMinus90Degrees(v2 p){ 278 | v2 result = {p.y, -p.x}; 279 | return result; 280 | } 281 | 282 | inline v2 Normalize(v2 a){ 283 | v2 result = V2FromLengthDir(1.f, AngleOf(a)); 284 | return result; 285 | } 286 | 287 | inline bool PointInCircle(v2 p, v2 c, float r){ 288 | float distanceSqr = LengthSqr(p - c); 289 | if (distanceSqr <= r*r) 290 | return true; 291 | return false; 292 | } 293 | // - Tangent doesn't count as inside. 294 | inline bool PointInRectangle(v2 point, v2 rectMin, v2 rectMax){ 295 | bool result = (point.x > rectMin.x & point.x < rectMax.x & point.y > rectMin.y & point.y < rectMax.y); 296 | return result; 297 | } 298 | 299 | inline v2 FloorV2(v2 a){ 300 | v2 result = {Floor(a.x), Floor(a.y)}; 301 | return result; 302 | } 303 | inline v2 MinV2(v2 a, v2 b){ 304 | v2 result = {Min(a.x, b.x), Min(a.y, b.y)}; 305 | return result; 306 | } 307 | inline v2 MaxV2(v2 a, v2 b){ 308 | v2 result = {Max(a.x, b.x), Max(a.y, b.y)}; 309 | return result; 310 | } 311 | 312 | bool PointsAreCW(v2 *points, int num){ 313 | float angleSum = 0.f; 314 | v2 prevPoint = points[num - 1]; 315 | float prevAngle = AngleOf(prevPoint - points[num - 2]); 316 | for(int i = 0; i < num; i++){ 317 | float newAngle = AngleOf(points[i] - prevPoint); 318 | angleSum += AngleDifference(newAngle, prevAngle); 319 | prevAngle = newAngle; 320 | prevPoint = points[i]; 321 | } 322 | return (angleSum < 0); 323 | } 324 | 325 | 326 | 327 | //============================================================================== 328 | // 329 | // Some Platform Stuff 330 | // 331 | //============================================================================== 332 | 333 | struct button_state { 334 | bool isDown; 335 | int transitionCount; 336 | }; 337 | 338 | bool ButtonWentDown(button_state *b){ 339 | if (b->isDown && (b->transitionCount % 2)){ 340 | return true; 341 | } 342 | return false; 343 | } 344 | bool ButtonWentUp(button_state *b){ 345 | if (!b->isDown && (b->transitionCount % 2)){ 346 | return true; 347 | } 348 | return false; 349 | } 350 | 351 | struct keyboard_input{ 352 | union{ 353 | button_state asArray[48]; 354 | struct{ 355 | button_state letters[26]; 356 | button_state numbers[10]; 357 | button_state escape; 358 | button_state enter; 359 | button_state space; 360 | button_state shift; 361 | button_state control; 362 | button_state backspace; 363 | button_state alt; 364 | button_state tab; 365 | button_state arrowLeft; 366 | button_state arrowRight; 367 | button_state arrowUp; 368 | button_state arrowDown; 369 | }; 370 | }; 371 | }; 372 | struct input_state { 373 | keyboard_input keyboard; 374 | button_state mouseButtons[5]; 375 | v2 mousePos; 376 | v2 windowDim; 377 | }; 378 | 379 | void UpdateButtonState(button_state *b, bool newIsDown){ 380 | if (!b->isDown != !newIsDown){ 381 | b->isDown = newIsDown; 382 | b->transitionCount++; 383 | } 384 | } 385 | 386 | static HANDLE globalStdHandle = {}; 387 | static LARGE_INTEGER globalPerformanceFrequency; 388 | static bool globalRunning; 389 | static input_state globalInput = {}; 390 | static HCURSOR globalCursor; 391 | static char globalExePath[1024] = {}; 392 | 393 | void DebugPrint(char *str){ 394 | //OutputDebugStringA(str); 395 | WriteFile(globalStdHandle, str, (DWORD)strlen(str), 0, 0); 396 | } 397 | void DebugPrintf(char *format, ...){ 398 | va_list argptr; 399 | va_start(argptr, format); 400 | 401 | char localStr[1024]; 402 | vsprintf_s(localStr, sizeof(localStr), format, argptr); 403 | 404 | va_end(argptr); 405 | 406 | localStr[1023] = 0; // null-terminate 407 | DebugPrint(localStr); 408 | } 409 | 410 | 411 | 412 | //============================================================================== 413 | // 414 | // Game State 415 | // 416 | //============================================================================== 417 | 418 | struct wall{ 419 | v2 p[3]; 420 | v2 normals[3]; 421 | }; 422 | // Create wall from 3 points. 423 | wall Wall(v2 p0, v2 p1, v2 p2){ 424 | wall w; 425 | w.p[0] = p0; 426 | w.p[1] = p1; 427 | w.p[2] = p2; 428 | 429 | if (Cross(w.p[1] - w.p[0], w.p[2] - w.p[1]) < 0){ // Points are CW 430 | // Make them CCW 431 | v2 temp = w.p[1]; 432 | w.p[1] = w.p[2]; 433 | w.p[2] = temp; 434 | } 435 | 436 | // Compute normals 437 | v2 edgeDir01 = Normalize(w.p[1] - w.p[0]); 438 | v2 edgeDir12 = Normalize(w.p[2] - w.p[1]); 439 | v2 edgeDir20 = Normalize(w.p[0] - w.p[2]); 440 | w.normals[0] = V2(edgeDir01.y, -edgeDir01.x); 441 | w.normals[1] = V2(edgeDir12.y, -edgeDir12.x); 442 | w.normals[2] = V2(edgeDir20.y, -edgeDir20.x); 443 | 444 | return w; 445 | } 446 | struct game_state{ 447 | v2 cameraPos; 448 | float cameraScale; 449 | 450 | v2 rectPos; 451 | v2 rectDim; 452 | v2 rectSpeed; 453 | 454 | v2 circlePos; 455 | float circleRadius; 456 | v2 circleSpeed; 457 | 458 | int numWalls; 459 | wall walls[100]; 460 | }; 461 | static game_state gameState = {}; 462 | 463 | 464 | 465 | //============================================================================== 466 | // 467 | // Colllision Detection 468 | // 469 | //============================================================================== 470 | 471 | bool PointWallCollision(v2 point, wall *w){ 472 | for(int i = 0; i < 3; i++){ 473 | float proj = Dot(w->normals[i], point - w->p[i]); // Center projected onto normal 474 | if (proj > 0) // Too far outwards from the edge 475 | return false; 476 | } 477 | return true; 478 | } 479 | bool PointWallCollision(v2 point){ 480 | for(int i = 0; i < gameState.numWalls; i++){ 481 | if (PointWallCollision(point, &gameState.walls[i])) 482 | return true; 483 | } 484 | return false; 485 | } 486 | 487 | bool RectangleWallCollision(v2 rMin, v2 rMax, wall *w){ 488 | // Check if rectangle and triangle are separated by an axis-aligned line. 489 | // (The bitwise '&' operator here does the same as the boolean '&&', but it's faster because it avoids some branches) 490 | if ( (w->p[0].x >= rMax.x & w->p[1].x >= rMax.x & w->p[2].x >= rMax.x) 491 | || (w->p[0].y >= rMax.y & w->p[1].y >= rMax.y & w->p[2].y >= rMax.y) 492 | || (w->p[0].x <= rMin.x & w->p[1].x <= rMin.x & w->p[2].x <= rMin.x) 493 | || (w->p[0].y <= rMin.y & w->p[1].y <= rMin.y & w->p[2].y <= rMin.y)) 494 | return false; 495 | 496 | // Check if rectangle and triangle are separated by one of triangle's edges. 497 | for(int i = 0; i < 3; i++){ 498 | v2 p = w->p[i]; 499 | v2 n = w->normals[i]; 500 | 501 | float rectProjected[4]; 502 | rectProjected[0] = Dot(rMin - p, n); 503 | rectProjected[1] = Dot(V2(rMin.x, rMax.y) - p, n); 504 | rectProjected[2] = Dot(rMax - p, n); 505 | rectProjected[3] = Dot(V2(rMax.x, rMin.y) - p, n); 506 | 507 | float rectProjectedMin = Min(Min(Min(rectProjected[0], rectProjected[1]), rectProjected[2]), rectProjected[3]); 508 | 509 | if (rectProjectedMin >= 0){ 510 | return false; 511 | } 512 | } 513 | return true; // No separating axis found 514 | } 515 | bool RectangleWallCollision(v2 rMin, v2 rMax){ 516 | for(int i = 0; i < gameState.numWalls; i++){ 517 | if (RectangleWallCollision(rMin, rMax, &gameState.walls[i])) 518 | return true; 519 | } 520 | return false; 521 | } 522 | 523 | bool CircleWallCollision(v2 c, float r, wall *w){ 524 | // This loop checks the collision of the center with the triangle and the edge rectangles. 525 | bool centerOnTriangle = true; 526 | for(int i = 0; i < 3; i++){ 527 | float proj = Dot(w->normals[i], c - w->p[i]); // Center projected onto normal 528 | 529 | if (proj > r) // (Optional early-out: circle is too far outwards to collide) 530 | return false; 531 | 532 | if (proj < 0) // The center is inward from this edge: it might be inside the triangle or not, 533 | continue; // and inside other edge rects or not, but this will be found by the other iterations. 534 | 535 | // Check if center is inside the edge rects. 536 | v2 edgeDir = Rotate90Degrees(w->normals[i]); 537 | float edgeProj0 = Dot(edgeDir, c - w->p[i]); // Center projected onto edge direction, relative to current vertex. 538 | float edgeProj1 = Dot(edgeDir, c - w->p[(i + 1) % 3]); // Center projected onto edge direction, relative to next vertex. 539 | if (edgeProj0 > 0 && edgeProj1 < 0){ 540 | // The projected center falls between the two vertices of the edge: the center is inside the edge rect. 541 | return true; 542 | } 543 | 544 | centerOnTriangle = false; 545 | } 546 | 547 | if (!centerOnTriangle){ 548 | // Center wasn't inside the triangle or the edge rects 549 | // Check if a vertex is inside the circle. 550 | for(int i = 0; i < 3; i++){ 551 | float distanceSqr = LengthSqr(w->p[i] - c); 552 | if (distanceSqr < r*r) 553 | return true; 554 | } 555 | } 556 | return centerOnTriangle; 557 | } 558 | bool CircleWallCollision(v2 c, float r){ 559 | for(int i = 0; i < gameState.numWalls; i++){ 560 | if (CircleWallCollision(c, r, &gameState.walls[i])) 561 | return true; 562 | } 563 | return false; 564 | } 565 | 566 | 567 | 568 | //============================================================================== 569 | // 570 | // Movement 571 | // 572 | //============================================================================== 573 | 574 | v2 MoveCircle(v2 pos, float r, v2 speed, bool *outCollided, v2 *outCollisionNormal){ 575 | if (speed == V2(0)){ 576 | *outCollided = false; 577 | return pos; 578 | } 579 | 580 | wall *walls = gameState.walls; 581 | int numWalls = gameState.numWalls; 582 | 583 | // 584 | // Move 585 | // 586 | bool collided = false; 587 | 588 | v2 newPos = pos; 589 | float numerator = 1.f; 590 | float denominator = 1.f; 591 | 592 | v2 lastCollisionPos = {}; 593 | int firstCollidingWallIndex = numWalls; // (This default value will cause the collision normal code to be skipped.) 594 | 595 | bool alreadyShifted = false; // We can only shift once 596 | v2 permanentShift = {0, 0}; // After having shifted, all subsequent steps will apply the same shift, stored here. 597 | 598 | float speedLength = Length(speed); 599 | 600 | float stepSize = 1.f; 601 | float speedFactor = 1.f; 602 | 603 | for(int steps = 0; steps < 15; steps++){ 604 | v2 p = pos + speedFactor*speed + permanentShift; 605 | 606 | bool collision = false; 607 | for(int i = 0; i < numWalls; i++){ // Check collision against all walls 608 | wall *wll = &walls[i]; 609 | if (CircleWallCollision(p, r, wll)){ 610 | if (alreadyShifted){ // Accept the collision 611 | collision = true; 612 | lastCollisionPos = p; 613 | firstCollidingWallIndex = i; 614 | }else{ // Try to shift 615 | // "Shifting" means we'll check again for collision after applying a tiny offset in both directions 616 | // perpendicular to the speed, in order to facilitate smooth movement tangent to a wall. 617 | 618 | float shiftDistance = .1f; 619 | v2 shiftVector = shiftDistance*Normalize(Rotate90Degrees(speed)); 620 | v2 shifts[2] = {shiftVector, -shiftVector}; 621 | bool shiftFailed[2] = {false, false}; 622 | 623 | // We store p + shift here to avoid it having different results when calculated in different places 624 | // because of optimizer or something, which could potentially put us inside a wall. 625 | v2 shiftedP[2] = {p + shifts[0], p + shifts[1]}; 626 | 627 | for(int j = 0; j < numWalls; j++){ 628 | wall *w = &walls[j]; 629 | for(int k = 0; k < 2; k++){ 630 | if (!shiftFailed[k] && CircleWallCollision(shiftedP[k], r, w)){ 631 | shiftFailed[k] = true; 632 | } 633 | } 634 | if (shiftFailed[0] & shiftFailed[1]) 635 | break; 636 | } 637 | 638 | if (shiftFailed[0] & shiftFailed[1]){ // Both shifts were unsuccessful 639 | collision = true; 640 | lastCollisionPos = p; 641 | firstCollidingWallIndex = i; 642 | }else{ 643 | // One of the shifts is a free position. 644 | for(int j = 0; j < 2; j++){ 645 | if (!shiftFailed[j]){ 646 | p = shiftedP[j]; // p will be assigned to newPos. 647 | permanentShift = shifts[j]; 648 | alreadyShifted = true; 649 | break; 650 | } 651 | } 652 | } 653 | break; // Stop iterating walls for this step (because we either found a collision or already iterated all walls and found a free shifted position). 654 | } 655 | } 656 | } 657 | 658 | stepSize *= .5f; 659 | if (collision){ 660 | speedFactor -= stepSize; 661 | collided = true; 662 | }else{ 663 | speedFactor += stepSize; 664 | newPos = p; 665 | if (steps == 0) 666 | break; // No collisions found on the first step. 667 | } 668 | 669 | if (stepSize*speedLength <= .05f) // Limit step length. 670 | break; 671 | } 672 | 673 | 674 | // 675 | // Calculate normal 676 | // 677 | v2 bestNormal = -Normalize(speed); // Default value in case we don't find any. 678 | float bestTime = MAX_FLOAT; // Lowest time until collision, to find the edge that would collide first. 679 | for(int wallIndex = firstCollidingWallIndex; wallIndex < numWalls; wallIndex++){ 680 | wall *w = &walls[wallIndex]; 681 | if (!CircleWallCollision(lastCollisionPos, r, w)) 682 | continue; 683 | 684 | for(int i = 0; i < 3; i++){ 685 | if (Dot(w->normals[i], speed) >= 0) 686 | continue; // Ignore edges facing away 687 | 688 | v2 t0 = w->p[i]; 689 | v2 t1 = w->p[(i + 1) % 3]; 690 | 691 | // Let's find the closest point on the edge. 692 | v2 edgeDir = Rotate90Degrees(w->normals[i]); 693 | float cProjected = Dot(edgeDir, newPos - t0); // Center projected onto the edge. 694 | float t1Projected = Dot(edgeDir, t1 - t0); // Next vertex projected onto the edge. 695 | v2 collisionPoint = t0 + edgeDir*Clamp(cProjected, 0, t1Projected); // Closest point on the edge 696 | 697 | v2 n = Normalize(newPos - collisionPoint); // Collision normal of this edge. 698 | float distance = Dot(newPos - collisionPoint, n) - r; // (We use Dot() to avoid the squareroot of Length()). 699 | float nDotSpeed = Dot(n, speed); 700 | if (!nDotSpeed) 701 | continue; // Avoid division by 0. 702 | float time = distance/-nDotSpeed; // Time to hit wall 703 | if (time < bestTime && Dot(n, speed) <= 0){ // Ignore normals that face away from speed (rare but technically possible because of shifts) 704 | bestTime = time; 705 | bestNormal = n; 706 | } 707 | } 708 | } 709 | 710 | *outCollided = collided; 711 | *outCollisionNormal = bestNormal; 712 | return newPos; 713 | } 714 | 715 | 716 | // Collision box is centered at pos. 717 | v2 MoveRectangle(v2 pos, v2 halfDim, v2 speed, bool *outCollided, v2 *outCollisionNormal){ 718 | if (speed == V2(0)){ 719 | *outCollided = false; 720 | return pos; 721 | } 722 | 723 | wall *walls = gameState.walls; 724 | int numWalls = gameState.numWalls; 725 | 726 | // 727 | // Move 728 | // 729 | bool collided = false; 730 | 731 | v2 newPos = pos; 732 | 733 | v2 lastCollisionPos = {}; 734 | int firstCollidingWallIndex = numWalls; // (This default value will cause the collision normal code to be skipped.) 735 | 736 | bool alreadyShifted = false; // We can only shift once 737 | v2 permanentShift = {0, 0}; // After having shifted, all subsequent steps will apply the same shift, stored here. 738 | 739 | float speedLength = Length(speed); 740 | 741 | float stepSize = 1.f; 742 | float speedFactor = 1.f; 743 | 744 | for(int steps = 0; steps < 15; steps++){ 745 | v2 p = pos + speedFactor*speed + permanentShift; 746 | 747 | bool collision = false; 748 | for(int i = 0; i < numWalls; i++){ // Check collision against all walls 749 | wall *wll = &walls[i]; 750 | if (RectangleWallCollision(p - halfDim, p + halfDim, wll)){ 751 | if (alreadyShifted){ // Accept the collision 752 | collision = true; 753 | firstCollidingWallIndex = i; 754 | }else{ // Try to shift 755 | // "Shifting" means we'll check again for collision after applying a tiny offset in both directions 756 | // perpendicular to the speed, in order to facilitate smooth movement tangent to a wall. 757 | 758 | float shiftDistance = .1f; 759 | v2 shiftVector = shiftDistance*Normalize(Rotate90Degrees(speed)); 760 | v2 shifts[2] = {shiftVector, -shiftVector}; 761 | bool shiftFailed[2] = {false, false}; 762 | 763 | // We store p + shift here to avoid it having different results when calculated in different places 764 | // because of optimizer or something, which could potentially put us inside a wall. 765 | v2 shiftedP[2] = {p + shifts[0], p + shifts[1]}; 766 | 767 | for(int j = 0; j < numWalls; j++){ 768 | wall *w = &walls[j]; 769 | for(int k = 0; k < 2; k++){ 770 | if (!shiftFailed[k] && RectangleWallCollision(shiftedP[k] - halfDim, shiftedP[k] + halfDim, w)){ 771 | shiftFailed[k] = true; 772 | } 773 | } 774 | if (shiftFailed[0] & shiftFailed[1]) 775 | break; 776 | } 777 | 778 | if (shiftFailed[0] & shiftFailed[1]){ // Both shifts were unsuccessful 779 | collision = true; 780 | firstCollidingWallIndex = i; 781 | }else{ 782 | // One of the shifts is a free position. 783 | for(int j = 0; j < 2; j++){ 784 | if (!shiftFailed[j]){ 785 | p = shiftedP[j]; // p will be assigned to newPos. 786 | permanentShift = shifts[j]; 787 | alreadyShifted = true; 788 | break; 789 | } 790 | } 791 | } 792 | break; // Stop iterating walls for this step (because we either found a collision or already iterated all walls and found a free shifted position). 793 | } 794 | } 795 | } 796 | 797 | stepSize *= .5f; 798 | if (collision){ 799 | speedFactor -= stepSize; 800 | collided = true; 801 | lastCollisionPos = p; 802 | }else{ 803 | speedFactor += stepSize; 804 | newPos = p; 805 | if (steps == 0) 806 | break; // No collisions found on the first step. 807 | } 808 | 809 | if (stepSize*speedLength <= .05f) // Limit step length. 810 | break; 811 | } 812 | 813 | // 814 | // Calculate normal 815 | // 816 | 817 | v2 newR0 = newPos - halfDim; 818 | v2 newR1 = newPos + halfDim; 819 | v2 newRectPoints[4] = {newR0, V2(newR0.x, newR1.y), newR1, V2(newR1.x, newR0.y)}; 820 | 821 | // We find the two edges (one horizontal and one vertical) that face the direction of the speed, 822 | // and these arrays hold their normal and a point of the edge. 823 | v2 rectEdgePoints[2] = { {(speed.x < 0 ? newR0.x : newR1.x), newPos.y}, 824 | {newPos.x, (speed.y < 0 ? newR0.y : newR1.y)} }; 825 | v2 rectEdgeNormals[2] = { {(speed.x < 0 ? -1.f : 1.f), 0}, 826 | {0, (speed.y < 0 ? -1.f : 1.f)}}; 827 | 828 | v2 bestNormal = -Normalize(speed); // Final collision normal (Default value in case we don't find any) 829 | float bestTime = MAX_FLOAT; // Lowest time until collision, to find the edge that would collide first. 830 | 831 | for(int wallIndex = 0; wallIndex < numWalls; wallIndex++){ 832 | wall *w = &walls[wallIndex]; 833 | if (!RectangleWallCollision(lastCollisionPos - halfDim, lastCollisionPos + halfDim, w)) 834 | continue; 835 | 836 | v2 localNormal = bestNormal; // Best normal of the current wall. (default value "just in case") 837 | float localBestTime = MIN_FLOAT; // Highest time to hit an edge 838 | 839 | // Find the edge with the highest time until impact 840 | 841 | // Project rectangle onto triangle normals 842 | for(int i = 0; i < 3; i++){ 843 | v2 n = w->normals[i]; 844 | v2 p = w->p[i]; 845 | float nDotSpeed = Dot(n, speed); 846 | if (nDotSpeed < 0){ // Ignore edges facing away. 847 | float d = Min(Min(Min(Dot(newRectPoints[0] - p, n), // Projected distance to rectangle 848 | Dot(newRectPoints[1] - p, n)), 849 | Dot(newRectPoints[2] - p, n)), 850 | Dot(newRectPoints[3] - p, n)); 851 | float t = d/-nDotSpeed; // Time to hit edge 852 | if (t > localBestTime){ 853 | localBestTime = t; 854 | localNormal = n; 855 | } 856 | } 857 | } 858 | 859 | // Project triangle onto rectangle normals (we only need to check the two edges facing the direction of the speed) 860 | for(int i = 0; i < 2; i++){ 861 | v2 n = rectEdgeNormals[i]; 862 | float nDotSpeed = Dot(n, speed); 863 | if (nDotSpeed){ // Gota do this to avoid potential division by 0. 864 | float d = Min(Min(Dot(w->p[0] - rectEdgePoints[i], n), // Projected distance to triangle 865 | Dot(w->p[1] - rectEdgePoints[i], n)), 866 | Dot(w->p[2] - rectEdgePoints[i], n)); 867 | float t = d/nDotSpeed; // Time to hit edge 868 | if (t > localBestTime){ 869 | localBestTime = t; 870 | localNormal = -n; 871 | } 872 | } 873 | } 874 | 875 | // Compare the best normal in this wall with the best of previous walls. 876 | float epsilon = 0.000001f; 877 | if (localBestTime < bestTime || 878 | (localBestTime - bestTime < epsilon && Dot(localNormal, speed) < Dot(bestNormal, speed))) // If there's a tie we'll take the normal most aligned with the speed. 879 | { 880 | bestNormal = localNormal; 881 | bestTime = localBestTime; 882 | } 883 | } 884 | 885 | 886 | *outCollided = collided; 887 | *outCollisionNormal = bestNormal; 888 | return newPos; 889 | } 890 | 891 | 892 | 893 | //============================================================================== 894 | // 895 | // Platform Stuff 896 | // 897 | //============================================================================== 898 | 899 | LARGE_INTEGER GetCurrentTimeCounter(){ 900 | LARGE_INTEGER result = {}; 901 | QueryPerformanceCounter(&result); 902 | return result; 903 | } 904 | 905 | float GetSecondsElapsed(LARGE_INTEGER t0, LARGE_INTEGER t1){ 906 | float result = (float)(t1.QuadPart - t0.QuadPart) / (float)globalPerformanceFrequency.QuadPart; 907 | return result; 908 | } 909 | 910 | v2 GetWindowDimension(HWND window){ 911 | RECT rect = {}; 912 | GetClientRect(window, &rect); 913 | 914 | v2 result = {(float)(rect.right - rect.left), (float)(-rect.top + rect.bottom)}; 915 | return result; 916 | } 917 | 918 | LRESULT CALLBACK WindowProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) { 919 | switch (msg) { 920 | case WM_DESTROY: 921 | { 922 | PostQuitMessage(0); 923 | globalRunning = false; 924 | return 0; 925 | } 926 | 927 | } 928 | 929 | return DefWindowProc(window, msg, wParam, lParam); 930 | } 931 | 932 | 933 | 934 | //============================================================================== 935 | // 936 | // main() 937 | // 938 | //============================================================================== 939 | int WINAPI WinMain(HINSTANCE instance, HINSTANCE prevInstance, PSTR cmdLine, int cmdShow) 940 | { 941 | QueryPerformanceFrequency(&globalPerformanceFrequency); 942 | 943 | globalCursor = LoadCursor(0, IDC_ARROW); 944 | 945 | // Load exe path 946 | GetModuleFileNameA(0, globalExePath, ArrayCount(globalExePath)); 947 | { 948 | int len = strlen(globalExePath); 949 | for(int i = len-1; i >= 0; i--){ 950 | if (globalExePath[i] == '\\') 951 | break; 952 | globalExePath[i] = '\0'; 953 | } 954 | } 955 | // 956 | // Create window 957 | // 958 | WNDCLASS windowClass = {}; 959 | windowClass.lpfnWndProc = WindowProc; 960 | windowClass.hInstance = instance; 961 | windowClass.lpszClassName = "main window class"; 962 | windowClass.style = CS_HREDRAW | CS_VREDRAW; 963 | RegisterClass(&windowClass); 964 | 965 | v2 initialWindowSize = {1220.f, 840.f}; 966 | RECT windowFrameSize = {0, 0, (int)initialWindowSize.x, (int)initialWindowSize.y}; 967 | AdjustWindowRect(&windowFrameSize, WS_OVERLAPPEDWINDOW|WS_VISIBLE, false); 968 | HWND window = CreateWindowEx(0, windowClass.lpszClassName, "Physics Article Demo", 969 | WS_OVERLAPPEDWINDOW|WS_VISIBLE, 970 | CW_USEDEFAULT, CW_USEDEFAULT, 971 | windowFrameSize.right, windowFrameSize.bottom, 972 | //(int)initialWindowSize.x, (int)initialWindowSize.y, 973 | 0, 0, instance, 0); 974 | if (!window) 975 | return 0; 976 | //ShowWindow(window, cmdShow); 977 | HCURSOR cursor = LoadCursor(NULL, IDC_ARROW); 978 | SetCursor(cursor); 979 | 980 | 981 | 982 | // 983 | // Create console 984 | // 985 | if (CREATE_CONSOLE){ 986 | if(AttachConsole((DWORD)-1) == 0){ // wasn't launched from console 987 | AllocConsole(); // alloc your own instead 988 | } 989 | } 990 | globalStdHandle = GetStdHandle(STD_OUTPUT_HANDLE); 991 | 992 | 993 | globalInput.windowDim = GetWindowDimension(window); 994 | 995 | HDC dc = GetDC(window); 996 | 997 | // 998 | // OpenGl 999 | // 1000 | PIXELFORMATDESCRIPTOR pfd = {}; 1001 | pfd.nSize = sizeof(pfd); 1002 | pfd.nVersion = 1; 1003 | pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL; 1004 | pfd.iPixelType = PFD_TYPE_RGBA; 1005 | pfd.cColorBits = 32; 1006 | int pf = ChoosePixelFormat(dc, &pfd); 1007 | 1008 | if (!pf) 1009 | return 0; 1010 | 1011 | if (!SetPixelFormat(dc, pf, &pfd)) 1012 | return 0; 1013 | 1014 | DescribePixelFormat(dc, pf, sizeof(pfd), &pfd); 1015 | 1016 | // Device context 1017 | HGLRC rc = wglCreateContext(dc); 1018 | wglMakeCurrent(dc, rc); 1019 | 1020 | int timeInFrames = 0; 1021 | 1022 | // 1023 | // Init Game State 1024 | // 1025 | gameState.cameraPos = V2(0, 0); 1026 | gameState.cameraScale = 1.f; 1027 | 1028 | gameState.rectPos = V2(0, 0); 1029 | gameState.rectDim = V2(40, 60); 1030 | gameState.circlePos = V2(98, 0); 1031 | gameState.circleRadius = 50.f; 1032 | 1033 | gameState.numWalls = 20; 1034 | gameState.walls[0] = Wall(V2(-250.f, -50.f), V2(-250.f, -150.f), V2(150.f, -50.f)); // Center Floor 1035 | gameState.walls[1] = Wall(V2(150.f, -50.f), V2(-250.f, -150.f), V2(150.f, -150.f)); 1036 | 1037 | gameState.walls[2] = Wall(V2(-250.f, -150.f), V2(-250.f, 50.f), V2(-350.f, 50.f)); // Left Floor 1038 | gameState.walls[3] = Wall(V2(-250.f, -150.f), V2(-350.f, 50.f), V2(-350.f, -150.f)); 1039 | 1040 | gameState.walls[4] = Wall(V2(150, -150), V2(150, -50), V2(550, -50)); // Right Floor 1041 | gameState.walls[5] = Wall(V2(150, -150), V2(550, -50), V2(550, -150)); 1042 | 1043 | gameState.walls[6] = Wall(V2(-350.f, 50.f), V2(-550.f, 150.f), V2(-550.f, 50.f)); // Left Slope 1044 | gameState.walls[7] = Wall(V2(-350.f, 50.f), V2(-550.f, 50.f), V2(-350.f, -150.f)); 1045 | gameState.walls[8] = Wall(V2(-115, -43), V2(-248, 33), V2(-250, -50)); 1046 | 1047 | gameState.walls[9] = Wall(V2(150, -50), V2(250, 50), V2(250, -50)); // Right Slopes 1048 | gameState.walls[10] = Wall(V2(250, 50), V2(350, -50), V2(250, -50)); 1049 | gameState.walls[11] = Wall(V2(350, -50), V2(550, 150), V2(550, -50)); 1050 | gameState.walls[12] = Wall(V2(321, -60), V2(386, -53), V2(351, -20)); // Intersecting wall 1051 | 1052 | gameState.walls[13] = Wall(V2(407, 117), V2(429, 270), V2(284, 150)); // Random flying walls 1053 | gameState.walls[14] = Wall(V2(-120, 220), V2(53, 63), V2(133, 265)); 1054 | gameState.walls[15] = Wall(V2(-384, 193), V2(-350, 250), V2(-400, 250)); 1055 | 1056 | gameState.walls[16] = Wall(V2(550, 150), V2(600, 150), V2(550, 500)); // Right edge barrier 1057 | gameState.walls[17] = Wall(V2(600, 150), V2(600, 500), V2(550, 500)); 1058 | gameState.walls[18] = Wall(V2(-600, 150), V2(-550, 150), V2(-600, 500)); // Left edge barrier 1059 | gameState.walls[19] = Wall(V2(-550, 150), V2(-550, 500), V2(-600, 500)); 1060 | 1061 | 1062 | 1063 | LARGE_INTEGER lastFrameTime = GetCurrentTimeCounter(); 1064 | globalRunning = true; 1065 | while(globalRunning){ 1066 | 1067 | // 1068 | // Message Loop 1069 | // 1070 | MSG msg = { }; 1071 | while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) > 0) { 1072 | switch(msg.message){ 1073 | case WM_KEYUP: 1074 | case WM_KEYDOWN: 1075 | { 1076 | // 1077 | // Keyboard Input 1078 | // 1079 | bool wentDown = (msg.message == WM_KEYDOWN); 1080 | auto k = &globalInput.keyboard; 1081 | if (msg.wParam == VK_ESCAPE){ 1082 | UpdateButtonState(&k->escape, wentDown); 1083 | }else if (msg.wParam == VK_RETURN){ 1084 | UpdateButtonState(&k->enter, wentDown); 1085 | }else if (msg.wParam == VK_SPACE){ 1086 | UpdateButtonState(&k->space, wentDown); 1087 | }else if (msg.wParam == VK_SHIFT){ 1088 | UpdateButtonState(&k->shift, wentDown); 1089 | }else if (msg.wParam == VK_CONTROL){ 1090 | UpdateButtonState(&k->control, wentDown); 1091 | }else if (msg.wParam == VK_BACK){ 1092 | UpdateButtonState(&k->backspace, wentDown); 1093 | }else if (msg.wParam == VK_MENU){ 1094 | UpdateButtonState(&k->alt, wentDown); 1095 | }else if (msg.wParam == VK_TAB){ 1096 | UpdateButtonState(&k->tab, wentDown); 1097 | }else if (msg.wParam == VK_LEFT){ 1098 | UpdateButtonState(&k->arrowLeft, wentDown); 1099 | }else if (msg.wParam == VK_RIGHT){ 1100 | UpdateButtonState(&k->arrowRight, wentDown); 1101 | }else if (msg.wParam == VK_UP){ 1102 | UpdateButtonState(&k->arrowUp, wentDown); 1103 | }else if (msg.wParam == VK_DOWN){ 1104 | UpdateButtonState(&k->arrowDown, wentDown); 1105 | }else if (msg.wParam >= 'A' && msg.wParam <= 'Z'){ 1106 | UpdateButtonState(&k->letters[msg.wParam - 'A'], wentDown); 1107 | }else if (msg.wParam >= '0' && msg.wParam <= '9'){ 1108 | UpdateButtonState(&k->numbers[msg.wParam - '0'], wentDown); 1109 | } 1110 | 1111 | } break; 1112 | 1113 | default: 1114 | { 1115 | TranslateMessage(&msg); 1116 | DispatchMessage(&msg); 1117 | } 1118 | } 1119 | } 1120 | 1121 | 1122 | // 1123 | // Mouse Input 1124 | // 1125 | globalInput.windowDim = GetWindowDimension(window); 1126 | 1127 | POINT mousePoint; 1128 | GetCursorPos(&mousePoint); 1129 | ScreenToClient(window, &mousePoint); 1130 | globalInput.mousePos.x = (float)(int)mousePoint.x; 1131 | globalInput.mousePos.y = (float)(int)(globalInput.windowDim.y - mousePoint.y); 1132 | 1133 | bool hasFocus = (GetFocus() == window); 1134 | if (PointInRectangle(globalInput.mousePos, V2(-.1f), globalInput.windowDim) && hasFocus){ 1135 | UpdateButtonState(&globalInput.mouseButtons[0], GetKeyState(VK_LBUTTON) & (1 << 15)); 1136 | UpdateButtonState(&globalInput.mouseButtons[1], GetKeyState(VK_MBUTTON) & (1 << 15)); 1137 | UpdateButtonState(&globalInput.mouseButtons[2], GetKeyState(VK_RBUTTON) & (1 << 15)); 1138 | UpdateButtonState(&globalInput.mouseButtons[3], GetKeyState(VK_XBUTTON1) & (1 << 15)); 1139 | UpdateButtonState(&globalInput.mouseButtons[4], GetKeyState(VK_XBUTTON2) & (1 << 15)); 1140 | } 1141 | 1142 | // 1143 | // Game Code 1144 | // 1145 | v2 windowDim = globalInput.windowDim; 1146 | v2 viewPos = gameState.cameraPos - (windowDim/gameState.cameraScale)/2 + V2(0, 140); 1147 | v2 mouseWorldPos = viewPos + globalInput.mousePos/gameState.cameraScale; 1148 | 1149 | 1150 | // Left click: Place walls 1151 | static int placingWallPointIndex = 0; 1152 | if (!globalInput.keyboard.shift.isDown){ 1153 | if (gameState.numWalls < ArrayCount(gameState.walls)){ 1154 | wall *w = &gameState.walls[gameState.numWalls]; 1155 | v2 pointPos = mouseWorldPos; 1156 | if (globalInput.keyboard.control.isDown){ // Snap to grid 1157 | float gridSize = 50; 1158 | pointPos = FloorV2((mouseWorldPos + V2(gridSize/2))/gridSize)*gridSize; 1159 | } 1160 | w->p[placingWallPointIndex] = pointPos; 1161 | if (ButtonWentDown(&globalInput.mouseButtons[0])){ 1162 | placingWallPointIndex = (placingWallPointIndex + 1) % 3; 1163 | if (placingWallPointIndex == 0){ // Made a new wall 1164 | // Set normals 1165 | gameState.walls[gameState.numWalls] = Wall(gameState.walls[gameState.numWalls].p[0], gameState.walls[gameState.numWalls].p[1], gameState.walls[gameState.numWalls].p[2]); 1166 | DebugPrintf("Placed a wall {(%.0f, %.0f), (%.0f, %.0f), (%.0f, %.0f)}\n", 1167 | gameState.walls[gameState.numWalls].p[0].x, gameState.walls[gameState.numWalls].p[0].y, 1168 | gameState.walls[gameState.numWalls].p[1].x, gameState.walls[gameState.numWalls].p[1].y, 1169 | gameState.walls[gameState.numWalls].p[2].x, gameState.walls[gameState.numWalls].p[2].y); 1170 | gameState.numWalls++; 1171 | } 1172 | if (gameState.numWalls < ArrayCount(gameState.walls)) 1173 | gameState.walls[gameState.numWalls].p[placingWallPointIndex] = pointPos; 1174 | } 1175 | } 1176 | } 1177 | 1178 | // Right click: Remove walls. 1179 | if (ButtonWentDown(&globalInput.mouseButtons[2])){ 1180 | if (placingWallPointIndex){ 1181 | placingWallPointIndex = 0; // Cancel current half-baked wall. 1182 | }else{ // Remove walls. 1183 | for(int i = 0; i < gameState.numWalls; i++){ 1184 | if (PointWallCollision(mouseWorldPos, &gameState.walls[i])){ 1185 | gameState.numWalls--; 1186 | if (i < gameState.numWalls){ // Fill hole in array 1187 | memmove(&gameState.walls[i], &gameState.walls[i + 1], sizeof(wall)*(gameState.numWalls - i)); 1188 | } 1189 | } 1190 | } 1191 | } 1192 | } 1193 | 1194 | // 1195 | // Player Movement: Circle 1196 | // 1197 | { 1198 | // Read input and set speed. 1199 | bool grounded; 1200 | v2 groundNormal; 1201 | // We just call this to get the grounded state 1202 | MoveCircle(gameState.circlePos, gameState.circleRadius, V2(0, -1.f), &grounded, &groundNormal); 1203 | if (!grounded) 1204 | groundNormal = V2(0, 1.f); 1205 | 1206 | // Read input and set speed. 1207 | // Move in direction of ground to avoid "stepping" down slopes. 1208 | float ax = 0; // "Horizontal" acceleration 1209 | v2 groundDir = RotateMinus90Degrees(groundNormal); 1210 | if (globalInput.keyboard.letters['D' - 'A'].isDown){ 1211 | ax = .7f; // Accelerate right 1212 | }else if (globalInput.keyboard.letters['A' - 'A'].isDown){ 1213 | ax = -.7f; // Accelerate left 1214 | }else{ 1215 | ax = Dot(gameState.circleSpeed, groundDir)*-.1f; // Decelerate. 1216 | } 1217 | gameState.circleSpeed += ax*groundDir; 1218 | // Limit speed 1219 | float maxSpeed = 22.f; 1220 | if (Length(gameState.circleSpeed) > maxSpeed) 1221 | gameState.circleSpeed *= maxSpeed/Length(gameState.circleSpeed); 1222 | 1223 | 1224 | float minSpeedY = -30.f; 1225 | if (grounded){ 1226 | if (globalInput.keyboard.letters['W' - 'A'].isDown){ 1227 | gameState.circleSpeed.y = 10.f; // Jump 1228 | }else{ 1229 | float diff = Abs(AngleDifference(PI/2, AngleOf(groundNormal))); 1230 | if (diff > PI/4 && gameState.circleSpeed.y > minSpeedY) 1231 | gameState.circleSpeed.y -= .3f; 1232 | } 1233 | }else if (gameState.circleSpeed.y > minSpeedY){ 1234 | gameState.circleSpeed.y -= .4f; // Gravity 1235 | } 1236 | 1237 | float speedLength = Length(gameState.circleSpeed); 1238 | float toMove = speedLength; 1239 | for(int i = 0; i < 6; i++){ // Artificial iteration limit 1240 | bool collided; 1241 | v2 collisionNormal; 1242 | v2 oldPos = gameState.circlePos; 1243 | gameState.circlePos = MoveCircle(gameState.circlePos, gameState.circleRadius, V2FromLengthDir(toMove, AngleOf(gameState.circleSpeed)), &collided, &collisionNormal); 1244 | toMove -= Length(gameState.circlePos - oldPos); 1245 | if (!collided) 1246 | break; 1247 | v2 effectiveSpeed = V2FromLengthDir(speedLength, AngleOf(gameState.circleSpeed)); 1248 | if (Dot(effectiveSpeed, collisionNormal) < -9.f) { 1249 | // Hit a wall hard: bounce! 1250 | gameState.circleSpeed = (gameState.circleSpeed - 2*Dot(gameState.circleSpeed, collisionNormal)*collisionNormal)*.4f; 1251 | }else if (Abs(AngleDifference(AngleOf(collisionNormal), PI/2)) > PI*.2f){ 1252 | // There's considerable slope: slide! 1253 | gameState.circleSpeed = gameState.circleSpeed - Dot(gameState.circleSpeed, collisionNormal)*collisionNormal; 1254 | }else{ 1255 | gameState.circleSpeed = gameState.circleSpeed - Dot(gameState.circleSpeed, collisionNormal)*collisionNormal; 1256 | break; 1257 | } 1258 | if (toMove < .1f) 1259 | break; 1260 | } 1261 | } 1262 | 1263 | 1264 | // 1265 | // Player Movement: Rectangle 1266 | // 1267 | { 1268 | v2 oldPos = gameState.rectPos; 1269 | 1270 | bool grounded; 1271 | v2 groundNormal; 1272 | // We just call this to get the grounded state 1273 | MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2(0, -1.f), &grounded, &groundNormal); 1274 | if (!grounded){ 1275 | groundNormal = V2(0, 1.f); 1276 | } 1277 | 1278 | // Read input and set speed. 1279 | // Move in direction of ground to avoid "stepping" down slopes. 1280 | float ax = 0; // "Horizontal" acceleration 1281 | v2 groundDir = RotateMinus90Degrees(groundNormal); 1282 | if (globalInput.keyboard.arrowRight.isDown){ 1283 | ax = .7f; // Accelerate right 1284 | }else if (globalInput.keyboard.arrowLeft.isDown){ 1285 | ax = -.7f; // Accelerate left 1286 | }else{ 1287 | ax = Dot(gameState.rectSpeed, groundDir)*-.1f; // Decelerate. 1288 | } 1289 | gameState.rectSpeed += ax*groundDir; 1290 | 1291 | float minSpeedY = -30.f; 1292 | bool jumped = false; 1293 | if (grounded){ 1294 | if (globalInput.keyboard.arrowUp.isDown){ 1295 | gameState.rectSpeed.y = 10.f; // Jump 1296 | jumped = true; 1297 | }else{ 1298 | float diff = Abs(AngleDifference(PI/2, AngleOf(groundNormal))); 1299 | if (diff > PI/4 && gameState.rectSpeed.y > minSpeedY) 1300 | gameState.rectSpeed.y -= .3f; 1301 | } 1302 | }else if (gameState.rectSpeed.y > minSpeedY){ 1303 | gameState.rectSpeed.y -= .4f; // Gravity 1304 | } 1305 | 1306 | // Limit speed 1307 | float maxSpeed = 12.f; 1308 | if (Length(gameState.rectSpeed) > maxSpeed) 1309 | gameState.rectSpeed *= maxSpeed/Length(gameState.rectSpeed); 1310 | 1311 | float speedLength = Length(gameState.rectSpeed); 1312 | float toMove = speedLength; 1313 | bool bounced = false; 1314 | for(int i = 0; i < 6; i++){ // Artificial iteration limit 1315 | bool collided; 1316 | v2 collisionNormal; 1317 | v2 oldPos = gameState.rectPos; 1318 | v2 DEBUG_test = MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2FromLengthDir(toMove, AngleOf(gameState.rectSpeed)), &collided, &collisionNormal); 1319 | 1320 | gameState.rectPos = MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2FromLengthDir(toMove, AngleOf(gameState.rectSpeed)), &collided, &collisionNormal); 1321 | toMove -= Length(gameState.rectPos - oldPos); 1322 | 1323 | if (!collided) 1324 | break; 1325 | 1326 | v2 effectiveSpeed = V2FromLengthDir(speedLength, AngleOf(gameState.rectSpeed)); 1327 | if (Dot(effectiveSpeed, collisionNormal) < -9.f && Dot(collisionNormal, V2(0, 1)) < .4f) { 1328 | // Hit a wall hard: bounce! 1329 | gameState.rectSpeed = (gameState.rectSpeed - 2*Dot(gameState.rectSpeed, collisionNormal)*collisionNormal)*.4f; 1330 | bounced = true; 1331 | }else{ 1332 | v2 prevSpeed = gameState.rectSpeed; 1333 | gameState.rectSpeed = gameState.rectSpeed - Dot(gameState.rectSpeed, collisionNormal)*collisionNormal; 1334 | 1335 | if (Abs(collisionNormal.y) > .8f) // The edge is very horizontal 1336 | if (Abs(Dot(Normalize(prevSpeed), collisionNormal)) > .5f) // and we're going quite perpendicular to the edge 1337 | break; // Don't slide. 1338 | 1339 | if (Abs(Dot(Normalize(prevSpeed), collisionNormal)) > .9f) // Going very perpendicular to the edge 1340 | break; // Don't slide. 1341 | } 1342 | if (toMove < .1f) 1343 | break; 1344 | } 1345 | 1346 | // Don't fly off ascending slopes! 1347 | 1348 | if (true){ // Set to false to disable sticking to ground. 1349 | // Get the grounded state after moving 1350 | bool newGrounded; 1351 | v2 newGroundNormal; 1352 | MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2(0, -1.f), &newGrounded, &newGroundNormal); 1353 | v2 speedDir = Normalize(gameState.rectSpeed); 1354 | if (!bounced && !jumped && grounded && !newGrounded && Abs(speedDir.y) < .95f){ // If stopped being grounded, and not travelling extremely vertically... 1355 | // We'll try to move the entity down. 1356 | float maxDistance = 40.f; // The max distance we'll move down (independent of speed) 1357 | float maxSlope = 3.f; // This further restricts maxDistance based on the horizontal speed. 1358 | float deltaX = Abs(gameState.rectPos.x - oldPos.x); 1359 | float finalMaxDistance = Min(maxDistance, deltaX*maxSlope); 1360 | 1361 | bool foundGround; 1362 | v2 newPos = MoveRectangle(gameState.rectPos, gameState.rectDim/2, V2(0, -finalMaxDistance), &foundGround, &newGroundNormal); 1363 | if (foundGround && deltaX){ 1364 | float prevY = oldPos.y; 1365 | bool tooMuchSlope = false; 1366 | int steps = Ceil(deltaX/2); 1367 | for(int i = 0; i < steps; i ++){ 1368 | float t = (float)(i + 1)/(float)steps; 1369 | v2 p = {Lerp(oldPos.x, gameState.rectPos.x, t), 1370 | Lerp(oldPos.y, gameState.rectPos.y, t)}; 1371 | finalMaxDistance = Min(maxDistance, t*deltaX*maxSlope); 1372 | v2 intermediatePos = MoveRectangle(p, gameState.rectDim/2, V2(0, -finalMaxDistance), &foundGround, &newGroundNormal); 1373 | 1374 | float dy = intermediatePos.y - prevY; 1375 | float stepSize = (gameState.rectPos.x - oldPos.x)/(float)steps; 1376 | float slope = Abs(dy)/stepSize; 1377 | if (slope > maxSlope){ 1378 | tooMuchSlope = true; 1379 | break; 1380 | } 1381 | prevY = intermediatePos.y; 1382 | } 1383 | if (!tooMuchSlope){ 1384 | gameState.rectPos = newPos; 1385 | gameState.rectSpeed = gameState.rectSpeed - Dot(gameState.rectSpeed, newGroundNormal)*newGroundNormal; 1386 | } 1387 | } 1388 | } 1389 | } 1390 | } 1391 | 1392 | 1393 | 1394 | // 1395 | // Render 1396 | // 1397 | glViewport(0,0, windowDim.x, windowDim.y); 1398 | 1399 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 1400 | glEnable(GL_BLEND); 1401 | 1402 | glMatrixMode(GL_MODELVIEW); 1403 | glLoadIdentity(); 1404 | 1405 | glMatrixMode(GL_PROJECTION); 1406 | float a = (windowDim.x ? 2.0f/windowDim.x : 1.f); 1407 | float b = (windowDim.y ? 2.0f/windowDim.y : 1.f); 1408 | float proj[] = { a, 0, 0, 0, 1409 | 0, b, 0, 0, 1410 | 0, 0, 1.f, 0, 1411 | -1.f, -1.0f, 0, 1.f }; 1412 | glLoadMatrixf(proj); 1413 | 1414 | 1415 | glClearColor(.8f, .82f, .7f, 1.f); 1416 | glClear(GL_COLOR_BUFFER_BIT); 1417 | 1418 | // 1419 | // Render world 1420 | // 1421 | 1422 | // Draw walls (fill) 1423 | glBegin(GL_TRIANGLES); 1424 | for(int i = 0; i < gameState.numWalls; i++){ 1425 | glColor4f(1.f, .32f, .4f, .8f); 1426 | glVertex2f((gameState.walls[i].p[0].x - viewPos.x)*gameState.cameraScale, (gameState.walls[i].p[0].y - viewPos.y)*gameState.cameraScale); 1427 | glVertex2f((gameState.walls[i].p[1].x - viewPos.x)*gameState.cameraScale, (gameState.walls[i].p[1].y - viewPos.y)*gameState.cameraScale); 1428 | glVertex2f((gameState.walls[i].p[2].x - viewPos.x)*gameState.cameraScale, (gameState.walls[i].p[2].y - viewPos.y)*gameState.cameraScale); 1429 | } 1430 | glEnd(); 1431 | 1432 | // Draw walls (outline) 1433 | glBegin(GL_LINES); 1434 | for(int i = 0; i < gameState.numWalls; i++){ 1435 | wall *w = &gameState.walls[i]; 1436 | glColor4f(0.02f, 0.f, 0.1f, .8f); 1437 | glVertex2f((w->p[0].x - viewPos.x)*gameState.cameraScale, (w->p[0].y - viewPos.y)*gameState.cameraScale); 1438 | glVertex2f((w->p[1].x - viewPos.x)*gameState.cameraScale, (w->p[1].y - viewPos.y)*gameState.cameraScale); 1439 | 1440 | glVertex2f((w->p[1].x - viewPos.x)*gameState.cameraScale, (w->p[1].y - viewPos.y)*gameState.cameraScale); 1441 | glVertex2f((w->p[2].x - viewPos.x)*gameState.cameraScale, (w->p[2].y - viewPos.y)*gameState.cameraScale); 1442 | 1443 | glVertex2f((w->p[2].x - viewPos.x)*gameState.cameraScale, (w->p[2].y - viewPos.y)*gameState.cameraScale); 1444 | glVertex2f((w->p[0].x - viewPos.x)*gameState.cameraScale, (w->p[0].y - viewPos.y)*gameState.cameraScale); 1445 | 1446 | // Normals 1447 | //glColor4f(1.f, 1.f, 0.1f, .8f); 1448 | //float d = 15.f; 1449 | //glVertex2f(((w->p[0].x + w->p[1].x)/2 - viewPos.x)*gameState.cameraScale, ((w->p[0].y + w->p[1].y)/2 - viewPos.y)*gameState.cameraScale); 1450 | //glVertex2f(((w->p[0].x + w->p[1].x)/2 + d*w->normals[0].x - viewPos.x)*gameState.cameraScale, ((w->p[0].y + w->p[1].y)/2 + d*w->normals[0].y - viewPos.y)*gameState.cameraScale); 1451 | 1452 | //glVertex2f(((w->p[1].x + w->p[2].x)/2 - viewPos.x)*gameState.cameraScale, ((w->p[1].y + w->p[2].y)/2 - viewPos.y)*gameState.cameraScale); 1453 | //glVertex2f(((w->p[1].x + w->p[2].x)/2 + d*w->normals[1].x - viewPos.x)*gameState.cameraScale, ((w->p[1].y + w->p[2].y)/2 + d*w->normals[1].y - viewPos.y)*gameState.cameraScale); 1454 | 1455 | //glVertex2f(((w->p[2].x + w->p[0].x)/2 - viewPos.x)*gameState.cameraScale, ((w->p[2].y + w->p[0].y)/2 - viewPos.y)*gameState.cameraScale); 1456 | //glVertex2f(((w->p[2].x + w->p[0].x)/2 + d*w->normals[2].x - viewPos.x)*gameState.cameraScale, ((w->p[2].y + w->p[0].y)/2 + d*w->normals[2].y - viewPos.y)*gameState.cameraScale); 1457 | } 1458 | glEnd(); 1459 | 1460 | 1461 | // Draw circle 1462 | glColor4f(0.70f, 0.40f, .83f, .85f); 1463 | if (CircleWallCollision(gameState.circlePos, gameState.circleRadius)) 1464 | glColor4f(1.f, 0, 0, .8f); 1465 | glBegin(GL_TRIANGLES); 1466 | for(int i = 0; i < 32; i++){ 1467 | glVertex2f((gameState.circlePos.x - viewPos.x)*gameState.cameraScale, (gameState.circlePos.y - viewPos.y)*gameState.cameraScale); 1468 | glVertex2f((gameState.circlePos.x + gameState.circleRadius*Cos(i*2*PI/32.f) - viewPos.x)*gameState.cameraScale, (gameState.circlePos.y - gameState.circleRadius*Sin(i*2*PI/32.f) - viewPos.y)*gameState.cameraScale); 1469 | glVertex2f((gameState.circlePos.x + gameState.circleRadius*Cos((i + 1)*2*PI/32.f) - viewPos.x)*gameState.cameraScale, (gameState.circlePos.y - gameState.circleRadius*Sin((i + 1)*2*PI/32.f) - viewPos.y)*gameState.cameraScale); 1470 | } 1471 | glEnd(); 1472 | 1473 | // Draw rect 1474 | glColor4f(0.20f, 0.24f, 0.93f, .8f); 1475 | if (RectangleWallCollision(gameState.rectPos - gameState.rectDim/2, gameState.rectPos - gameState.rectDim/2 + gameState.rectDim)){ 1476 | glColor4f(.73f, .1f, .1f, .8f); 1477 | } 1478 | //glColor4f(0.f, 0.f, 0.f, .8f); 1479 | glBegin(GL_QUADS); 1480 | glVertex2f((gameState.rectPos.x - gameState.rectDim.x/2 - viewPos.x)*gameState.cameraScale, (gameState.rectPos.y - gameState.rectDim.y/2 - viewPos.y)*gameState.cameraScale); 1481 | glVertex2f((gameState.rectPos.x + gameState.rectDim.x/2 - viewPos.x)*gameState.cameraScale, (gameState.rectPos.y - gameState.rectDim.y/2 - viewPos.y)*gameState.cameraScale); 1482 | glVertex2f((gameState.rectPos.x + gameState.rectDim.x/2 - viewPos.x)*gameState.cameraScale, (gameState.rectPos.y + gameState.rectDim.y/2 - viewPos.y)*gameState.cameraScale); 1483 | glVertex2f((gameState.rectPos.x - gameState.rectDim.x/2 - viewPos.x)*gameState.cameraScale, (gameState.rectPos.y + gameState.rectDim.y/2 - viewPos.y)*gameState.cameraScale); 1484 | glEnd(); 1485 | 1486 | 1487 | // Draw circle speed 1488 | { 1489 | glColor4f(0, 0, 0, .7f); 1490 | glBegin(GL_LINES); 1491 | glVertex2f((gameState.circlePos.x - viewPos.x)*gameState.cameraScale, (gameState.circlePos.y - viewPos.y)*gameState.cameraScale); 1492 | float f = 3.f; 1493 | glVertex2f((gameState.circlePos.x + f*gameState.circleSpeed.x - viewPos.x)*gameState.cameraScale, (gameState.circlePos.y + f*gameState.circleSpeed.y - viewPos.y)*gameState.cameraScale); 1494 | glEnd(); 1495 | } 1496 | // Draw rect speed 1497 | { 1498 | glColor4f(0, 0, 0, .7f); 1499 | glBegin(GL_LINES); 1500 | glVertex2f((gameState.rectPos.x - viewPos.x)*gameState.cameraScale, (gameState.rectPos.y - viewPos.y)*gameState.cameraScale); 1501 | float f = 3.f; 1502 | glVertex2f((gameState.rectPos.x + f*gameState.rectSpeed.x - viewPos.x)*gameState.cameraScale, (gameState.rectPos.y + f*gameState.rectSpeed.y - viewPos.y)*gameState.cameraScale); 1503 | glEnd(); 1504 | } 1505 | 1506 | // Draw placing wall 1507 | if (gameState.numWalls < ArrayCount(gameState.walls)){ 1508 | wall *w = &gameState.walls[gameState.numWalls]; 1509 | if (placingWallPointIndex > 0 || globalInput.keyboard.control.isDown){ 1510 | // Highlight new points 1511 | int numPointsToHighlight = placingWallPointIndex + 1; 1512 | for(int i = 0; i < numPointsToHighlight; i++){ 1513 | glBegin(GL_QUADS); 1514 | glColor4f(1.f, .7f, .0f, .8f); 1515 | float r = 10.f; 1516 | glVertex2f((w->p[i].x - viewPos.x)*gameState.cameraScale - r, (w->p[i].y - viewPos.y)*gameState.cameraScale - r); 1517 | glVertex2f((w->p[i].x - viewPos.x)*gameState.cameraScale + r, (w->p[i].y - viewPos.y)*gameState.cameraScale - r); 1518 | glVertex2f((w->p[i].x - viewPos.x)*gameState.cameraScale + r, (w->p[i].y - viewPos.y)*gameState.cameraScale + r); 1519 | glVertex2f((w->p[i].x - viewPos.x)*gameState.cameraScale - r, (w->p[i].y - viewPos.y)*gameState.cameraScale + r); 1520 | glEnd(); 1521 | } 1522 | // Show line between the first two points. 1523 | if (placingWallPointIndex == 1){ 1524 | glBegin(GL_LINES); 1525 | glColor4f(.9f, .7f, .1f, .8f); 1526 | glVertex2f((w->p[0].x - viewPos.x)*gameState.cameraScale, (w->p[0].y - viewPos.y)*gameState.cameraScale); 1527 | glVertex2f((w->p[1].x - viewPos.x)*gameState.cameraScale, (w->p[1].y - viewPos.y)*gameState.cameraScale); 1528 | glEnd(); 1529 | } 1530 | // Show triangle when placing the third point. 1531 | if (placingWallPointIndex == 2){ 1532 | glBegin(GL_TRIANGLES); 1533 | glColor4f(1.f, .7f, .2f, .5f); 1534 | glVertex2f((w->p[0].x - viewPos.x)*gameState.cameraScale, (w->p[0].y - viewPos.y)*gameState.cameraScale); 1535 | glVertex2f((w->p[1].x - viewPos.x)*gameState.cameraScale, (w->p[1].y - viewPos.y)*gameState.cameraScale); 1536 | glVertex2f((w->p[2].x - viewPos.x)*gameState.cameraScale, (w->p[2].y - viewPos.y)*gameState.cameraScale); 1537 | glEnd(); 1538 | } 1539 | } 1540 | } 1541 | 1542 | 1543 | glFlush(); 1544 | SwapBuffers(dc); 1545 | 1546 | 1547 | 1548 | // 1549 | // Sleep to render at 60 FPS 1550 | // 1551 | while(true){ 1552 | LARGE_INTEGER newFrameTime = GetCurrentTimeCounter(); 1553 | float timeElapsed = GetSecondsElapsed(lastFrameTime, newFrameTime); 1554 | if (timeElapsed > 1/60.f){ 1555 | lastFrameTime = newFrameTime; 1556 | break; 1557 | } 1558 | if (1/60.f - timeElapsed > 0.005f){ 1559 | Sleep(1); 1560 | } 1561 | } 1562 | 1563 | // Reset button input. 1564 | for(int i = 0; i < ArrayCount(globalInput.keyboard.asArray); i++){ 1565 | globalInput.keyboard.asArray[i].transitionCount = 0; 1566 | } 1567 | for(int i = 0; i < ArrayCount(globalInput.mouseButtons); i++){ 1568 | globalInput.mouseButtons[i].transitionCount = 0; 1569 | } 1570 | } 1571 | 1572 | if (CREATE_CONSOLE){ 1573 | FreeConsole(); 1574 | } 1575 | 1576 | wglMakeCurrent(NULL, NULL); 1577 | ReleaseDC(window, dc); 1578 | wglDeleteContext(rc); 1579 | DestroyWindow(window); 1580 | 1581 | return 0; 1582 | } 1583 | --------------------------------------------------------------------------------