{"id":1644,"date":"2023-12-02T01:49:46","date_gmt":"2023-12-02T01:49:46","guid":{"rendered":"https:\/\/timallanwheeler.com\/blog\/?p=1644"},"modified":"2024-07-20T03:23:25","modified_gmt":"2024-07-20T03:23:25","slug":"a-side-scroller-physics-system","status":"publish","type":"post","link":"https:\/\/timallanwheeler.com\/blog\/2023\/12\/02\/a-side-scroller-physics-system\/","title":{"rendered":"A Side Scroller Physics System"},"content":{"rendered":"\n<p>With <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/08\/31\/toom-with-non-euclidean-geometry\/\">TOOM<\/a> <span id=\"su_tooltip_69e9e42c94073_button\" class=\"su-tooltip-button su-tooltip-button-outline-yes\" aria-describedby=\"su_tooltip_69e9e42c94073\" data-settings='{\"position\":\"top\",\"behavior\":\"hover\",\"hideDelay\":0}' tabindex=\"0\">&#8216;<em>done&#8217;<\/em><\/span><span style=\"display:none;z-index:100\" id=\"su_tooltip_69e9e42c94073\" class=\"su-tooltip\" role=\"tooltip\"><span class=\"su-tooltip-inner su-tooltip-shadow-no\" style=\"z-index:100;background:#222222;color:#FFFFFF;font-size:16px;border-radius:5px;text-align:left;max-width:300px;line-height:1.25\"><span class=\"su-tooltip-title\"><\/span><span class=\"su-tooltip-content su-u-trim\">Or rather, as done as I want it to be.<\/span><\/span><span id=\"su_tooltip_69e9e42c94073_arrow\" class=\"su-tooltip-arrow\" style=\"z-index:100;background:#222222\" data-popper-arrow><\/span><\/span>, I found myself wanting to try something new. So, I started a new vscode workspace and embarked on a new side project &#8211; a 2D side scroller.<\/p>\n\n\n\n<p>My overarching goal, once again, is to explore and take it as far as I find it interesting. That being said, I want to overcome the challenge of building a stable game with general 2D physics. By that I mean, &#8220;vectorial&#8221; physics of the sort used by Braid, with arbitrary 2D level geometry:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"512\" height=\"261\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-7.png\" alt=\"\" class=\"wp-image-1654\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-7.png 512w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-7-300x153.png 300w\" sizes=\"auto, (max-width: 512px) 100vw, 512px\" \/><\/figure>\n<\/div>\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"512\" height=\"261\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-8.png\" alt=\"\" class=\"wp-image-1655\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-8.png 512w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-8-300x153.png 300w\" sizes=\"auto, (max-width: 512px) 100vw, 512px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\"><a href=\"http:\/\/braid-game.com\/\">Braid<\/a> level game view and level editor view (image from <a href=\"http:\/\/higherorderfun.com\/blog\/2012\/05\/20\/the-guide-to-implementing-2d-platformers\/\">Higher-Order Fun<\/a>) <\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>One reason for this goal is that I&#8217;ve tried to noodle it out before, but was surprised to find a significant lack of resources. In fact, I had to laugh out loud when I read <a href=\"http:\/\/higherorderfun.com\/blog\/2012\/05\/20\/the-guide-to-implementing-2d-platformers\/\">this excellent heavily cited blog post about 2D platformers<\/a>. It covers tile-based and rastered platforms in great detail, only to leave vectorial platformers with a vague &#8220;you do it yourself&#8221; and a warning not to use a physics engine. Well, challenge accepted! Let&#8217;s get into it!<\/p>\n\n\n\n<p>There are a few other things I want to learn from this adventure. I plan to eventually use 3D GPU-accelerated graphics. Its worth knowing how that works, and the work on TOOM convinced me that drawing with the CPU is super slow. The aesthetic I have in mind is pretty low-poly, so while I&#8217;d normally go with pixel art because that&#8217;s all I can reasonably attempt myself, I nevertheless want to give 3D a try. The other big reason to go with 3D is that I can build an animation system and animate characters once rather than 8x for the various viewing angles. I was wanting to build a skeletal animation system, but felt that 2D skeletal animation systems very easily feel flat without serious artistic effort.<\/p>\n\n\n\n<p>The general image I have in my mind is a souls-like 2D platformer with movement and combat somewhat similar to playing Link in Super Smash, except with some additional movement mechanics like ladders and other climbable surfaces and no bubble shield. It&#8217;ll probably never be a full game but maybe I can make a proper level and boss at the end.<\/p>\n\n\n\n<p>I&#8217;ll be using C++ and <a href=\"https:\/\/code.visualstudio.com\/\">vscode<\/a> for this, on Linux. I&#8217;ll try to minimize library use, but will leverage <a href=\"https:\/\/www.libsdl.org\/\">SDL2<\/a> as my platform library and probably <a href=\"https:\/\/www.opengl.org\/\">OpenGL<\/a> for 3D graphics. I love <a href=\"https:\/\/github.com\/ocornut\/imgui\">ImGui<\/a> and will use that for dev UI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What I tried first Didn&#8217;t Work<\/h2>\n\n\n\n<p>When I first started, I tried to go with an object system where the world is made of convex polygonal colliders. Seems reasonable:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"743\" height=\"432\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/polygon_collision_fail.gif\" alt=\"\" class=\"wp-image-1658\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The player (and one enemy) have colliders represented by these six-sided shapes. The white polygons are static geometry. So far so good.<\/p>\n\n\n\n<p>When an entity moves, it ticks forward by \\(\\Delta t\\). I could have done this the boring way by:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Moving the entity its full distance<\/li>\n\n\n\n<li>Checking for collisions<\/li>\n\n\n\n<li>Shunting the entity out of any colliding polygons<\/li>\n<\/ol>\n\n\n\n<p>I&#8217;ve<a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/01\/27\/2d-player-collision-against-static-geometry\/\"> written about<\/a> why this is a bad idea. In short, this approach can cause tunneling or other weird physics effects, especially when the entity is moving fast or when there are multiple intersecting bodies.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"335\" height=\"292\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-9.png\" alt=\"\" class=\"wp-image-1660\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-9.png 335w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-9-300x261.png 300w\" sizes=\"auto, (max-width: 335px) 100vw, 335px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The system I implemented sought to instead find the <em>first<\/em> collision. This is the same as taking the source polygon and sweeping it along the direction of travel and finding the longest sweep \\(\\delta t \\in [0, \\Delta t]\\) that does not intersect anything:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"286\" height=\"221\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-10.png\" alt=\"\" class=\"wp-image-1661\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>This sweeping method is harder to formulate. We aren&#8217;t really colliding the player&#8217;s collider with each static collider. Instead, we are colliding the swept player collider <span id=\"su_tooltip_69e9e42c9414e_button\" class=\"su-tooltip-button su-tooltip-button-outline-yes\" aria-describedby=\"su_tooltip_69e9e42c9414e\" data-settings='{\"position\":\"top\",\"behavior\":\"hover\",\"hideDelay\":0}' tabindex=\"0\"><em>with each static collider<\/em><\/span><span style=\"display:none;z-index:100\" id=\"su_tooltip_69e9e42c9414e\" class=\"su-tooltip\" role=\"tooltip\"><span class=\"su-tooltip-inner su-tooltip-shadow-no\" style=\"z-index:100;background:#222222;color:#FFFFFF;font-size:16px;border-radius:5px;text-align:left;max-width:300px;line-height:1.25\"><span class=\"su-tooltip-title\"><\/span><span class=\"su-tooltip-content su-u-trim\">We can use a broad collision phase narrow down to a small number of local potential colliders.<\/span><\/span><span id=\"su_tooltip_69e9e42c9414e_arrow\" class=\"su-tooltip-arrow\" style=\"z-index:100;background:#222222\" data-popper-arrow><\/span><\/span>, but we still want to back out the maximum step we can take in our direction of motion. So we&#8217;re effectively sweeping the player collider along \\(\\Delta t \\cdot \\boldsymbol{v}\\) and then checking for collisions, and we have to find the smallest \\(\\delta t\\) such that the volume swept along \\(\\delta t \\cdot \\boldsymbol{v}\\) does not collide.<\/p>\n\n\n\n<p>The way I implemented that was to fall back to our trusty strategy of using the <span id=\"su_tooltip_69e9e42c941d1_button\" class=\"su-tooltip-button su-tooltip-button-outline-yes\" aria-describedby=\"su_tooltip_69e9e42c941d1\" data-settings='{\"position\":\"top\",\"behavior\":\"hover\",\"hideDelay\":0}' tabindex=\"0\"><em>Minkowski sum<\/em><\/span><span style=\"display:none;z-index:100\" id=\"su_tooltip_69e9e42c941d1\" class=\"su-tooltip\" role=\"tooltip\"><span class=\"su-tooltip-inner su-tooltip-shadow-no\" style=\"z-index:100;background:#222222;color:#FFFFFF;font-size:16px;border-radius:5px;text-align:left;max-width:300px;line-height:1.25\"><span class=\"su-tooltip-title\"><\/span><span class=\"su-tooltip-content su-u-trim\">Which is really a Minkowski difference.<\/span><\/span><span id=\"su_tooltip_69e9e42c941d1_arrow\" class=\"su-tooltip-arrow\" style=\"z-index:100;background:#222222\" data-popper-arrow><\/span><\/span>. That is, we can expand the other polygon by the player&#8217;s collision volume first, and then all we have to do is check whether the line segment between the player&#8217;s starting location and that location shifted by \\(\\Delta t \\cdot \\boldsymbol{v}\\) intersects with the polygon:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"412\" height=\"291\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-11.png\" alt=\"\" class=\"wp-image-1665\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-11.png 412w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-11-300x212.png 300w\" sizes=\"auto, (max-width: 412px) 100vw, 412px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>That mostly worked. It just has a lot of trouble with corner cases. For example, when the player is standing on a surface and we&#8217;re traveling along it. We don&#8217;t want <a href=\"https:\/\/en.wikipedia.org\/wiki\/Floating-point_error_mitigation#:~:text=Floating%2Dpoint%20error%20mitigation%20is,best%2C%20can%20only%20be%20managed.\">floating point errors<\/a> to accidentally place the player inside the supporting polygon. This was particularly problematic in the gameplay gif above, where the player ends up wedged between two squares and slides through one of them.<\/p>\n\n\n\n<p>I had a lot of trouble with walking. I wanted the player to be able to walk across colliders along the floor without treating it like a ramp and launching off for a bit: <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"457\" height=\"199\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-12.png\" alt=\"\" class=\"wp-image-1670\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-12.png 457w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-12-300x131.png 300w\" sizes=\"auto, (max-width: 457px) 100vw, 457px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>This means we have to do some hacky checks to figure out when we&#8217;re walking along a polygon and reach its end, and may need to transition to walking along a new polygon. I tried getting this all to work reliably, and got pretty far, but ultimately the number of hacks I had to add made the system untenable and left a bad taste in my mouth.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Redesign<\/h2>\n\n\n\n<p>The fundamental problem is that my previous system was fully continuous, but that continuity inherently had issues with floating point effects when the player is supported by geometry. In these cases, we want something more discrete that can concretely tell us which side of the geometry we are on. Furthermore, I wanted something that could more easily tell me whether, when walking along a surface, there was another surface to walk on after passing the end of the current segment. These thoughts lead me to the trusty Delaunay mesh data structure.<\/p>\n\n\n\n<p>I&#8217;ve written blog posts that use constrained Delaunay meshes (aka <a href=\"https:\/\/en.wikipedia.org\/wiki\/Doubly_connected_edge_list\">DCEL<\/a>s) a <a href=\"https:\/\/timallanwheeler.com\/blog\/2022\/08\/03\/delaunay-triangulations\/\">few<\/a> <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/01\/27\/2d-player-collision-against-static-geometry\/\">times<\/a> <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/07\/29\/taking-toom-off-grid-without-bsps\/\">now<\/a>. Now that I know this data structure exists, I keep finding uses for it.<\/p>\n\n\n\n<p>Delaunay meshes are triangle meshes in which we try to keep the triangles as equilateral as possible (to avoid very fractured \/ long and narrow tiles). Constrained Delaunay meshes are the same thing, except we force certain edges to exist. This makes these meshes useful for things like representing map geometry, where we force walls to be edges in the mesh:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"701\" height=\"442\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/09\/Selection_685.png\" alt=\"\" class=\"wp-image-1487\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/09\/Selection_685.png 701w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/09\/Selection_685-300x189.png 300w\" sizes=\"auto, (max-width: 701px) 100vw, 701px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">The E1M1 map from DOOM, after I\u2019ve loaded it into a constrained Delaunay mesh<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>I actually advised that one use constrained Delaunay meshes for 2D player movement in <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/01\/27\/2d-player-collision-against-static-geometry\/\">this previous blog post<\/a>. I hadn&#8217;t initially followed my own suggestion because the mesh is dependent on both the level geometry and the player collider (each polygon is expanded by the player collider via a Minkowski sum). If the player ducks and their collider shrinks, then we have to change our mesh. If an enemy has a different sized collider, then it should use a different mesh. If a piece of level geometry moves, such as a platform or door, then we need to change the mesh. All of these problems are still problems, but I&#8217;ll have to find workarounds for them. Nevertheless, I&#8217;m going with exactly what I spelled out in that earlier blog post.<\/p>\n\n\n\n<p>My new plan was to keep track of both the player&#8217;s position and the triangle the collision mesh that they find themselves in. The collision mesh already accounts for the player&#8217;s collision volume, so we need only keep track of their center position. When they move, we check to see if their motion would take then out of their containing triangle, and if it does, we handle collisions (if necessary) at the crossed edge.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"743\" height=\"432\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/polygon_collision_win.gif\" alt=\"\" class=\"wp-image-1673\"\/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">Here we see the updated system, along with the collision mesh.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Building the Mesh<\/h2>\n\n\n\n<p>Since I&#8217;ve covered how the movement works before, I&#8217;d rather focus on how we get the mesh.<\/p>\n\n\n\n<p>This mesh is obtained by expanding each polygon by the player&#8217;s collision volume, and then adding all resulting edges to the Delaunay mesh. These edges are constrained to ensure that they remain in the final mesh (otherwise, we might flip edges in order to make them more equilateral). The main difficulty arises in the fact that the edges of the expanded polygons may intersect, and we nevertheless have to support them in our final mesh:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"535\" height=\"294\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/Selection_751.png\" alt=\"\" class=\"wp-image-1676\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/Selection_751.png 535w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/Selection_751-300x165.png 300w\" sizes=\"auto, (max-width: 535px) 100vw, 535px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">For example, these expanded polygons overlap.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>I had previously tackled this using polygon unions via <a href=\"https:\/\/angusj.com\/clipper2\/Docs\/Overview.htm\">Clipper2<\/a>. I could do that again, but want to avoid the loss of information that comes with replacing two or more overlapping polygons with a single polygon. For example, I might want to know which feature my player is standing on. For example, I may end up wanting to support removing objects. Plus, figuring out my own solution is a reward unto itself.<\/p>\n\n\n\n<p>What I ended up doing was updating my method for constraining edges in a DCEL. During the development of TOOM, I had made it possible to load the DOOM level geometry and constrain the appropriate edges. Here we just added sides as we went, and constrained them after adding them:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">for (Linedef linedef : linedefs) {\n     \/\/ Insert vertex a and b.\n     for (i16 i_vertex : {linedef.i_vertex_start, linedef.i_vertex_end}) {\n         const common::Vec2f&amp; v = doom_vertices[i_vertex];\n         InsertVertexResult res = mesh.InsertVertex(v);\n         if (res.category == IN_FACE || res.category == ON_EDGE) {\n             mesh.EnforceLocallyDelaunay(res.i_qe);\n             vertex_indices[i_vertex] = mesh.GetVertexIndex(res.i_qe);\n         }\n     }\n\n     \/\/ Now ensure that there is a line between A and B.\n     \/\/ If there is not, flip edges until there is.\n     QuarterEdgeIndex qe_a =\n        mesh.GetQuarterEdge(vertex_indices[linedef.i_vertex_start]);\n     QuarterEdgeIndex qe_b =\n        mesh.GetQuarterEdge(vertex_indices[linedef.i_vertex_end]);\n     QuarterEdgeIndex qe_ab = mesh.EnforceEdge(qe_a, qe_b);\n     mesh.ConstrainEdge(qe_ab);\n\n     \/\/ sidedef and passability stuff...\n}<\/code><\/pre>\n\n\n\n<p>For example, we might add the vertices for A and B, and then end up with a mesh that is locally Delaunay but does not have an edge between A and B:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"389\" height=\"292\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-14.png\" alt=\"\" class=\"wp-image-1681\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-14.png 389w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-14-300x225.png 300w\" sizes=\"auto, (max-width: 389px) 100vw, 389px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The call to EnforceEdge then flips CD in order to get the edge we want:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"408\" height=\"292\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-15.png\" alt=\"\" class=\"wp-image-1682\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-15.png 408w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-15-300x215.png 300w\" sizes=\"auto, (max-width: 408px) 100vw, 408px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>This approach worked for the DOOM maps because the sides there never crossed over one another. For example, if CD is another enforced edge, there was no way to also enforce AB by flipping edges:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"371\" height=\"281\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-16.png\" alt=\"\" class=\"wp-image-1683\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-16.png 371w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-16-300x227.png 300w\" sizes=\"auto, (max-width: 371px) 100vw, 371px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>I needed a more general way to specify two points A and B and then adjust the mesh such that those two points are joined by a straight line, potentially via multiple colinear segments.<\/p>\n\n\n\n<p>The updated algorithm has an outer loop that alternately tries tackling the problem from A to B and from B to A. As soon as it finds or produces a connection, it terminates. If it fails to make progress from both directions, it exits out.<\/p>\n\n\n\n<p>Attempting to make progress from A to B starts by finding the edge from A that is as close as possible in a counter-clockwise \/ right-hand sense to the desired segment AB:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"322\" height=\"259\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-17.png\" alt=\"\" class=\"wp-image-1684\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-17.png 322w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-17-300x241.png 300w\" sizes=\"auto, (max-width: 322px) 100vw, 322px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>We&#8217;ll call the other side of this segment C. If C = B, we are done, as then AB already exists.<\/p>\n\n\n\n<p>It is possible that C lies along AB. When this happens, we replace A with C and return to the outer loop:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"435\" height=\"300\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-18.png\" alt=\"\" class=\"wp-image-1685\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-18.png 435w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-18-300x207.png 300w\" sizes=\"auto, (max-width: 435px) 100vw, 435px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>If we haven&#8217;t returned yet, then AC is counter-clockwise of AB. We need to flip CD, where D is on the other side of AB from C. If CD is not constrained, we can flip it.<\/p>\n\n\n\n<p>The most interesting case occurs when CD is constrained. We cannot flip CD because it is fixed &#8211; we want it to remain an edge in the final mesh. So we instead introduce a new point E at the intersection of AB and CD, split CD at E, and add AE and EB:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"293\" height=\"283\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-20.png\" alt=\"\" class=\"wp-image-1688\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>We can see this in action in the TOOM mesh editor, updated with this method:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"743\" height=\"432\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/enforce_constrained_edge.gif\" alt=\"\" class=\"wp-image-1689\"\/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">Here we have a constrained edge <\/p>\n\n\n\n<p><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"743\" height=\"432\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/enforce_constrained_edge2.gif\" alt=\"\" class=\"wp-image-1690\"\/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">Here we have to flip or intersect multiple edges<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>So far I&#8217;m just building a mesh with constrained edges matching those of the expanded collision polygons, and only have a single such mesh to reflect the player&#8217;s collision polygon. I do not yet track which polygon corresponds to which triangle, nor do edges otherwise have any special properties. Nevertheless, what I have is sufficient for basic 2D player movement.<\/p>\n\n\n\n<p>The player (and any other entity) has a 2D position and a triangle face in which their position lies. The 2D position is continuous, but the triangle face is discrete. Having something discrete resolves the issue with floating point rounding &#8211; we know exactly which side of a line we are (supposed to be) on.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"572\" height=\"340\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-21.png\" alt=\"\" class=\"wp-image-1692\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-21.png 572w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-21-300x178.png 300w\" sizes=\"auto, (max-width: 572px) 100vw, 572px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Note that the constrained Delaunay mesh does not actually contain the white collider polygons:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"577\" height=\"298\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-22.png\" alt=\"\" class=\"wp-image-1693\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-22.png 577w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/image-22-300x155.png 300w\" sizes=\"auto, (max-width: 577px) 100vw, 577px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>I&#8217;ll probably talk about this more next time, but movement physics currently get processed in two different ways. If the moving entity is airborne, then they move according to <span id=\"su_tooltip_69e9e42c94247_button\" class=\"su-tooltip-button su-tooltip-button-outline-yes\" aria-describedby=\"su_tooltip_69e9e42c94247\" data-settings='{\"position\":\"top\",\"behavior\":\"hover\",\"hideDelay\":0}' tabindex=\"0\"><em>the standard laws of physics<\/em><\/span><span style=\"display:none;z-index:100\" id=\"su_tooltip_69e9e42c94247\" class=\"su-tooltip\" role=\"tooltip\"><span class=\"su-tooltip-inner su-tooltip-shadow-no\" style=\"z-index:100;background:#222222;color:#FFFFFF;font-size:16px;border-radius:5px;text-align:left;max-width:300px;line-height:1.25\"><span class=\"su-tooltip-title\"><\/span><span class=\"su-tooltip-content su-u-trim\">Apply acceleration, use velocity and delta t to get future position, sweep along the line segment, and check for triangle traversals.<\/span><\/span><span id=\"su_tooltip_69e9e42c94247_arrow\" class=\"su-tooltip-arrow\" style=\"z-index:100;background:#222222\" data-popper-arrow><\/span><\/span>. If we collide with a solid edge, we lose all velocity going into it and then continue on in our new direction. Colliding with a solid edge can result in the agent transitioning to the supported state.<\/p>\n\n\n\n<p>If the moving entity is standing or walking on a surface, i.e. is supported, we handle things a bit differently. Here their velocity is projected along the supporting surface. Any time they move past the end of the supporting surface, we find the next supporting edge (if there is one) and allow them to transition over. If they are moving fast enough or there is no supporting edge to connect to, they transition to being airborne.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"553\" height=\"217\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2023\/11\/walk_along_surface.gif\" alt=\"\" class=\"wp-image-1696\"\/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">The player walking across supporting surfaces<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Constrained Delaunay meshes resolved many of the tricky issues that arose with tackling 2D player movement for arbitrary terrain geometry. Knowing which face a player is in is a discrete fact that removes the issues with numeric imprecision in general polygon-on-polygon collisions, without having to resort to shunting. It also naturally allows us to leverage the mesh to query things like the next supporting face.  <\/p>\n\n\n\n<p>The code for the updated Delaunay mesh is available <a href=\"https:\/\/timallanwheeler.com\/data\/posts\/2023_12_sidescroller\/delaunay_mesh.zip\">here<\/a>.<\/p>\n\n\n\n<p>In the future I hope to support additional motion states, such as climbing on ladders and surfaces. I&#8217;ll want to add support for dropping down &#8220;through&#8221; a platform, wall slides and jumps, and some other basic sidescroller movement features. I want to add 3D GPU graphics, and with that, animations and combat. It turns out there is a lot that goes into a basic game.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>With TOOM , I found myself wanting to try something new. So, I started a new vscode workspace and embarked on a new side project &#8211; a 2D side scroller. My overarching goal, once again, is to explore and take it as far as I find it interesting. That being said, I want to overcome [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[10],"class_list":["post-1644","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-sidescroller"],"_links":{"self":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/1644","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/comments?post=1644"}],"version-history":[{"count":76,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/1644\/revisions"}],"predecessor-version":[{"id":2442,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/1644\/revisions\/2442"}],"wp:attachment":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/media?parent=1644"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/categories?post=1644"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/tags?post=1644"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}