tag:gamedevelopment.tutsplus.com,2005:/categories/tile-based-games Envato Tuts+ Game Development - Tile-Based Games 2017-12-20T12:00:00Z tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-29715 Unity 2D Tile-Based Isometric and Hexagonal 'Sokoban' Game <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29715/final_image/alt-sokoban-complete.png" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><p>In this tutorial, we will be converting a conventional 2D tile-based Sokoban game into isometric and hexagonal views. If you are new to isometric or hexagonal games, it may be overwhelming at first to try following through both of them at the same time. In that case, I recommend choosing isometric first and then coming back at a later stage for the hexagonal version.</p><p>We will be building on top of the earlier Unity tutorial: <a href="https://gamedevelopment.tutsplus.com/tutorials/unity-2d-tile-based-sokoban-game--cms-29714" rel="external" target="_blank">Unity 2D Tile-Based Sokoban Game</a>. Please go through the tutorial first as most of the code remains unchanged and all the core concepts remain the same. I'll also link to other tutorials explaining some of the underlying concepts.<br></p><p>The most important aspect in creating isometric or hexagonal versions from a 2D version is figuring out the positioning of the elements. We will use conversion methods based on equations to convert between the various coordinate systems.<br></p><p>This tutorial has two sections, one for the isometric version and the other for the hexagonal version.</p><h2> <span class="sectionnum">1.</span> Isometric Sokoban Game</h2><p>Let's dive right in to the isometric version once you have gone through the original tutorial. The image below shows how the isometric version would look, provided we use the same level information used in the original tutorial.</p><figure class="post_image"><img alt="the isometric version of the sokoban level" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29715/image/alt-sokoban-isometric.png"></figure><h3>Isometric View<br> </h3><p>Isometric theory, conversion equation, and implementation are explained in multiple tutorials on Envato Tuts+. An old Flash-based explanation can be found in <a href="https://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511" rel="external" target="_blank">this detailed tutorial</a>. I would recommend <a href="https://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-primer-for-game-developers-updated--cms-28392" rel="external" target="_blank">this Phaser-based tutorial</a> as it is more recent and future proof. <br></p><p>Although the scripting languages used in those tutorials are ActionScript 3 and JavaScript respectively, the theory is applicable everywhere, irrespective of programming languages. Essentially it boils down to these conversion equations which are to be used to convert 2D Cartesian coordinates to isometric coordinates or vice versa.</p><pre class="brush: javascript noskimlinks noskimwords">//Cartesian to isometric: isoX = cartX - cartY; isoY = (cartX + cartY) / 2; //Isometric to Cartesian: cartX = (2 * isoY + isoX) / 2; cartY = (2 * isoY - isoX) / 2;</pre><p>We will be using the following Unity function for the conversion to isometric coordinates.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 CartesianToIsometric(Vector2 cartPt){ Vector2 tempPt=new Vector2(); tempPt.x=cartPt.x-cartPt.y; tempPt.y=(cartPt.x+cartPt.y)/2; return (tempPt); }</pre><h3>Changes in Art</h3><p>We will be using the same level information to create our 2D array, <code class="inline">levelData</code>, which will drive the isometric representation. Most of the code will also remain the same, other than that specific to the isometric view. <br></p><p>The art, however, needs to have some changes with respect to the pivot points. Please refer to the image below and the explanation which follows.</p><figure class="post_image"><img alt="isometric sprites and their offsets" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29715/image/alt-sokoban-sprite-offsets.png"></figure><p>The <code class="inline">IsometricSokoban</code> game script uses modified sprites as <code class="inline">heroSprite</code>, <code class="inline">ballSprite</code>, and <code class="inline">blockSprite</code>. The image shows the new pivot points used for these sprites. This change gives the pseudo 3D look we are aiming for with the isometric view. The blockSprite is a new sprite which we add when we find an <code class="inline">invalidTile</code>. <br></p><p>It will help me explain the most important aspect of isometric games, depth sorting. Although the sprite is just a hexagon, we are considering it as a 3D cube where the pivot is situated at the middle of the bottom face of the cube.</p><h3>Changes in Code</h3><p>Please download the code shared through the linked git repository before proceeding further. The <code class="inline">CreateLevel</code> method has a few changes which deal with the scale and positioning of the tiles and the addition of the <code class="inline">blockTile</code>. The scale of the <code class="inline">tileSprite</code>, which is just a diamond shape image representing our ground tile, needs to be altered as below.</p><pre class="brush: csharp noskimlinks noskimwords">tile.transform.localScale=new Vector2(tileSize-1,(tileSize-1)/2);//size is critical for isometric shape</pre><p>This reflects the fact that an isometric tile will have a height of half of its width. The <code class="inline">heroSprite</code> and the <code class="inline">ballSprite</code> have a size of <code class="inline">tileSize/2</code>.<br></p><pre class="brush: csharp noskimlinks noskimwords">hero.transform.localScale=Vector2.one*(tileSize/2);//we use half the tilesize for occupants</pre><p>Wherever we find an <code class="inline">invalidTile</code>, we add a <code class="inline">blockTile</code> using the following code.</p><pre class="brush: csharp noskimlinks noskimwords">tile = new GameObject("block"+i.ToString()+"_"+j.ToString());//create new tile float rootThree=Mathf.Sqrt(3); float newDimension= 2*tileSize/rootThree; tile.transform.localScale=new Vector2(newDimension,tileSize);//we need to set some height sr = tile.AddComponent&lt;SpriteRenderer&gt;();//add a sprite renderer sr.sprite=blockSprite;//assign block sprite sr.sortingOrder=1;//this also need to have higher sorting order Color c= Color.gray; c.a=0.9f; sr.color=c; tile.transform.position=GetScreenPointFromLevelIndices(i,j);//place in scene based on level indices occupants.Add(tile, new Vector2(i,j));//store the level indices of block in dict</pre><p>The hexagon needs to be scaled differently to get the isometric look. This will not be an issue when the art is handled by artists. We are applying a slightly lower alpha value to the <code class="inline">blockSprite</code> so that we can see through it, which enables us to see the depth sorting properly. Notice that we are adding these tiles to the <code class="inline">occupants</code> dictionary as well, which will be used later for depth sorting.<br></p><p>The positioning of the tiles is done using the <code class="inline">GetScreenPointFromLevelIndices</code> method, which in turn uses the <code class="inline">CartesianToIsometric</code> conversion method explained earlier. The <code class="inline">Y</code> axis points in the opposite direction for Unity, which needs to be considered while adding the <code class="inline">middleOffset</code> to position the level in the middle of the screen.<br></p><pre class="brush: csharp noskimlinks noskimwords">Vector2 GetScreenPointFromLevelIndices(int row,int col){ //converting indices to position values, col determines x &amp; row determine y Vector2 tempPt=CartesianToIsometric(new Vector2(col*tileSize/2,row*tileSize/2));//removed the '-' inthe y part as axis correction can happen after coversion tempPt.x-=middleOffset.x;//we apply the offset outside the coordinate conversion to align the level in screen middle tempPt.y*=-1;//unity y axis correction tempPt.y+=middleOffset.y;//we apply the offset outside the coordinate conversion to align the level in screen middle return tempPt; }</pre><p>At the end of the <code class="inline">CreateLevel</code> method as well as at the end of the <code class="inline">TryMoveHero</code> method, we call the <code class="inline">DepthSort</code> method. Depth sorting is the most important aspect of an isometric implementation. Essentially, we determine which tiles go behind or in front of other tiles in the level. The <code class="inline">DepthSort</code> method is as shown below.</p><pre class="brush: csharp noskimlinks noskimwords">private void DepthSort() { int depth=1; SpriteRenderer sr; Vector2 pos=new Vector2(); for (int i = 0; i &lt; rows; i++) { for (int j = 0; j &lt; cols; j++) { int val=levelData[i,j]; if(val!=groundTile &amp;&amp; val!=destinationTile){//a tile which needs depth sorting pos.x=i; pos.y=j; GameObject occupant=GetOccupantAtPosition(pos);//find the occupant at this position if(occupant==null)Debug.Log("no occupant"); sr=occupant.GetComponent&lt;SpriteRenderer&gt;(); sr.sortingOrder=depth;//assign new depth depth++;//increment depth } } } }</pre><p>The beauty of a 2D array-based implementation is that for the proper isometric depth sorting, we just need to assign sequentially higher depth while we parse through the level in order, using sequential for loops. This works for our simple level with only a single layer of ground. If we had multiple ground levels at various heights, then the depth sorting could get complicated.</p><p>Everything else remains the same as the 2D implementation explained in the previous tutorial.<br></p><h3>Completed Level</h3><p>You can use the same keyboard controls to play the game. The only difference is that the hero will not be moving vertically or horizontally but isometrically. The finished level would look like the image below.</p><figure class="post_image"><img alt="Isometric version finished level" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29715/image/alt-sokoban-isometric-finish.png"></figure><p>Check out how the depth sorting is clearly visible with our new <code class="inline">blockTiles</code>. <br></p><p>That wasn't hard, was it? I invite you to change the level data in the text file to try out new levels. Next up is the hexagonal version, which is a bit more complicated, and I would advise you to take a break to play with the isometric version before proceeding.</p><h2> <span class="sectionnum">2.</span> Hexagonal Sokoban Game</h2><p>The hexagonal version of the Sokoban level would look like the image below.</p><figure class="post_image"><img alt="hexagonal sokoban level" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29715/image/alt-sokoban-hexagonal.png"></figure><h3>Hexagonal View<br> </h3><p>We are using the horizontal alignment for the hexagonal grid for this tutorial. The theory behind the hexagonal implementation requires a lot of further reading. Please refer to <a href="https://gamedevelopment.tutsplus.com/tutorials/introduction-to-axial-coordinates-for-hexagonal-tile-based-games--cms-28820" rel="external" target="_blank">this tutorial series</a> for a basic understanding. The theory is implemented in the helper class <code class="inline">HexHelperHorizontal</code>, which can be found in the <code class="inline">utils</code> folder.</p><h3>Hexagonal Coordinate Conversion</h3><p>The <code class="inline">HexagonalSokoban</code> game script uses convenience methods from the helper class for coordinate conversions and other hexagonal features. The helper class <code class="inline">HexHelperHorizontal</code> will only work with a horizontally aligned hexagonal grid. It includes methods to convert coordinates between offset, axial, and cubic systems. <br></p><p>The offset coordinate is the same 2D Cartesian coordinate. It also includes a <code class="inline">getNeighbors</code> method, which takes in an axial coordinate and returns a <code class="inline">List&lt;Vector2&gt;</code> with all the six neighbors of that cell coordinate. The order of the list is clockwise, starting with the northeast neighbor's cell coordinate.</p><h3>Changes in Controls</h3><p>With a hexagonal grid, we have six directions of motion instead of four, as the hexagon has six sides whereas a square has four. So we have six keyboard keys to control the movement of our hero, as shown in the image below.</p><figure class="post_image"><img alt="Hexagonal keyboard control keys" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29715/image/alt-sokoban-hexagonal-controls.png"></figure><p>The keys are arranged in the same layout as a hexagonal grid if you consider the keyboard key <code class="inline">S</code> as the middle cell, with all the control keys as its hexagonal neighbors. It helps reduce the confusion with controlling the motion. The corresponding changes to the input code are as below.</p><pre class="brush: csharp noskimlinks noskimwords">private void ApplyUserInput() {//we have 6 directions of motion controlled by e,d,x,z,a,w in a cyclic sequence starting with NE to NW if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(0);//north east }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(1);//east }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(2);//south east }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(3);//south west }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(4);//west }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(5);//north west } }</pre><p>There is no change in art, and there are no pivot changes necessary.</p><h3>Other Changes in Code</h3><p>I will be explaining the code changes with respect to the original 2D Sokoban tutorial and not the isometric version above. Please do refer to the linked source code for this tutorial. The most interesting fact is that almost all of the code remains the same. The <code class="inline">CreateLevel</code> method has only one change, which is the <code class="inline">middleOffset</code> calculation.</p><pre class="brush: csharp noskimlinks noskimwords">middleOffset.x=cols*tileWidth+tileWidth*0.5f;//this is changed for hexagonal middleOffset.y=rows*tileSize*3/4+tileSize*0.75f;//this is changed for isometric </pre><p>One major change is obviously the way the screen coordinates are found in the <code class="inline">GetScreenPointFromLevelIndices</code> method.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 GetScreenPointFromLevelIndices(int row,int col){ //converting indices to position values, col determines x &amp; row determine y Vector2 tempPt=new Vector2(row,col); tempPt=HexHelperHorizontal.offsetToAxial(tempPt);//convert from offset to axial //convert axial point to screen point tempPt=HexHelperHorizontal.axialToScreen(tempPt,sideLength); tempPt.x-=middleOffset.x-Screen.width/2;//add offsets for middle align tempPt.y*=-1;//unity y axis correction tempPt.y+=middleOffset.y-Screen.height/2; return tempPt; }</pre><p>Here we use the helper class to first convert the coordinate to axial and then find the corresponding screen coordinate. Please note the use of the <code class="inline">sideLength</code> variable for the second conversion. It is the value of the length of a side of the hexagon tile, which is again equal to half of the distance between the two pointy ends of the hexagon. Hence:</p><pre class="brush: csharp noskimlinks noskimwords">sideLength=tileSize*0.5f;</pre><p>The only other change is the <code class="inline">GetNextPositionAlong</code> method, which is used by the <code class="inline">TryMoveHero</code> method to find the next cell in a given direction. This method is completely changed to accommodate the entirely new layout of our grid.</p><pre class="brush: csharp noskimlinks noskimwords">private Vector2 GetNextPositionAlong(Vector2 objPos, int direction) {//this method is completely changed to accommodate the different way neighbours are found in hexagonal logic objPos=HexHelperHorizontal.offsetToAxial(objPos);//convert from offset to axial List&lt;Vector2&gt; neighbours= HexHelperHorizontal.getNeighbors(objPos); objPos=neighbours[direction];//the neighbour list follows the same order sequence objPos=HexHelperHorizontal.axialToOffset(objPos);//convert back from axial to offset return objPos; }</pre><p>Using the helper class, we can easily return the coordinates of the neighbor in the given direction.</p><p>Everything else remains the same as the original 2D implementation. That wasn't hard, was it? That being said, understanding how we arrived at the conversion equations by following the hexagonal tutorial, which is the crux of the whole process, is not easy. If you play and complete the level, you will get the result as below.</p><figure class="post_image"><img alt="hexagonal sokoban finished level" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29715/image/alt-sokoban-hexagonal-finish.png"></figure><h2>Conclusion<br> </h2><p>The main element in both conversions was the coordinate conversions. The isometric version involves additional changes in the art with their pivot point as well as the need for depth sorting. <br></p><p>I believe you have found how easy it is to create grid-based games using just two-dimensional array-based level data and a tile-based approach. There are unlimited possibilities and games that you can create with this new understanding.</p><p>If you have understood all the concepts we have discussed so far, I would invite you to change the control method to tap and add some path finding. Good luck.</p> 2017-12-20T12:00:00.000Z 2017-12-20T12:00:00.000Z Juwal Bose tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-29714 Unity 2D Tile-Based 'Sokoban' Game <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29714/final_image/sokoban_full.png" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><p>In this tutorial we will be exploring an approach for creating a sokoban or crate-pusher game using tile-based logic and a two-dimensional array to hold level data. We are using Unity for development with C# as the scripting language. Please download the source files provided with this tutorial to follow along.</p><h2> <span class="sectionnum">1.</span> The Sokoban Game</h2><p>There may be few among us who may not have played a Sokoban game variant. The original version may even be older than some of you. Please check out the <a href="https://en.wikipedia.org/wiki/Sokoban" rel="external" target="_blank">wiki page</a> for some details. Essentially, we have a character or user-controlled element which has to push crates or similar elements onto its destination tile. </p><p>The level consists of a square or rectangular grid of tiles where a tile can be a non-walkable one or a walkable one. We can walk on the walkable tiles and push the crates onto them. Special walkable tiles would be marked as destination tiles, which is where the crate should eventually rest in order to complete the level. The character is usually controlled using a keyboard. Once all crates have reached a destination tile, the level is complete.</p><p>Tile-based development essentially means that our game is composed of a number of tiles spread in a predetermined way. A level data element will represent how the tiles would need to be spread out to create our level. In our case, we'll be using a square tile-based grid. You can read more on <a href="https://gamedevelopment.tutsplus.com/categories/tile-based-games" target="_self">tile-based games</a> here on Envato Tuts+.</p><h2> <span class="sectionnum">2.</span> Preparing the Unity Project</h2><p>Let's see how we have organised our Unity project for this tutorial.</p><h3>The Art</h3><p>For this tutorial project, we are not using any external art assets, but will use the sprite primitives created with the latest Unity version 2017.1. The image below shows how we can create different shaped sprites within Unity.</p><figure class="post_image"><img alt="How to create sprites within United 20171" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29714/image/sokoban-sprites.png"></figure><p>We will use the <strong>Square</strong> sprite to represent a single tile in our sokoban level grid. We will use the <strong>Triangle</strong> sprite to represent our character, and we will use the <strong>Circle</strong> sprite to represent a crate, or in this case a ball. The normal ground tiles are white, whereas the destination tiles have a different colour to stand out.</p><h3>The Level Data</h3><p>We will be representing our level data in the form of a two-dimensional array which provides the perfect correlation between the logic and visual elements. We use a simple text file to store the level data, which makes it easier for us to edit the level outside of Unity or change levels simply by changing the files loaded. The <strong>Resources</strong> folder has a <code class="inline">level</code> text file, which has our default level.</p><pre class="brush: plain noskimlinks noskimwords">1,1,1,1,1,1,1 1,3,1,-1,1,0,1 -1,0,1,2,1,1,-1 1,1,1,3,1,3,1 1,1,0,-1,1,1,1</pre><p>The level has seven columns and five rows. A value of <code class="inline">1</code> means that we have a ground tile at that position. A value of <code class="inline">-1</code> means that it is a non-walkable tile, whereas a value of <code class="inline">0</code> means that it is a destination tile. The value <code class="inline">2</code> represents our hero, and <code class="inline">3</code> represents a pushable ball. Just by looking at the level data, we can visualise what our level would look like.</p><h2> <span class="sectionnum">3.</span> Creating a Sokoban Game Level</h2><p>To keep things simple, and as it is not a very complicated logic, we have only a single <code class="inline">Sokoban.cs</code><em></em> script file for the project, and it's attached to the scene camera. Please keep it open in your editor while you follow the rest of the tutorial.</p><h3>Special Level Data</h3><p>The level data represented by the 2D array is not only used to create the initial grid but is also used throughout the game to track level changes and game progress. This means that the current values are not sufficient to represent some of the level states during game play. </p><p>Each value represents the state of the corresponding tile in the level. We need additional values for representing a ball on the destination tile and the hero on the destination tile, which respectively are <code class="inline">-3</code> and <code class="inline">-2</code>. These values could be any value that you assign in the game script, not necessarily the same values we have used here. </p><h3>Parsing the Level Text File<br> </h3><p>The first step is to load our level data into a 2D array from the external text file. We use the <code class="inline">ParseLevel</code> method to load the <code class="inline">string</code> value and split it to populate our <code class="inline">levelData</code> 2D array.</p><pre class="brush: csharp noskimlinks noskimwords">void ParseLevel(){ TextAsset textFile = Resources.Load (levelName) as TextAsset; string[] lines = textFile.text.Split (new[] { '\r', '\n' }, System.StringSplitOptions.RemoveEmptyEntries);//split by new line, return string[] nums = lines.Split(new[] { ',' });//split by , rows=lines.Length;//number of rows cols=nums.Length;//number of columns levelData = new int[rows, cols]; for (int i = 0; i &lt; rows; i++) { string st = lines[i]; nums = st.Split(new[] { ',' }); for (int j = 0; j &lt; cols; j++) { int val; if (int.TryParse (nums[j], out val)){ levelData[i,j] = val; } else{ levelData[i,j] = invalidTile; } } } }</pre><p>While parsing, we determine the number of rows and columns our level has as we populate our <code class="inline">levelData</code>.</p><h3>Drawing Level</h3><p>Once we have our level data, we can draw our level on the screen. We use the CreateLevel method to do just that.</p><pre class="brush: csharp noskimlinks noskimwords">void CreateLevel(){ //calculate the offset to align whole level to scene middle middleOffset.x=cols*tileSize*0.5f-tileSize*0.5f; middleOffset.y=rows*tileSize*0.5f-tileSize*0.5f;; GameObject tile; SpriteRenderer sr; GameObject ball; int destinationCount=0; for (int i = 0; i &lt; rows; i++) { for (int j = 0; j &lt; cols; j++) { int val=levelData[i,j]; if(val!=invalidTile){//a valid tile tile = new GameObject("tile"+i.ToString()+"_"+j.ToString());//create new tile tile.transform.localScale=Vector2.one*(tileSize-1);//set tile size sr = tile.AddComponent&lt;SpriteRenderer&gt;();//add a sprite renderer sr.sprite=tileSprite;//assign tile sprite tile.transform.position=GetScreenPointFromLevelIndices(i,j);//place in scene based on level indices if(val==destinationTile){//if it is a destination tile, give different color sr.color=destinationColor; destinationCount++;//count destinations }else{ if(val==heroTile){//the hero tile hero = new GameObject("hero"); hero.transform.localScale=Vector2.one*(tileSize-1); sr = hero.AddComponent&lt;SpriteRenderer&gt;(); sr.sprite=heroSprite; sr.sortingOrder=1;//hero needs to be over the ground tile sr.color=Color.red; hero.transform.position=GetScreenPointFromLevelIndices(i,j); occupants.Add(hero, new Vector2(i,j));//store the level indices of hero in dict }else if(val==ballTile){//ball tile ballCount++;//increment number of balls in level ball = new GameObject("ball"+ballCount.ToString()); ball.transform.localScale=Vector2.one*(tileSize-1); sr = ball.AddComponent&lt;SpriteRenderer&gt;(); sr.sprite=ballSprite; sr.sortingOrder=1;//ball needs to be over the ground tile sr.color=Color.black; ball.transform.position=GetScreenPointFromLevelIndices(i,j); occupants.Add(ball, new Vector2(i,j));//store the level indices of ball in dict } } } } } if(ballCount&gt;destinationCount)Debug.LogError("there are more balls than destinations"); }</pre><p>For our level, we have set a <code class="inline">tileSize</code> value of <code class="inline">50</code>, which is the length of the side of one square tile in our level grid. We loop through our 2D array and determine the value stored at each of the <code class="inline">i</code> and <code class="inline">j</code> indices of the array. If this value is not an <code class="inline">invalidTile</code> (-1) then we create a new <code class="inline">GameObject</code> named <code class="inline">tile</code>. We attach a <code class="inline">SpriteRenderer</code> component to <code class="inline">tile</code> and assign the corresponding <code class="inline">Sprite</code> or <code class="inline">Color</code> depending on the value at the array index. </p><p>While placing the <code class="inline">hero</code> or the <code class="inline">ball</code>, we need to first create a ground tile and then create these tiles. As the hero and ball need to be overlaying the ground tile, we give their <code class="inline">SpriteRenderer</code> a higher <code class="inline">sortingOrder</code>. All tiles are assigned a <code class="inline">localScale</code> of <code class="inline">tileSize</code> so they are <code class="inline">50x50</code> in our scene. </p><p>We keep track of the number of balls in our scene using the <code class="inline">ballCount</code> variable, and there should be the same or a higher number of destination tiles in our level to make level completion possible. The magic happens in a single line of code where we determine the position of each tile using the <code class="inline">GetScreenPointFromLevelIndices(int row,int col)</code> method.</p><pre class="brush: csharp noskimlinks noskimwords">//... tile.transform.position=GetScreenPointFromLevelIndices(i,j);//place in scene based on level indices //... Vector2 GetScreenPointFromLevelIndices(int row,int col){ //converting indices to position values, col determines x &amp; row determine y return new Vector2(col*tileSize-middleOffset.x,row*-tileSize+middleOffset.y); }</pre><p>The world position of a tile is determined by multiplying the level indices with the <code class="inline">tileSize</code> value. The <code class="inline">middleOffset</code> variable is used to align the level in the middle of the screen. Notice that the <code class="inline">row</code> value is multiplied by a negative value in order to support the inverted <code class="inline">y</code> axis in Unity.</p><h2> <span class="sectionnum">4.</span> Sokoban Logic</h2><p>Now that we have displayed our level, let's proceed to the game logic. We need to listen for user key press input and move the <code class="inline">hero</code> based on the input. The key press determines a required direction of motion, and the <code class="inline">hero</code> needs to be moved in that direction. There are various scenarios to consider once we have determined the required direction of motion. Let's say that the tile next to <code class="inline">hero</code> in this direction is <em>tileK</em>.</p><ul> <li>Is there a tile in the scene at that position, or is it outside our grid?</li> <li>Is tileK a walkable tile?</li> <li>Is tileK occupied by a ball?</li> </ul><p>If the position of tileK is outside the grid, we do no need to do anything. If tileK is valid and is walkable, then we need to move <code class="inline">hero</code> to that position and update our <code class="inline">levelData</code> array. If tileK has a ball, then we need to consider the next neighbour in the same direction, say <em>tileL</em>.</p><ul> <li>Is tileL outside the grid?</li> <li>Is tileL a walkable tile?</li> <li>Is tileL occupied by a ball?</li> </ul><p>Only in the case where tileL is a walkable, non-occupied tile should we move the <code class="inline">hero</code> and the ball at tileK to tileK and tileL respectively. After successful movement, we need to update the <code class="inline">levelData</code> array.</p><h3>Supporting Functions</h3><p>The above logic means that we need to know which tile our <code class="inline">hero</code> is currently at. We also need to determine if a certain tile has a ball and should have access to that ball. </p><p>To facilitate this, we use a <code class="inline">Dictionary</code> called <code class="inline">occupants</code> which stores a <code class="inline">GameObject</code> as key and its array indices stored as <code class="inline">Vector2</code> as value. In the <code class="inline">CreateLevel</code> method, we populate <code class="inline">occupants</code> when we create <code class="inline">hero</code> or ball. Once we have the dictionary populated, we can use the <code class="inline">GetOccupantAtPosition</code> to get back the <code class="inline">GameObject</code> at a given array index.</p><pre class="brush: csharp noskimlinks noskimwords">Dictionary&lt;GameObject,Vector2&gt; occupants;//reference to balls &amp; hero //.. occupants.Add(hero, new Vector2(i,j));//store the level indices of hero in dict //.. occupants.Add(ball, new Vector2(i,j));//store the level indices of ball in dict //.. private GameObject GetOccupantAtPosition(Vector2 heroPos) {//loop through the occupants to find the ball at given position GameObject ball; foreach (KeyValuePair&lt;GameObject, Vector2&gt; pair in occupants) { if (pair.Value == heroPos) { ball = pair.Key; return ball; } } return null; }</pre><p>The <code class="inline">IsOccupied</code> method determines whether the <code class="inline">levelData</code> value at the indices provided represents a ball.</p><pre class="brush: csharp noskimlinks noskimwords">private bool IsOccupied(Vector2 objPos) {//check if there is a ball at given array position return (levelData[(int)objPos.x,(int)objPos.y]==ballTile || levelData[(int)objPos.x,(int)objPos.y]==ballOnDestinationTile); }</pre><p>We also need a way to check if a given position is inside our grid and if that tile is walkable. The <code class="inline">IsValidPosition</code> method checks the level indices passed in as parameters to determine whether it falls inside our level dimensions. It also checks whether we have an <code class="inline">invalidTile</code> as that index in the <code class="inline">levelData</code>.</p><pre class="brush: csharp noskimlinks noskimwords">private bool IsValidPosition(Vector2 objPos) {//check if the given indices fall within the array dimensions if(objPos.x&gt;-1&amp;&amp;objPos.x&lt;rows&amp;&amp;objPos.y&gt;-1&amp;&amp;objPos.y&lt;cols){ return levelData[(int)objPos.x,(int)objPos.y]!=invalidTile; }else return false; }</pre><h3>Responding to User Input</h3><p>In the <code class="inline">Update</code> method of our game script, we check for the user <code class="inline">KeyUp</code> events and compare against our input keys stored in the <code class="inline">userInputKeys</code> array. Once the required direction of motion is determined, we call the <code class="inline">TryMoveHero</code> method with the direction as a parameter.</p><pre class="brush: csharp noskimlinks noskimwords">void Update(){ if(gameOver)return; ApplyUserInput();//check &amp; use user input to move hero and balls } private void ApplyUserInput() { if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(0);//up }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(1);//right }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(2);//down }else if(Input.GetKeyUp(userInputKeys)){ TryMoveHero(3);//left } }</pre><p>The <code class="inline">TryMoveHero</code> method is where our core game logic explained at the start of this section is implemented. Please go through the following method carefully to see how the logic is implemented as explained above.</p><pre class="brush: csharp noskimlinks noskimwords">private void TryMoveHero(int direction) { Vector2 heroPos; Vector2 oldHeroPos; Vector2 nextPos; occupants.TryGetValue(hero,out oldHeroPos); heroPos=GetNextPositionAlong(oldHeroPos,direction);//find the next array position in given direction if(IsValidPosition(heroPos)){//check if it is a valid position &amp; falls inside the level array if(!IsOccupied(heroPos)){//check if it is occupied by a ball //move hero RemoveOccupant(oldHeroPos);//reset old level data at old position hero.transform.position=GetScreenPointFromLevelIndices((int)heroPos.x,(int)heroPos.y); occupants[hero]=heroPos; if(levelData[(int)heroPos.x,(int)heroPos.y]==groundTile){//moving onto a ground tile levelData[(int)heroPos.x,(int)heroPos.y]=heroTile; }else if(levelData[(int)heroPos.x,(int)heroPos.y]==destinationTile){//moving onto a destination tile levelData[(int)heroPos.x,(int)heroPos.y]=heroOnDestinationTile; } }else{ //we have a ball next to hero, check if it is empty on the other side of the ball nextPos=GetNextPositionAlong(heroPos,direction); if(IsValidPosition(nextPos)){ if(!IsOccupied(nextPos)){//we found empty neighbor, so we need to move both ball &amp; hero GameObject ball=GetOccupantAtPosition(heroPos);//find the ball at this position if(ball==null)Debug.Log("no ball"); RemoveOccupant(heroPos);//ball should be moved first before moving the hero ball.transform.position=GetScreenPointFromLevelIndices((int)nextPos.x,(int)nextPos.y); occupants[ball]=nextPos; if(levelData[(int)nextPos.x,(int)nextPos.y]==groundTile){ levelData[(int)nextPos.x,(int)nextPos.y]=ballTile; }else if(levelData[(int)nextPos.x,(int)nextPos.y]==destinationTile){ levelData[(int)nextPos.x,(int)nextPos.y]=ballOnDestinationTile; } RemoveOccupant(oldHeroPos);//now move hero hero.transform.position=GetScreenPointFromLevelIndices((int)heroPos.x,(int)heroPos.y); occupants[hero]=heroPos; if(levelData[(int)heroPos.x,(int)heroPos.y]==groundTile){ levelData[(int)heroPos.x,(int)heroPos.y]=heroTile; }else if(levelData[(int)heroPos.x,(int)heroPos.y]==destinationTile){ levelData[(int)heroPos.x,(int)heroPos.y]=heroOnDestinationTile; } } } } CheckCompletion();//check if all balls have reached destinations } }</pre><p>In order to get the next position along a certain direction based on a provided position, we use the <code class="inline">GetNextPositionAlong</code> method. It is just a matter of incrementing or decrementing either of the indices according to the direction.</p><pre class="brush: csharp noskimlinks noskimwords">private Vector2 GetNextPositionAlong(Vector2 objPos, int direction) { switch(direction){ case 0: objPos.x-=1;//up break; case 1: objPos.y+=1;//right break; case 2: objPos.x+=1;//down break; case 3: objPos.y-=1;//left break; } return objPos; }</pre><p>Before moving hero or ball, we need to clear their currently occupied position in the <code class="inline">levelData</code> array. This is done using the <code class="inline">RemoveOccupant</code> method.</p><pre class="brush: csharp noskimlinks noskimwords">private void RemoveOccupant(Vector2 objPos) { if(levelData[(int)objPos.x,(int)objPos.y]==heroTile||levelData[(int)objPos.x,(int)objPos.y]==ballTile){ levelData[(int)objPos.x,(int)objPos.y]=groundTile;//ball moving from ground tile }else if(levelData[(int)objPos.x,(int)objPos.y]==heroOnDestinationTile){ levelData[(int)objPos.x,(int)objPos.y]=destinationTile;//hero moving from destination tile }else if(levelData[(int)objPos.x,(int)objPos.y]==ballOnDestinationTile){ levelData[(int)objPos.x,(int)objPos.y]=destinationTile;//ball moving from destination tile } }</pre><p>If we find a <code class="inline">heroTile</code> or <code class="inline">ballTile</code> at the given index, we need to set it to <code class="inline">groundTile</code>. If we find a <code class="inline">heroOnDestinationTile</code> or <code class="inline">ballOnDestinationTile</code> then we need to set it to <code class="inline">destinationTile</code>.</p><h3>Level Completion</h3><p>The level is complete when all balls are at their destinations.</p><figure class="post_image"><img alt="A Completed Level" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29714/image/sokoban-level-complete.png"></figure><p>After each successful movement, we call the <code class="inline">CheckCompletion</code> method to see if the level is completed. We loop through our <code class="inline">levelData</code> array and count the number of <code class="inline">ballOnDestinationTile</code> occurrences. If this number is equal to our total number of balls determined by <code class="inline">ballCount</code>, the level is complete.</p><pre class="brush: csharp noskimlinks noskimwords">private void CheckCompletion() { int ballsOnDestination=0; for (int i = 0; i &lt; rows; i++) { for (int j = 0; j &lt; cols; j++) { if(levelData[i,j]==ballOnDestinationTile){ ballsOnDestination++; } } } if(ballsOnDestination==ballCount){ Debug.Log("level complete"); gameOver=true; } }</pre><h2>Conclusion</h2><p>This is a simple and efficient implementation of sokoban logic. You can create your own levels by altering the text file or creating a new one and changing the <code class="inline">levelName</code> variable to point to your new text file. </p><p>The current implementation uses the keyboard to control the hero. I would invite you to try and change the control to tap-based so that we can support touch-based devices. This would involve adding some 2D path finding as well if you fancy tapping on any tile to lead the hero there.</p><p>There will be a follow-up tutorial where we'll explore how the current project can be used to create isometric and hexagonal versions of sokoban with minimal changes. </p> 2017-10-31T13:00:29.000Z 2017-10-31T13:00:29.000Z Juwal Bose tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-29035 Hexagonal Character Movement Using Axial Coordinates <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29035/final_image/hexchar-final.png" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><p>In the <a href="https://gamedevelopment.tutsplus.com/tutorials/introduction-to-axial-coordinates-for-hexagonal-tile-based-games--cms-28820" rel="external" target="_blank">first part of the series</a>, we explored the different coordinate systems for hexagonal tile-based games with the help of a hexagonal Tetris game. One thing you may have noticed is that we are still relying on the offset coordinates for drawing the level onto the screen using the <code class="inline">levelData</code> array. </p><p>You may also be curious to know how we could determine the axial coordinates of a hexagonal tile from the pixel coordinates on the screen. The method used in the <a href="https://gamedevelopment.tutsplus.com/tutorials/creating-hexagonal-minesweeper--cms-28655" rel="external" target="_blank">hexagonal minesweeper tutorial</a> relies on the offset coordinates and is not a simple solution. Once we figure this out, we will proceed to create solutions for hexagonal character movement and pathfinding.</p><h2> <span class="sectionnum">1.</span> Converting Coordinates Between Pixel and Axial</h2><p>This will involve some math. We will be using the horizontal layout for the entire tutorial. Let's start by finding a very helpful relationship between the regular hexagon's width and height. Please refer to the image below.</p><figure class="post_image"><img alt="Hexagonal tile its angles lengths are displayed" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29035/image/hexchar-tilerelations.png"></figure><p>Consider the blue regular hexagon on the left of the image. We already know that all the sides are of equal length. All the interior angles are 120 degrees each. Connecting each corner to the centre of the hexagon will yield six triangles, one of which is shown using red lines. This triangle has all the internal angles equal to 60 degrees. </p><p>As the red line splits the two corner angles in the middle, we get <code class="inline">120/2=60</code>. The third angle is <code class="inline">180-(60+60)=60</code> as the sum of all angles within the triangle should be 180 degrees. Thus essentially the triangle is an equilateral triangle, which further means that each side of the triangle has the same length. So in the blue hexagon the two red lines, the green line and each blue line segment are of the same length. From the image, it is clear that the green line is <code class="inline">hexTileHeight/2</code>.</p><p>Proceeding to the hexagon on the right, we can see that as the side length is equal to <code class="inline">hexTileHeight/2</code>, the height of the top triangular portion should be <code class="inline">hexTileHeight/4</code> and the height of the bottom triangular portion should be <code class="inline">hexTileHeight/4</code>, which totals to the full height of the hexagon, <code class="inline">hexTileHeight</code>. </p><p>Now consider the small right-angled triangle in the top left with one green and one blue angle. The blue angle is 60 degrees as it is the half of the corner angle, which in turn means that the green angle is 30 degrees (<code class="inline">180-(60+90)</code>). Using this information, we arrive at a relationship between the height and width of the regular hexagon.</p><pre class="brush: plain noskimlinks noskimwords">tan 30 = opposite side/adjacent side; 1/sqrt(3) = (hexTileHeight/4)/(hexTileWidth/2); hexTileWidth = sqrt(3)*hexTileHeight/2; hexTileHeight = 2*hexTileWidth/sqrt(3);</pre><h3>Converting Axial to Pixel Coordinates<br> </h3><p>Before we approach the conversion, let's revisit the image of the horizontal hexagonal layout where we have highlighted the row and column in which one of the coordinates remains the same.</p><figure class="post_image"><img alt="Horizontal hexagonal layout with rows and columns highlighted where coordinates remain same" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/29035/image/hexchar-axial-horizontal.png"></figure><p>Considering the screen y value, we can see that each row has a y offset of <code class="inline">3*hexTileHeight/4</code>, while going down on the green line, the only value that changes is <code class="inline">i</code>. Hence, we can conclude that the y pixel value only depends on the axial <code class="inline">i</code> coordinate.</p><pre class="brush: plain noskimlinks noskimwords">y= (3*hexTileHeight/4)*i; y = 3/2*s*i;</pre><p>Where <code class="inline">s</code> is the side length, which was found to be <code class="inline">hexTileHeight/2</code>.</p><p>The screen x value is a bit more complicated than this. When considering the tiles within a single row, each tile has an x offset of <code class="inline">hexTileWidth</code>, which clearly depends only on the axial <code class="inline">j</code> coordinate. But each alternative row has an additional offset of <code class="inline">hexTileWidth/2</code> depending on the axial <code class="inline">i</code> coordinate.</p><p>Again considering the green line, if we imagine it was a square grid then the line would have been vertical, satisfying the equation <code class="inline">x=j*hexTileWidth</code>. As the only coordinate that changes along the green line is <code class="inline">i</code>, the offset will depend on it. This leads us to the following equation.</p><pre class="brush: plain noskimlinks noskimwords">x=j*hexTileWidth+(i*hexTileWidth/2); = j* sqrt(3)*hexTileHeight/2 + i* sqrt(3)*hexTileHeight/4; = sqrt(3)*s*(j+ (i/2));</pre><p>So here we have them: the equations to convert axial coordinates to screen coordinates. The corresponding conversion function is as below.</p><pre class="brush: javascript noskimlinks noskimwords">var rootThree=Math.sqrt(3); var sideLength=hexTileHeight/2; function axialToScreen(axialPoint){ var tileX=rootThree*sideLength*(axialPoint.y+(axialPoint.x/2)); var tileY=3*sideLength/2*axialPoint.x; axialPoint.x=tileX; axialPoint.y=tileY; return axialPoint; }</pre><p>The revised code for drawing the hexagonal grid is as follows.</p><pre class="brush: javascript noskimlinks noskimwords">for (var i = 0; i &lt; levelData.length; i++) { for (var j = 0; j &lt; levelData.length; j++) { axialPoint.x=i; axialPoint.y=j; axialPoint=offsetToAxial(axialPoint); screenPoint=axialToScreen(axialPoint); if(levelData[i][j]!=-1){ hexTile= new HexTileNode(game, screenPoint.x, screenPoint.y, 'hex', false,i,j,levelData[i][j]); hexGrid.add(hexTile); } } }</pre><h3>Converting Pixel to Axial Coordinates</h3><p>Reversing those equations with the simple substitution of one variable will lead us to the screen to axial conversion equations.</p><pre class="brush: plain noskimlinks noskimwords">i=y/(3/2*s); j=(x-(y/sqrt(3)))/s*sqrt(3);</pre><p>Although the required axial coordinates are integers, the equations will result in floating point numbers. So we will need to round them off and apply some corrections, relying on our main equation <code class="inline">x+y+z=0</code>. The conversion function is as below.</p><pre class="brush: javascript noskimlinks noskimwords">function screenToAxial(screenPoint){ var axialPoint=new Phaser.Point(); axialPoint.x=screenPoint.y/(1.5*sideLength); axialPoint.y=(screenPoint.x-(screenPoint.y/rootThree))/(rootThree*sideLength); var cubicZ=calculateCubicZ(axialPoint); var round_x=Math.round(axialPoint.x); var round_y=Math.round(axialPoint.y); var round_z=Math.round(cubicZ); if(round_x+round_y+round_z===0){ screenPoint.x=round_x; screenPoint.y=round_y; }else{ var delta_x=Math.abs(axialPoint.x-round_x); var delta_y=Math.abs(axialPoint.y-round_y); var delta_z=Math.abs(cubicZ-round_z); if(delta_x&gt;delta_y &amp;&amp; delta_x&gt;delta_z){ screenPoint.x=-round_y-round_z; screenPoint.y=round_y; }else if(delta_y&gt;delta_x &amp;&amp; delta_y&gt;delta_z){ screenPoint.x=round_x; screenPoint.y=-round_x-round_z; }else if(delta_z&gt;delta_x &amp;&amp; delta_z&gt;delta_y){ screenPoint.x=round_x screenPoint.y=round_y; } } return screenPoint; }</pre><p>Check out the interactive element, which uses these methods to display tiles and detect taps.</p><iframe width="100%" height="300" src="//jsfiddle.net/juwalbose/y6L1wpsu/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe><h2> <span class="sectionnum">2.</span> Character Movement</h2><p>The core concept of character movement in any grid is similar. We poll for user input, determine the direction, find the resulting position, check if the resulting position falls inside a wall in the grid, else move the character to that position. You may refer to my <a href="https://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-primer-for-game-developers-updated--cms-28392" target="_self">isometric character movement tutorial</a> to see this in action with respect to isometric coordinate conversion. </p><p>The only things that are different here are the coordinate conversion and the directions of motion. For a horizontally aligned hexagonal grid, there are six available directions for motion. We could use the keyboard keys <code class="inline">A</code>, <code class="inline">W</code>, <code class="inline">E</code>, <code class="inline">D</code>, <code class="inline">X</code>, and <code class="inline">Z</code> for controlling each direction. The default keyboard layout matches the directions perfectly, and the related functions are as below.</p><pre class="brush: javascript noskimlinks noskimwords">function moveLeft(){ movementVector.x=movementVector.y=0; movementVector.x=-1*speed; CheckCollisionAndMove(); } function moveRight(){ movementVector.x=movementVector.y=0; movementVector.x=speed; CheckCollisionAndMove(); } function moveTopLeft(){ movementVector.x=-0.5*speed;//Cos60 movementVector.y=-0.866*speed;//sine60 CheckCollisionAndMove(); } function moveTopRight(){ movementVector.x=0.5*speed;//Cos60 movementVector.y=-0.866*speed;//sine60 CheckCollisionAndMove(); } function moveBottomRight(){ movementVector.x=0.5*speed;//Cos60 movementVector.y=0.866*speed;//sine60 CheckCollisionAndMove(); } function moveBottomLeft(){ movementVector.x=-0.5*speed;//Cos60 movementVector.y=0.866*speed;//sine60 CheckCollisionAndMove(); }</pre><p>The diagonal directions of motion make an angle of 60 degrees with the horizontal direction. So we can directly calculate the new position using trigonometry by using <code class="inline">Cos 60</code> and <code class="inline">Sine 60</code>. From this <code class="inline">movementVector</code>, we find out the new resulting position and check if it falls inside a wall in the grid as below.</p><pre class="brush: javascript noskimlinks noskimwords">function CheckCollisionAndMove(){ var tempPos=new Phaser.Point(); tempPos.x=hero.x+movementVector.x; tempPos.y=hero.y+movementVector.y; var corner=new Phaser.Point(); //check tl corner.x=tempPos.x-heroSize/2; corner.y=tempPos.y-heroSize/2; if(checkCorner(corner))return; //check tr corner.x=tempPos.x+heroSize/2; corner.y=tempPos.y-heroSize/2; if(checkCorner(corner))return; //check bl corner.x=tempPos.x-heroSize/2; corner.y=tempPos.y+heroSize/2; if(checkCorner(corner))return; //check br corner.x=tempPos.x+heroSize/2; corner.y=tempPos.y+heroSize/2; if(checkCorner(corner))return; hero.x=tempPos.x; hero.y=tempPos.y; } function checkCorner(corner){ corner=screenToAxial(corner); corner=axialToOffset(corner); if(checkForOccuppancy(corner.x,corner.y)){ return true; } return false; }</pre><p>We add the <code class="inline">movementVector</code> to the hero position vector to get the new position for the hero sprite's centre. Then we find the position of the four corners of the hero sprite and check if those are colliding. If there are no collisions, then we set the new position to the hero sprite. Let's see that in action.</p><iframe width="100%" height="300" src="//jsfiddle.net/juwalbose/dbtnu16g/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe><p>Usually, this kind of free-flowing motion is not allowed in a grid-based game. Typically, characters move from tile to tile, that is, tile centre to tile centre, based on commands or tap. I trust that you can figure the solution out by yourself.</p><h2> <span class="sectionnum">3.</span> Pathfinding</h2><p>So here we are on the topic of pathfinding, a very scary topic for some. In my previous tutorials I never tried to create new pathfinding solutions but always preferred to use readily available solutions which are battle tested. </p><p>This time, I am making an exception and will be reinventing the wheel, mainly because there are various game mechanics possible and no single solution would benefit all. So it is handy to know how the whole thing is done in order to churn out your own custom solutions for your game mechanic. </p><p>The most basic algorithm that is used for pathfinding in grids is <em><a href="https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm" rel="external" target="_blank">Dijkstra's Algorithm</a></em>. We start at the first node and calculate the costs involved in moving to all the possible neighbour nodes. We close the first node and move to the neighbour node with the lowest cost involved. This is repeated for all the non-closed nodes till we reach the destination. A variant of this is the <em><a href="http://www.policyalmanac.org/games/aStarTutorial.htm" rel="external" target="_blank">A* algorithm</a></em>, where we also use a heuristic in addition to the cost. </p><p>A heuristic is used to calculate the approximate distance from the current node to the destination node. As we do not really know the path, this distance calculation is always an approximation. So a better heuristic will always yield a better path. Now, that being said, the best solution need not be the one which yields the best path as we need to consider the resource usage and performance of the algorithm as well, when all the calculations need to be done in real time or once per update loop. </p><p>The easiest and simplest heuristic is the <code class="inline">Manhattan heuristic</code> or <code class="inline">Manhattan distance</code>. In a 2D grid, this is actually the distance between the start node and end node as the crow flies, or the number of blocks we need to walk.</p><h3>Hexagonal Manhattan Variant</h3><p>For our hexagonal grid, we need to find a variant for the Manhattan heuristic to approximate the distance. As we are walking on the hexagonal tiles, the idea is to find the number of tiles we need to walk over to reach the destination. Let me show you the solution first. Please move the mouse over the interactive element below to see how far away the other tiles are from the tile under the mouse.</p><iframe width="100%" height="300" src="//jsfiddle.net/juwalbose/7t0myp6e/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe><p>In the example above, we find the tile under the mouse and find the distance of all other tiles from it. The logic is to find the difference of <code class="inline">i</code> and <code class="inline">j</code> axial coordinates of both tiles first, say <code class="inline">di</code> and <code class="inline">dj</code>. Find the absolute values of these differences, <code class="inline">absi</code> and <code class="inline">absj</code>, as distances are always positive. </p><p>We notice that when both <code class="inline">di</code> and <code class="inline">dj</code> are positive and when both <code class="inline">di</code> and <code class="inline">dj</code> are negative, the distance is <code class="inline">absi+absj</code>. When <code class="inline">di</code> and <code class="inline">dj</code> are of opposite signs, the distance is the bigger value among <code class="inline">absi</code> and <code class="inline">absj</code>. This leads to the heuristic calculation function <code class="inline">getHeuristic</code> as below.</p><pre class="brush: javascript noskimlinks noskimwords">getHeuristic=function(i,j){ j=(j-(Math.floor(i/2))); var di=i-this.originali; var dj=j-this.convertedj; var si=Math.sign(di); var sj=Math.sign(dj); var absi=di*si; var absj=dj*sj; if(si!=sj){ this.heuristic= Math.max(absi,absj); }else{ this.heuristic= (absi+absj); } }</pre><p>One thing to notice is that we are not considering if the path is really walkable or not; we just assume that it is walkable and set the distance value. </p><h3>Finding the Hexagonal Path</h3><p>Let's proceed with pathfinding for our hexagonal grid with the newly found heuristic method. As we will be using recursion, it will be easier to understand once we breakdown the core logic of our approach. Each hexagonal tile will have a heuristic distance and a cost value associated with it.</p><ul> <li>We have a recursive function, say <code class="inline">findPath(tile)</code>, which takes in one hexagonal tile, which is the current tile. Initially this will be the starting tile.</li> <li>If the tile is equal to the end tile, the recursion ends and we have found the path. Else we proceed with the calculation.</li> <li>We find all the walkable neighbours of the tile. We will loop through all the neighbour tiles and apply further logic to each of them unless they are <code class="inline">closed</code>.</li> <li>If a neighbour is not previously <em>visited</em> and not closed, we find the distance of the neighbour tile to the end tile using our heuristic. We set the neighbour tile's <code class="inline">cost</code> to <em>current tile's cost + 10</em>. We set the neighbour tile as <em>visited</em>. We set the neighbour tile's <code class="inline">previous tile</code> as the current tile. We do this for a previously visited neighbour as well if the current tile's cost + 10 is less than that neighbour's cost.</li> <li>We calculate the total cost as the sum of the neighbour tile's cost value and the heuristic distance value. Among all the neighbours, we select the neighbour which gives the lowest total cost and call <code class="inline">findPath</code> on that neighbour tile.</li> <li>We set the current tile to closed so that it won't be considered anymore.</li> <li>In some cases, we'll fail to find any tile which satisfies the conditions, and then we close the current tile, open the previous tile, and redo.</li> </ul><p>There is an obvious failure condition in the logic when more than one tile satisfies the conditions. A better algorithm will find all the different paths and select the one with the shortest length, but we won't be doing that here. Check the pathfinding in action below.</p><iframe width="100%" height="300" src="//jsfiddle.net/juwalbose/ed1frr68/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe><p>For this example, I am calculating neighbours differently than in the Tetris example. When using axial coordinates, the neighbour tiles have coordinates which are higher or lower by a value of 1.</p><pre class="brush: javascript noskimlinks noskimwords">function getNeighbors(i,j){ //coordinates are in axial var tempArray=[]; var axialPoint=new Phaser.Point(i,j); var neighbourPoint=new Phaser.Point(); neighbourPoint.x=axialPoint.x-1;//tr neighbourPoint.y=axialPoint.y; populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); neighbourPoint.x=axialPoint.x+1;//bl neighbourPoint.y=axialPoint.y; populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); neighbourPoint.x=axialPoint.x;//l neighbourPoint.y=axialPoint.y-1; populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); neighbourPoint.x=axialPoint.x;//r neighbourPoint.y=axialPoint.y+1; populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); neighbourPoint.x=axialPoint.x-1;//tr neighbourPoint.y=axialPoint.y+1; populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); neighbourPoint.x=axialPoint.x+1;//bl neighbourPoint.y=axialPoint.y-1; populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); return tempArray; }</pre><p>The <code class="inline">findPath</code> recursive function is as below.</p><pre class="brush: javascript noskimlinks noskimwords">function findPath(tile){//passes in a hexTileNode if(Phaser.Point.equals(tile,endTile)){ //success, destination reached console.log('success'); //now paint the path. paintPath(tile); }else{//find all neighbors var neighbors=getNeighbors(tile.originali,tile.convertedj); var newPt=new Phaser.Point(); var hexTile; var totalCost=0; var currentLowestCost=100000; var nextTile; //find heuristics &amp; cost for all neighbors while(neighbors.length){ newPt=neighbors.shift(); hexTile=hexGrid.getByName("tile"+newPt.x+"_"+newPt.y); if(!hexTile.nodeClosed){//if node was not already calculated if((hexTile.nodeVisited &amp;&amp; (tile.cost+10)&lt;hexTile.cost) || !hexTile.nodeVisited){//if node was already visited, compare cost hexTile.getHeuristic(endTile.originali,endTile.originalj); hexTile.cost=tile.cost+10; hexTile.previousNode=tile;//point to previous node hexTile.nodeVisited=true; hexTile.showDifference();//display heuristic &amp; cost }else continue; totalCost=hexTile.cost+hexTile.heuristic; if(totalCost&lt;currentLowestCost){//select the next neighbour with lowest total cost nextTile=hexTile; currentLowestCost=totalCost; } }else{ console.log('node closed'); } } tile.nodeClosed=true; if(nextTile!==null){ findPath(nextTile);//call algo on the new tile nextTileToCall=nextTile; }else{ if(tile.previousNode!==null){ //current tile is now closed, open previous tile and redo. tile.previousNode.cost-=10; tile.previousNode.nodeClosed=false; findPath(tile.previousNode);//call algo on the previous tile nextTileToCall=tile.previousNode; }else{ //no path nextTileToCall=null; } } } }</pre><p>It may require further and multiple reading to properly understand what is going on, but believe me, it is worth the effort. This is only a very basic solution and could be improved a lot. For moving the character along the calculated path, you can refer to my <a href="https://gamedevelopment.tutsplus.com/tutorials/updated-primer-for-creating-isometric-worlds-continued--cms-28503" rel="external" target="_blank">isometric path following tutorial</a>. </p><p>Marking the path is done using another simple recursive function, <code class="inline">paintPath(tile)</code>, which is first called with the end tile. We just mark the <code class="inline">previousNode</code> of the tile if present.</p><pre class="brush: javascript noskimlinks noskimwords">function paintPath(tile){ tile.markDirty(); if(tile.previousNode!==null){ paintPath(tile.previousNode); } }</pre><h2>Conclusion</h2><p>With the help of all the three hexagonal tutorials I have shared, you should be able to get started with your next awesome hexagonal tile-based game. </p><p>Please be advised that there are other approaches as well, and there's a lot of further reading out there if you are up for it. Please do let me know through the comments if you need anything more to be explored in relation to hexagonal tile-based games. </p> 2017-08-16T12:00:46.000Z 2017-08-16T12:00:46.000Z Juwal Bose tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-28820 Introduction to Axial Coordinates for Hexagonal Tile-Based Games <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/final_image/final-hextris.png" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><p>The basic hexagonal tile-based approach explained in the <a href="https://gamedevelopment.tutsplus.com/tutorials/creating-hexagonal-minesweeper--cms-28655" rel="external" target="_blank">hexagonal minesweeper tutorial</a> gets the work done but is not very efficient. It uses direct conversion from the two-dimensional array-based level data and the screen coordinates, which makes it unnecessarily complicated to determine tapped tiles. </p><p>Also, the need to use different logic depending on the odd or even row/column of a tile is not convenient. This tutorial series explores the alternative screen coordinate systems which could be used to ease the logic and make things more convenient. I would strongly suggest that you read the hexagonal minesweeper tutorial before moving ahead with this tutorial as that one explains the grid rendering <a href="https://gamedevelopment.tutsplus.com/tutorials/creating-hexagonal-minesweeper--cms-28655" rel="external" target="_blank">based on a two-dimensional array</a>.</p><h2> <span class="sectionnum">1.</span> Axial Coordinates</h2><p>The default approach used for screen coordinates in the hexagonal minesweeper tutorial is called the offset coordinate approach. This is because the alternative rows or columns are offset by a value while aligning the hexagonal grid. </p><p>To refresh your memory, please refer to the image below, which shows the horizontal alignment with offset coordinate values displayed.</p><figure class="post_image"><img alt="horizontally aligned hexagonal grid with offset coordinate values" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/image/hextris-offset-horizontal.png"></figure><p>In the image above, a row with the same <code class="inline">i</code> value is highlighted in red, and a column with same <code class="inline">j</code> value is highlighted in green. To make everything simple, we won't be discussing the odd and even offset variants as both are just different ways to get the same result. </p><p>Let me introduce a better screen coordinate alternative, the axial coordinate. Converting an offset coordinate to an axial variant is very simple. The <code class="inline">i</code> value remains the same, but the <code class="inline">j</code> value is converted using the formula <code class="inline">axialJ = i - floor(j/2)</code>. A simple method can be used to convert an offset <code class="inline">Phaser.Point</code> to its axial variant, as shown below.</p><pre class="brush: javascript noskimlinks noskimwords">function offsetToAxial(offsetPoint){ offsetPoint.y=(offsetPoint.y-(Math.floor(offsetPoint.x/2))); return offsetPoint; }</pre><p>The reverse conversion would be as shown below.</p><pre class="brush: javascript noskimlinks noskimwords">function axialToOffset(axialPoint){ axialPoint.y=(axialPoint.y+(Math.floor(axialPoint.x/2))); return axialPoint; }</pre><p>Here the <code class="inline">x</code> value is the <code class="inline">i</code> value, and <code class="inline">y</code> value is the <code class="inline">j</code> value for the two-dimensional array. After conversion, the new values would look like the image below.</p><figure class="post_image"><img alt="horizontally aligned hexagonal grid with axial coordinate values" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/image/hextris-axial-horizontal.png"></figure><p>Notice that the green line where the <code class="inline">j</code> value remains the same does not zigzag anymore, but rather is now a diagonal to our hexagonal grid.</p><p>For the vertically aligned hexagonal grid, the offset coordinates are displayed in the image below.</p><figure class="post_image"><img alt="vertically aligned hexagonal grid with offset coordinate values" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/image/hextris-offsetco-vertical.png"></figure><p>The conversion to axial coordinates follows the same equations, with the difference that we keep the <code class="inline">j</code> value the same and alter the <code class="inline">i</code> value. The method below shows the conversion.</p><pre class="brush: javascript noskimlinks noskimwords">function offsetToAxial(offsetPoint){ offsetPoint.x=(offsetPoint.x-(Math.floor(offsetPoint.y/2))); return offsetPoint; }</pre><p>The result is as shown below.</p><figure class="post_image"><img alt="vertically aligned hexagonal grid with axial coordinate values" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/image/hextris-axial-vertical.png"></figure><p>Before we use the new coordinates to solve problems, let me quickly introduce you to another screen coordinate alternative: cube coordinates.</p><h2> <span class="sectionnum">2.</span> Cube or Cubic Coordinates</h2><p>Straightening up the zigzag itself has potentially solved most of the inconveniences we had with the offset coordinate system. Cube or cubic coordinates would further assist us in simplifying complicated logic like heuristics or rotating around a hexagonal cell. </p><p>As you may have guessed from the name, the cubic system has three values. The third <code class="inline">k</code> or <code class="inline">z</code> value is derived from the equation <code class="inline">x+y+z=0</code>, where <code class="inline">x</code> and <code class="inline">y</code> are the axial coordinates. This leads us to this simple method to calculate the <code class="inline">z</code> value.</p><pre class="brush: javascript noskimlinks noskimwords">function calculateCubicZ(axialPoint){ return -axialPoint.x-axialPoint.y; }</pre><p>The equation <code class="inline">x+y+z=0</code> is actually a 3D plane which passes through the diagonal of a three-dimensional cube grid. Displaying all three values for the grid will result in the following images for the different hexagonal alignments.</p><figure class="post_image"><img alt="horizontally aligned hexagonal grid with cube coordinate values" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/image/hextris-cubicc-horizontal.png"></figure><figure class="post_image"><img alt="vertically aligned hexagonal grid with cube coordinate values" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/image/hextris-cubicc-vertical.png"></figure><p>The blue line indicates the tiles where the <code class="inline">z</code> value remains the same. </p><h2> <span class="sectionnum">3.</span> Advantages of the New Coordinate System</h2><p>You may be wondering how these new coordinate systems help us with hexagonal logic. I will explain a few benefits before we move on to create a hexagonal Tetris using our new knowledge.</p><h3>Movement</h3><p>Let's consider the middle tile in the image above, which has cubic coordinate values of <code class="inline">3,6,-9</code>. We have noticed that one coordinate value remains the same for the tiles on the coloured lines. Further, we can see that the remaining coordinates either increase or decrease by 1 while tracing any of the coloured lines. For example, if the <code class="inline">x</code> value remains the same and the <code class="inline">y</code> value increases by 1 along a direction, the <code class="inline">z</code> value decreases by 1 to satisfy our governing equation <code class="inline">x+y+z=0</code>. This feature makes controlling movement much easier. We will put this to use in the second part of the series.</p><h3>Neighbours</h3><p>By the same logic, it is straightforward to find the neighbours for tile <code class="inline">x,y,z</code>. By keeping <code class="inline">x</code> the same, we get two diagonal neighbours, <code class="inline">x,y-1,z+1</code> and <code class="inline">x,y+1,z-1</code>. By keeping y the same, we get two vertical neighbours, <code class="inline">x-1,y,z+1</code> and <code class="inline">x+1,y,z-1</code>. By keeping z the same, we get the remaining two diagonal neighbours, <code class="inline">x+1,y-1,z</code> and <code class="inline">x-1,y+1,z</code>. The image below illustrates this for a tile at the origin.</p><figure class="post_image"><img alt="the cube coordinates of neighbours of a hexagonal tile at origin" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28820/image/hextris-neighbors.png"></figure><p>It is so much easier now that we don't need to use different logic based on even or odd rows/columns.</p><h3>Moving Around a Tile</h3><p>One interesting thing to notice in the above image is a kind of cyclic symmetry for all the tiles around the red tile. If we take the coordinates of any neighbouring tile, the coordinates of the immediate neighbouring tile can be obtained by cycling the coordinate values either left or right and then multiplying by -1. </p><p>For example, the top neighbour has a value of <code class="inline">-1,0,1</code>, which on rotating right once becomes <code class="inline">1,-1,0</code> and after multiplying by -1 becomes <code class="inline">-1,1,0</code>, which is the coordinate of the right neighbour. Rotating left and multiplying by -1 yields <code class="inline">0,-1,1</code>, which is the coordinate of the left neighbour. By repeating this, we can jump between all the neighbouring tiles around the centre tile. This is a very interesting feature which could assist in logic and algorithms. </p><p>Note that this is happening only due to the fact that the middle tile is considered to be at the origin. We could easily make any tile <code class="inline">x,y,z</code> to be at the origin by subtracting the values  <code class="inline">x</code>, <code class="inline">y</code> and <code class="inline">z</code> from it and all other tiles.</p><h3>Heuristics</h3><p>Calculating efficient heuristics is key when it comes to pathfinding or similar algorithms. Cubic coordinates make it easier to find simple heuristics for hexagonal grids due to the aspects mentioned above. We will discuss this in detail in the second part of this series.</p><p>These are some of the advantages of the new coordinate system. We could use a mix of the different coordinate systems in our practical implementations. For example, the two-dimensional array is still the best way to save the level data, the coordinates of which are the offset coordinates. </p><p>Let's try to create a hexagonal version of the famous Tetris game using this new knowledge.</p><h2> <span class="sectionnum">4.</span> Creating a Hexagonal Tetris</h2><p>We have all played Tetris, and if you are a game developer, you may have created your own version as well. Tetris is one of the easiest tile-based games one can implement, apart from tic tac toe or checkers, using a simple two-dimensional array. Let's first list the features of Tetris.</p><ul> <li>It starts with a blank two-dimensional grid.</li> <li>Different blocks appear at the top and move down one tile at a time until they reach the bottom.</li> <li>Once they reach the bottom, they get cemented there or become non-interactive. Basically, they become part of the grid.</li> <li>While dropping down, the block can be moved sideways, rotated clockwise/anticlockwise, and dropped down.</li> <li>The objective is to fill up all the tiles in any row, upon which the whole row disappears, collapsing the rest of the filled grid onto it.</li> <li>The game ends when there are no more free tiles on top for a new block to enter the grid.</li> </ul><h3>Representing the Different Blocks<br> </h3><p>As the game has blocks dropping vertically, we will use a vertically aligned hexagonal grid. This means that moving them sideways will make them move in a zigzag manner. A full row in the grid consists of a set of tiles in zigzag order. From this point onwards, you may start referring to the source code provided along with this tutorial. </p><p>The level data is stored in a two-dimensional array named <code class="inline">levelData</code>, and the rendering is done using the offset coordinates, as explained in the hexagonal minesweeper tutorial. Please refer to it if you're having difficulty following the code. </p><p>The interactive element in the next section shows the different blocks which we are going to use. There is one more additional block, which consists of three filled tiles aligned vertically like a pillar. <code class="inline">BlockData</code> is used to create the different blocks. </p><pre class="brush: javascript noskimlinks noskimwords">function BlockData(topB,topRightB,bottomRightB,bottomB,bottomLeftB,topLeftB){ this.tBlock=topB; this.trBlock=topRightB; this.brBlock=bottomRightB; this.bBlock=bottomB; this.blBlock=bottomLeftB; this.tlBlock=topLeftB; this.mBlock=1; }</pre><p>A blank block template is a set of seven tiles consisting of a middle tile surrounded by its six neighbours. For any Tetris block, the middle tile is always filled denoted by a value of <code class="inline">1</code>, whereas an empty tile would be denoted by a value of <code class="inline">0</code>. The different blocks are created by populating the tiles of <code class="inline">BlockData</code> as below.</p><pre class="brush: javascript noskimlinks noskimwords">var block1= new BlockData(1,1,0,0,0,1); var block2= new BlockData(0,1,0,0,0,1); var block3= new BlockData(1,1,0,0,0,0); var block4= new BlockData(1,1,0,1,0,0); var block5= new BlockData(1,0,0,1,0,1); var block6= new BlockData(0,1,1,0,1,1); var block7= new BlockData(1,0,0,1,0,0);</pre><p>We have a total of seven different blocks.</p><h3>Rotating the Blocks<br> </h3><p>Let me show you how the blocks rotate using the interactive element below. Tap and hold to rotate the blocks, and tap <code class="inline">x</code> to change the direction of rotation.</p><iframe width="100%" height="300" src="//jsfiddle.net/juwalbose/q0rvg0kd/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe><p>To rotate the block, we need to find all the tiles which have a value of <code class="inline">1</code>, set the value to <code class="inline">0</code>, rotate once around the middle tile to find the neighbouring tile, and set its value to <code class="inline">1</code>. To rotate a tile around another tile, we can use the logic explained in the <em>moving around a tile</em> section above. We arrive at the below method for this purpose.</p><pre class="brush: javascript noskimlinks noskimwords">function rotateTileAroundTile(tileToRotate, anchorTile){ tileToRotate=offsetToAxial(tileToRotate);//convert to axial var tileToRotateZ=calculateCubicZ(tileToRotate);//find z value anchorTile=offsetToAxial(anchorTile);//convert to axial var anchorTileZ=calculateCubicZ(anchorTile);//find z value tileToRotate.x=tileToRotate.x-anchorTile.x;//find x difference tileToRotate.y=tileToRotate.y-anchorTile.y;//find y difference tileToRotateZ=tileToRotateZ-anchorTileZ;//find z difference var pointArr=[tileToRotate.x,tileToRotate.y,tileToRotateZ];//populate array to rotate pointArr=arrayRotate(pointArr,clockWise);//rotate array, true for clockwise tileToRotate.x=(-1*pointArr)+anchorTile.x;//multiply by -1 &amp; remove the x difference tileToRotate.y=(-1*pointArr)+anchorTile.y;//multiply by -1 &amp; remove the y difference tileToRotate=axialToOffset(tileToRotate);//convert to offset return tileToRotate; } //... function arrayRotate(arr, reverse){//nifty method to rotate array elements if(reverse) arr.unshift(arr.pop()) else arr.push(arr.shift()) return arr } </pre><p>The variable <code class="inline">clockWise</code> is used to rotate clockwise or anticlockwise, which is accomplished by moving the array values in opposite directions in <code class="inline">arrayRotate</code>.</p><h3>Moving the Block<br> </h3><p>We keep track of the <code class="inline">i</code> and <code class="inline">j</code> offset coordinates for the middle tile of the block using the variables <code class="inline">blockMidRowValue</code> and <code class="inline">blockMidColumnValue</code> respectively. In order to move the block, we increment or decrement these values. We update the corresponding values in <code class="inline">levelData</code> with the block values using the <code class="inline">paintBlock</code> method. The updated <code class="inline">levelData</code> is used to render the scene after each state change.</p><pre class="brush: javascript noskimlinks noskimwords">var blockMidRowValue; var blockMidColumnValue; //... function moveLeft(){ blockMidColumnValue--; } function moveRight(){ blockMidColumnValue++; } function dropDown(){ paintBlock(true); blockMidRowValue++; } function paintBlock(){ clockWise=true; var val=1; changeLevelData(blockMidRowValue,blockMidColumnValue,val); var rotatingTile=new Phaser.Point(blockMidRowValue-1,blockMidColumnValue); if(currentBlock.tBlock==1){ changeLevelData(rotatingTile.x,rotatingTile.y,val*currentBlock.tBlock); } var midPoint=new Phaser.Point(blockMidRowValue,blockMidColumnValue); rotatingTile=rotateTileAroundTile(rotatingTile,midPoint); if(currentBlock.trBlock==1){ changeLevelData(rotatingTile.x,rotatingTile.y,val*currentBlock.trBlock); } midPoint.x=blockMidRowValue; midPoint.y=blockMidColumnValue; rotatingTile=rotateTileAroundTile(rotatingTile,midPoint); if(currentBlock.brBlock==1){ changeLevelData(rotatingTile.x,rotatingTile.y,val*currentBlock.brBlock); } midPoint.x=blockMidRowValue; midPoint.y=blockMidColumnValue; rotatingTile=rotateTileAroundTile(rotatingTile,midPoint); if(currentBlock.bBlock==1){ changeLevelData(rotatingTile.x,rotatingTile.y,val*currentBlock.bBlock); } midPoint.x=blockMidRowValue; midPoint.y=blockMidColumnValue; rotatingTile=rotateTileAroundTile(rotatingTile,midPoint); if(currentBlock.blBlock==1){ changeLevelData(rotatingTile.x,rotatingTile.y,val*currentBlock.blBlock); } midPoint.x=blockMidRowValue; midPoint.y=blockMidColumnValue; rotatingTile=rotateTileAroundTile(rotatingTile,midPoint); if(currentBlock.tlBlock==1){ changeLevelData(rotatingTile.x,rotatingTile.y,val*currentBlock.tlBlock); } } function changeLevelData(iVal,jVal,newValue,erase){ if(!validIndexes(iVal,jVal))return; if(erase){ if(levelData[iVal][jVal]==1){ levelData[iVal][jVal]=0; } }else{ levelData[iVal][jVal]=newValue; } } function validIndexes(iVal,jVal){ if(iVal&lt;0 || jVal&lt;0 || iVal&gt;=levelData.length || jVal&gt;=levelData.length){ return false; } return true; } </pre><p>Here, <code class="inline">currentBlock</code> points to the <code class="inline">blockData</code> in the scene. In <code class="inline">paintBlock</code>, first we set the <code class="inline">levelData</code> value for the middle tile of the block to <code class="inline">1</code> as it is always <code class="inline">1</code> for all blocks. The index of the midpoint is <code class="inline">blockMidRowValue</code>, <code class="inline">blockMidColumnValue</code>. </p><p>Then we move to the <code class="inline">levelData</code> index of the tile on top of the middle tile  <code class="inline">blockMidRowValue-1</code>,  <code class="inline">blockMidColumnValue</code>, and set it to <code class="inline">1</code> if the block has this tile as <code class="inline">1</code>. Then we rotate clockwise once around the middle tile to get the next tile and repeat the same process. This is done for all the tiles around the middle tile for the block.</p><h3>Checking Valid Operations</h3><p>While moving or rotating the block, we need to check if that is a valid operation. For example, we cannot move or rotate the block if the tiles it needs to occupy are already occupied. Also, we cannot move the block outside our two-dimensional grid. We also need to check if the block can drop any further, which would determine if we need to cement the block or not. </p><p>For all of these, I use a method <code class="inline">canMove(i,j)</code>, which returns a boolean indicating if placing the block at <code class="inline">i,j</code> is a valid move. For every operation, before actually changing the <code class="inline">levelData</code> values, we check if the new position for the block is a valid position using this method.</p><pre class="brush: javascript noskimlinks noskimwords">function canMove(iVal,jVal){ var validMove=true; var store=clockWise; var newBlockMidPoint=new Phaser.Point(blockMidRowValue+iVal,blockMidColumnValue+jVal); clockWise=true; if(!validAndEmpty(newBlockMidPoint.x,newBlockMidPoint.y)){//check mid, always 1 validMove=false; } var rotatingTile=new Phaser.Point(newBlockMidPoint.x-1,newBlockMidPoint.y); if(currentBlock.tBlock==1){ if(!validAndEmpty(rotatingTile.x,rotatingTile.y)){//check top validMove=false; } } newBlockMidPoint.x=blockMidRowValue+iVal; newBlockMidPoint.y=blockMidColumnValue+jVal; rotatingTile=rotateTileAroundTile(rotatingTile,newBlockMidPoint); if(currentBlock.trBlock==1){ if(!validAndEmpty(rotatingTile.x,rotatingTile.y)){ validMove=false; } } newBlockMidPoint.x=blockMidRowValue+iVal; newBlockMidPoint.y=blockMidColumnValue+jVal; rotatingTile=rotateTileAroundTile(rotatingTile,newBlockMidPoint); if(currentBlock.brBlock==1){ if(!validAndEmpty(rotatingTile.x,rotatingTile.y)){ validMove=false; } } newBlockMidPoint.x=blockMidRowValue+iVal; newBlockMidPoint.y=blockMidColumnValue+jVal; rotatingTile=rotateTileAroundTile(rotatingTile,newBlockMidPoint); if(currentBlock.bBlock==1){ if(!validAndEmpty(rotatingTile.x,rotatingTile.y)){ validMove=false; } } newBlockMidPoint.x=blockMidRowValue+iVal; newBlockMidPoint.y=blockMidColumnValue+jVal; rotatingTile=rotateTileAroundTile(rotatingTile,newBlockMidPoint); if(currentBlock.blBlock==1){ if(!validAndEmpty(rotatingTile.x,rotatingTile.y)){ validMove=false; } } newBlockMidPoint.x=blockMidRowValue+iVal; newBlockMidPoint.y=blockMidColumnValue+jVal; rotatingTile=rotateTileAroundTile(rotatingTile,newBlockMidPoint); if(currentBlock.tlBlock==1){ if(!validAndEmpty(rotatingTile.x,rotatingTile.y)){ validMove=false; } } clockWise=store; return validMove; } function validAndEmpty(iVal,jVal){ if(!validIndexes(iVal,jVal)){ return false; }else if(levelData[iVal][jVal]&gt;1){//occuppied return false; } return true; }</pre><p>The process here is the same as <code class="inline">paintBlock</code>, but instead of altering any values, this just returns a boolean indicating a valid move. Although I am using the <em>rotation around a middle tile</em> logic to find the neighbours, the easier and quite efficient alternative is to use the direct coordinate values of the neighbours, which can be easily determined from the middle tile coordinates.</p><h3>Rendering the Game</h3><p>The game level is visually represented by a <code class="inline">RenderTexture</code> named <code class="inline">gameScene</code>. In the array <code class="inline">levelData</code>, an unoccupied tile would have a value of <code class="inline">0</code>, and an occupied tile would have a value of <code class="inline">2</code> or higher. </p><p>A cemented block is denoted by a value of <code class="inline">2</code>, and a value of <code class="inline">5</code> denotes a tile which needs to be removed as it is part of a completed row. A value of <code class="inline">1</code> means that the tile is part of the block. After each game state change, we render the level using the information in <code class="inline">levelData</code>, as shown below.</p><pre class="brush: javascript noskimlinks noskimwords">//.. hexSprite.tint='0xffffff'; if(levelData[i][j]&gt;-1){ axialPoint=offsetToAxial(axialPoint); cubicZ=calculateCubicZ(axialPoint); if(levelData[i][j]==1){ hexSprite.tint='0xff0000'; }else if(levelData[i][j]==2){ hexSprite.tint='0x0000ff'; }else if(levelData[i][j]&gt;2){ hexSprite.tint='0x00ff00'; } gameScene.renderXY(hexSprite,startX, startY, false); } //...</pre><p>Hence a value of <code class="inline">0</code> is rendered without any tint, a value of <code class="inline">1</code> is rendered with red tint, a value of <code class="inline">2</code> is rendered with blue tint, and a value of <code class="inline">5</code> is rendered with green tint.</p><h2> <span class="sectionnum">5.</span> The Completed Game</h2><p>Putting everything together, we get the completed hexagonal Tetris game. Please go through the source code to understand the complete implementation. You will notice that we are using both offset coordinates and cubic coordinates for different purposes. For example, to find if a row is completed, we make use of offset coordinates and check the <code class="inline">levelData</code> rows.</p><iframe width="100%" height="300" src="//jsfiddle.net/juwalbose/5xtcsvwL/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe><h2>Conclusion</h2><p>This concludes the first part of the series. We have successfully created a hexagonal Tetris game using a combination of offset coordinates, axial coordinates, and cube coordinates. </p><p>In the concluding part of the series, we'll learn about character movement using the new coordinates on a horizontally aligned hexagonal grid.</p> 2017-08-15T12:00:13.000Z 2017-08-15T12:00:13.000Z Juwal Bose tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-28704 Basic 2D Platformer Physics, Part 8: Slopes <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28704/final_image/part_8_finalv2.gif" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><h2>Demo</h2><p>The demo shows the end result of the slope implementation. Use WASD to move the character. Right mouse button creates a tile. You can use the scroll wheel or the arrow keys to select a tile you want to place. The sliders change the size of the player's character.</p><p>The demo has been published under Unity 5.5.2f1, and the source code is also compatible with this version of Unity.</p><h2>Before We Start...</h2><p>As was true for the previous parts in the series, we'll be continuing our work where we left off in the last part. Last time we calculated and cached data needed to move the objects out of the slopes collision and changed how the collisions are checked against the tilemap. In this part we'll need the same setup from the end of last part.</p><p>You can download the project files from the previous part and write the code along with this tutorial.</p><p>In this part we'll be implementing the collision with slopes or other custom tiles, adding one-way slopes, and making it possible for the game object to travel along the slopes smoothly.<br></p><h2>Slopes Implementation</h2><h4>Vertical Slope Check</h4><p>We can finally get to slopes! First off, we'll try to handle when the bottom edge of the object is within a slope tile.</p><p>Let's go and take a look at our <code class="inline">CollidesWithTileBottom</code> function, particularly the part where we are handling the tiles.</p><pre class="brush: plain noskimlinks noskimwords">switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.onOneWay = false; state.pushesBottomTile = true; state.bottomTile = new Vector2i(x, bottomleftTile.y); return true; }</pre><p>To be able to see whether our object collides with the slope, we first need to get the offsets from the function we created earlier, which does most of our work.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y); SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType);</pre><p>Since we're checking one pixel below our character, we need to adjust the offset.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y); SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType); sf.freeUp -= 1; sf.collidingBottom -= 1;</pre><p>The condition for the collision is that the <code class="inline">freeUp</code> offset is greater or equal to 0, which means that either we move the character up or the character is standing on the slope.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y); SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType); sf.freeUp -= 1; sf.collidingBottom -= 1; if (sf.freeUp &gt;= 0) { }</pre><p>We shouldn't forget about the case when we want the character to stick to the slope, though. This means that even though the character walks off the slope, we want it to behave as if it were on the slope anyway. For this, we need to add a new constant which will contain the value of how steep a slope needs to be in order to be considered a vertical wall instead of a slope.</p><pre class="brush: csharp noskimlinks noskimwords">public const int cSlopeWallHeight = 4;</pre><p>If the offset is below this constant, it should be possible for the object to smoothly travel along the slope's curve. If it's equal or greater, it should be treated as a wall, and jumping would be needed to climb up.</p><p>Now we need to add another condition to our statement. This condition will check whether the character is supposed to be sticking to slopes, whether it was on a slope's last frame, and whether it needs to be pushed down or up by fewer pixels than our <code class="inline">cSlopeWallHeight</code> constant.</p><pre class="brush: csharp noskimlinks noskimwords">if (sf.freeUp &gt;= 0 || (mSticksToSlope &amp;&amp; state.pushedBottom &amp;&amp; sf.freeUp - sf.collidingBottom &lt; Constants.cSlopeWallHeight)) { }</pre><p>If the condition is true, we need to save this tile as a potential collidee with the object. We'll still need to iterate through all the other tiles along the X axis. First off, create the variables which will hold the X coordinate and the offset value for the colliding tile.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f)); int collidingBottom = int.MinValue; int slopeX = -1;</pre><p>Now save the values, if the defined condition holds true. If we already found a colliding tile, we need to compare the offsets, and the final colliding tile will be the one for which the character needs to be offset the most.</p><pre class="brush: csharp noskimlinks noskimwords">if ((sf.freeUp &gt;= 0 || (mSticksToSlope &amp;&amp; state.pushedBottom &amp;&amp; sf.freeUp - sf.collidingBottom &lt; Constants.cSlopeWallHeight)) &amp;&amp; sf.collidingBottom &gt;= collidingBottom) { collidingBottom = sf.collidingBottom; slopeX = x; } </pre><p>Finally, after we've iterated through all the tiles and found a tile the object is colliding with, we need to offset the object.</p><pre class="brush: csharp noskimlinks noskimwords">if (slopeX != -1) { state.pushesBottomTile = true; state.bottomTile = new Vector2i(slopeX, bottomleftTile.y); position.y += collidingBottom; topRight.y += collidingBottom; bottomLeft.y += collidingBottom; return true; } return false;</pre><p>That's pretty much it for the bottom check, so now let's do the top one. This one will be a bit simpler, as we don't even need to handle sticking.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); int freeDown = int.MaxValue; int slopeX = -1; for (int x = bottomleftTile.x; x &lt;= topRightTile.x; ++x) { var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y); if (Slopes.IsOneWay(tileCollisionType)) continue; switch (tileCollisionType) { default://slope Vector2 tileCenter = mMap.GetMapTilePosition(x, topRightTile.y); SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y + 0.5f, tileCollisionType); sf.freeDown += 1; sf.collidingTop += 1; if (sf.freeDown &lt; freeDown &amp;&amp; sf.freeDown &lt;= 0 &amp;&amp; sf.freeDown == sf.collidingTop) { freeDown = sf.freeDown; slopeX = x; } break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesTopTile = true; state.topTile = new Vector2i(x, topRightTile.y); return true; } } if (slopeX != -1) { state.pushesTopTile = true; state.topTile = new Vector2i(slopeX, topRightTile.y); position.y += freeDown; topRight.y += freeDown; bottomLeft.y += freeDown; return true; } return false; }</pre><p>That's it.</p><h4>Horizontal Slope Check</h4><p>The horizontal check will be a bit more complicated, as it is here where we'll be handling the most troublesome cases.</p><p>Let's start with handling the slopes on the right. There are a couple things that we'll need to be aware of, mostly concerning moving up the slopes. Let's consider the following situations.</p><figure class="post_image"><img alt="Different shaped slopes" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/slope_blocks.png"></figure><p>We'll need to handle those cases with special care because at some point when we move along the slope we're going to hit the ceiling. To prevent that, we'll need to do some more checks in case the character is moving horizontally.</p><p>For the vertical checks, we did move the object up from the tile, but in general we won't be using that functionality there. Since we're always checking a pixel that's just outside the object bounds, we'll never really overlap an obstacle. For the horizontal checks, it's a bit different, because this is the place where we'll be handling moving along the slope, so naturally the height adjustment will mainly take place here.</p><p>To make the proper collision response for the cases illustrated above, it'll be easier to check whether we can enter into a space horizontally, and if that's possible then check whether the object doesn't overlap with any solid pixels if it had to be moved vertically due to moving along a slope. If we fail to find the space, we know that it's impossible to move towards the checked direction, and we can set the horizontal wall flag.</p><p>Let's move to the <code class="inline">CollidesWithTileRight</code> function, to the part where we handle the slopes.</p><pre class="brush: plain noskimlinks noskimwords">default://slope Vector2 tileCenter = mMap.GetMapTilePosition(topRightTile.x, y); float leftTileEdge = (tileCenter.x - Map.cTileSize / 2); float rightTileEdge = (leftTileEdge + Map.cTileSize); float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2);</pre><p>We get the offset in a similar way we get it for the vertical checks, but the offset we care about is the one that's bigger. </p><pre class="brush: csharp noskimlinks noskimwords">var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); slopeOffset = Mathf.Abs(offset.freeUp) &lt; Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;</pre><p>Now, let's see if our character should treat the checked tile as a wall. We do this if either the slope offset is greater or equal to our <code class="inline">cSlopeWallHeight</code> constant or to get out of collision we'd need to offset the character up or down while we are already colliding with a tile in the same direction, which means that our object is squeezed between the top and bottom tiles.</p><pre class="brush: csharp noskimlinks noskimwords">var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); slopeOffset = Mathf.Abs(offset.freeUp) &lt; Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; if (Mathf.Abs(slopeOffset) &gt;= Constants.cSlopeWallHeight || (slopeOffset &lt; 0 &amp;&amp; state.pushesBottomTile) || (slopeOffset &gt; 0 &amp;&amp; state.pushesTopTile)) { state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; }</pre><p>If that's not the case and the offset is greater than 0, then we hit a slope. One problem here is that we do not know whether we hit a wall on other tiles that we have yet to check, so for now we'll just save the slope offset and tile collision type in case we need to use them later.</p><pre class="brush: cpp noskimlinks noskimwords">Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; TileCollisionType slopeCollisionType = TileCollisionType.Empty;</pre><p>Now, instead of seeing if the slope offset is greater than zero, let's compare it to another tile's slope offset, in case we already found a colliding slope in previous iterations.</p><pre class="brush: csharp noskimlinks noskimwords">var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); slopeOffset = Mathf.Abs(offset.freeUp) &lt; Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; if (Mathf.Abs(slopeOffset) &gt;= Constants.cSlopeWallHeight || (slopeOffset &lt; 0 &amp;&amp; state.pushesBottomTile) || (slopeOffset &gt; 0 &amp;&amp; state.pushesTopTile)) { state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; } else if (Mathf.Abs(slopeOffset) &gt; Mathf.Abs(oldSlopeOffset)) { slopeCollisionType = tileCollisionType; state.rightTile = new Vector2i(topRightTile.x, y); } else slopeOffset = oldSlopeOffset;</pre><h4>Handle the Squeezing Between Tiles</h4><p>After we finish looping through all the tiles of interest, let's see if we need to move the object. Let's handle the case where the slope offset ended up being non-zero.</p><pre class="brush: csharp noskimlinks noskimwords">if (slopeOffset != 0.0f) { }</pre><p>We'll have to handle two cases here, and we need to do slightly different things depending whether we need to offset our object up or down.</p><pre class="brush: csharp noskimlinks noskimwords">if (slopeOffset != 0.0f) { if (slopeOffset &gt; 0 &amp;&amp; slopeOffset &lt; Constants.cSlopeWallHeight) { } }</pre><p>First off, we need to check whether we can fit into the space after offsetting the object. If that's the case, then we're handling one of the cases illustrated above. Where the character is trying to move right, the offset is positive, but if we offset the object then it will be pushed into the top wall, so instead we'll just mark that it's colliding with the wall on the right side to block the movement in that direction.</p><pre class="brush: csharp noskimlinks noskimwords">if (slopeOffset &gt; 0 &amp;&amp; slopeOffset &lt; Constants.cSlopeWallHeight) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y += slopeOffset - Mathf.Sign(slopeOffset); tr.y += slopeOffset - Mathf.Sign(slopeOffset); bl.y += slopeOffset - Mathf.Sign(slopeOffset); PositionState s = new PositionState(); if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } }</pre><p>If we fit into the space, we'll mark that we collide with the bottom tile and offset the object's position appropriately.</p><pre class="brush: csharp noskimlinks noskimwords">if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } else { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesBottomTile = true; state.pushesBottomSlope = true; }</pre><p>We handle the case in which the object needs to be offset down in a similar manner.</p><pre class="brush: csharp noskimlinks noskimwords">if (slopeOffset != 0.0f) { if (slopeOffset &gt; 0 &amp;&amp; slopeOffset &lt; Constants.cSlopeWallHeight) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y += slopeOffset - Mathf.Sign(slopeOffset); tr.y += slopeOffset - Mathf.Sign(slopeOffset); bl.y += slopeOffset - Mathf.Sign(slopeOffset); PositionState s = new PositionState(); if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } else { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesBottomTile = true; state.pushesBottomSlope = true; } } else if (slopeOffset &lt; 0 &amp;&amp; slopeOffset &gt; -Constants.cSlopeWallHeight) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y += slopeOffset - Mathf.Sign(slopeOffset); tr.y += slopeOffset - Mathf.Sign(slopeOffset); bl.y += slopeOffset - Mathf.Sign(slopeOffset); PositionState s = new PositionState(); if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } else { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesTopTile = true; state.pushesTopSlope = true; } } }</pre><h4>Moving Object in Collision Check</h4><p>Now this function will offset the object up or down as is necessary if we want to step on the tile to the right, but what if we want to use this function just as a check, and we don't really want to move the character by calling it? To solve this issue, let's add an additional variable named 'move' to mark whether the function can move the object or not.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) { }</pre><p>And move the object only if this new flag is set to true.</p><pre class="brush: csharp noskimlinks noskimwords">if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } else if (move) { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesBottomTile = true; state.pushesBottomSlope = true; } //... if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } else if (move) { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesTopTile = true; state.pushesTopSlope = true; } </pre><h4>Handle Slope Sticking</h4><p>Now let's handle sticking to slopes. It's pretty straightforward, but we'll need to handle all the corner cases properly, so that the character will stick to the slope without any hiccups along the way.</p><p>Before we handle the corner cases, though, we can very easily handle slope sticking within a single tile in the vertical collision check. It will be enough if we add the following condition in the <code class="inline">CollidesWithTileBottom</code> function.</p><pre class="brush: plain noskimlinks noskimwords">if ((sf.freeUp &gt;= 0 &amp;&amp; sf.collidingBottom == sf.freeUp) || (mSticksToSlope &amp;&amp; state.pushedBottom &amp;&amp; sf.freeUp - sf.collidingBottom &lt; Constants.cSlopeWallHeight &amp;&amp; sf.freeUp &gt;= sf.collidingBottom)) { state.onOneWay = isOneWay; state.oneWayY = bottomleftTile.y; state.pushesBottomTile = true; state.bottomTile = new Vector2i(x, bottomleftTile.y); position.y += sf.collidingBottom; topRight.y += sf.collidingBottom; bottomLeft.y += sf.collidingBottom; return true; }</pre><p>This condition makes it so that if the distance between the object's position and the nearest ground is between 0 and the <code class="inline">cSlopeWallHeight</code>, then the character will get pushed down too, in addition to the original condition. This unfortunately works only within a single tile; the following illustration pinpoints the problem which we need to solve.</p><figure class="post_image"><img alt="Slope with three squares marked on it" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/slope_sticking_corner_casev2.png"></figure><p>The corner case we are talking about is just this: the character moves down and to the left from tile number one to tile number two. Tile number two is empty, so we need to check the tile below it and see if the offset from the character to tile number 3 is proper to keep walking along the slope there.</p><h4>Handle the Corner Cases</h4><p>It's going to be easier to handle these corner cases in the horizontal collision checks, so let's head back to the <code class="inline">CollidesWithTileRight</code> function. Let's go to the end of the function and handle the troublesome cases here. </p><p>First off, to handle the slope sticking, the <code class="inline">mSticksToSlope</code> flag needs to be set, the object must have been on the ground the previous frame, and the move flag needs to be on.</p><pre class="brush: csharp noskimlinks noskimwords">if (mSticksToSlope &amp;&amp; state.pushedBottomTile &amp;&amp; move) { }</pre><p>Now we need to find the tile to which we should stick. Since this function checks the collision on the right edge of the object, we'll be handling the slope sticking for the character's bottom left corner.</p><pre class="brush: plain noskimlinks noskimwords">var nextX = mMap.GetMapTileXAtPoint(topRight.x - 1.5f); var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1; var prevPos = mMap.GetMapTilePosition(new Vector2i(topRightTile.x, bottomLeftTile.y)); var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY)); var prevCollisionType = mMap.GetCollisionType(new Vector2i(topRightTile.x, bottomLeftTile.y)); var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY));</pre><p>Now we need to find a way to compare the height the object currently is on to the one it wants to step onto. If the next height is lower than the current one, but still higher than our <code class="inline">cSlopeWallHeight</code> constant, we'll push our object down onto the ground.</p><h4>Get Slope Height</h4><p>Let's go back to our Slope class to make a function which will return the height of a slope at a particular position.</p><pre class="brush: csharp noskimlinks noskimwords">public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) { switch (type) { case TileCollisionType.Empty: return 0; case TileCollisionType.Full: case TileCollisionType.OneWayPlatform: return Map.cTileSize; } }</pre><p>The parameters for the function are the x value on the slope and the slope type. If the slope is empty we can immediately return 0, and if it's full then we return the tile size.</p><p>We can easily get the height of a slope by using our cached offsets. If the tile is not transformed in any way, we just get an offset for an object that is one pixel wide at the position x, and its height is equal to the tile size.</p><pre class="brush: csharp noskimlinks noskimwords">public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) { switch (type) { case TileCollisionType.Empty: return 0; case TileCollisionType.Full: case TileCollisionType.OneWayPlatform: return Map.cTileSize; } var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][Map.cTileSize - 1]); return offset.collidingBottom; }</pre><p>Let's handle this for different transforms. If a slope is flipped on the X axis, we just need to mirror the x argument.</p><pre class="brush: csharp noskimlinks noskimwords">public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) { switch (type) { case TileCollisionType.Empty: return 0; case TileCollisionType.Full: case TileCollisionType.OneWayPlatform: return Map.cTileSize; } if (IsFlippedX(type)) x = Map.cTileSize - 1 - x; var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][Map.cTileSize - 1]); return offset.collidingBottom; }</pre><p>If the slope is flipped on the Y axis, we need to return the <code class="inline">collidingTop</code> instead of <code class="inline">collidingBottom</code> offset. Since <code class="inline">collidingTop</code> in this case will be negative, we'll also need to flip the sign for it.</p><pre class="brush: csharp noskimlinks noskimwords">var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][Map.cTileSize - 1]); return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom;</pre><p>Finally, if the tile is rotated by 90 degrees, we'll need to be returning <code class="inline">collidingLeft</code> or <code class="inline">collidingRight</code> offsets. Aside from that, to get a proper cached offset, we'll need to swap the x and y positions and size.</p><pre class="brush: csharp noskimlinks noskimwords">if (!IsFlipped90(type)) { var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][Map.cTileSize - 1]); return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom; } else { var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][Map.cTileSize - 1]); return IsFlippedY(type) ? offset.collidingLeft : -offset.collidingRight; }</pre><p>That's the final function.</p><pre class="brush: csharp noskimlinks noskimwords">public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) { switch (type) { case TileCollisionType.Empty: return 0; case TileCollisionType.Full: case TileCollisionType.OneWayPlatform: return Map.cTileSize; } if (IsFlippedX(type)) x = Map.cTileSize - 1 - x; if (!IsFlipped90(type)) { var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][Map.cTileSize - 1]); return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom; } else { var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][Map.cTileSize - 1]); return IsFlippedY(type) ? offset.collidingLeft : -offset.collidingRight; } }</pre><h4>Back to Corner Cases</h4><p>Let's move back to the <code class="inline">CollidesWithTileRight</code> function, right where we finished determining the slope types for the tiles the character moves between.</p><p>To use the function we just created, we need to determine the position at which we want to get the height of a tile.</p><pre class="brush: csharp noskimlinks noskimwords">var prevCollisionType = mMap.GetCollisionType(new Vector2i(bottomLeftTile.x, bottomLeftTile.y)); var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY)); int x1 = (int)Mathf.Clamp((bottomLeft.x - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); int x2 = (int)Mathf.Clamp((bottomLeft.x + 1.0f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);</pre><p>Now let's calculate the height between those two points.</p><pre class="brush: csharp noskimlinks noskimwords">int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType); int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType); var offset = slopeHeight + Map.cTileSize - nextSlopeHeight;</pre><p>If the offset is between 0 and the <code class="inline">cSlopeWallHeight</code> constant, then we're going to push the object down, but first we need to check whether we actually can push the object down. This is exactly the same routine we did earlier.</p><pre class="brush: csharp noskimlinks noskimwords">if (offset &lt; Constants.cSlopeWallHeight &amp;&amp; offset &gt; 0) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y -= offset - Mathf.Sign(offset); tr.y -= offset - Mathf.Sign(offset); bl.y -= offset - Mathf.Sign(offset); bl.x += 1.0f; tr.x += 1.0f; PositionState s = new PositionState(); if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) { position.y -= offset; bottomLeft.y -= offset; topRight.y -= offset; state.pushesBottomTile = true; state.pushesBottomSlope = true; } }</pre><p>All in all, the function should look like this.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; TileCollisionType slopeCollisionType = TileCollisionType.Empty; for (int y = bottomLeftTile.y; y &lt;= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); switch (tileCollisionType) { default://slope Vector2 tileCenter = mMap.GetMapTilePosition(topRightTile.x, y); float leftTileEdge = (tileCenter.x - Map.cTileSize / 2); float rightTileEdge = (leftTileEdge + Map.cTileSize); float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2); oldSlopeOffset = slopeOffset; var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); slopeOffset = Mathf.Abs(offset.freeUp) &lt; Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; if (Mathf.Abs(slopeOffset) &gt;= Constants.cSlopeWallHeight || (slopeOffset &lt; 0 &amp;&amp; state.pushesBottomTile) || (slopeOffset &gt; 0 &amp;&amp; state.pushesTopTile)) { state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; } else if (Mathf.Abs(slopeOffset) &gt; Mathf.Abs(oldSlopeOffset)) { slopeCollisionType = tileCollisionType; state.rightTile = new Vector2i(topRightTile.x, y); } else slopeOffset = oldSlopeOffset; break; case TileCollisionType.Empty: break; } } if (slopeOffset != 0.0f) { if (slopeOffset &gt; 0 &amp;&amp; slopeOffset &lt; Constants.cSlopeWallHeight) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y += slopeOffset - Mathf.Sign(slopeOffset); tr.y += slopeOffset - Mathf.Sign(slopeOffset); bl.y += slopeOffset - Mathf.Sign(slopeOffset); PositionState s = new PositionState(); if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } else if (move) { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesBottomTile = true; state.pushesBottomSlope = true; } } else if (slopeOffset &lt; 0 &amp;&amp; slopeOffset &gt; -Constants.cSlopeWallHeight) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y += slopeOffset - Mathf.Sign(slopeOffset); tr.y += slopeOffset - Mathf.Sign(slopeOffset); bl.y += slopeOffset - Mathf.Sign(slopeOffset); PositionState s = new PositionState(); if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) { state.pushesRightTile = true; state.pushesRightSlope = true; return true; } else if (move) { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesTopTile = true; state.pushesTopSlope = true; } } } if (mSticksToSlope &amp;&amp; state.pushedBottomTile &amp;&amp; move) { var nextX = mMap.GetMapTileXAtPoint(bottomLeft.x + 1.0f); var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1; var prevPos = mMap.GetMapTilePosition(new Vector2i(bottomLeftTile.x, bottomLeftTile.y)); var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY)); var prevCollisionType = mMap.GetCollisionType(new Vector2i(bottomLeftTile.x, bottomLeftTile.y)); var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY)); int x1 = (int)Mathf.Clamp((bottomLeft.x - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); int x2 = (int)Mathf.Clamp((bottomLeft.x + 1.0f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType); int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType); var offset = slopeHeight + Map.cTileSize - nextSlopeHeight; if (offset &lt; Constants.cSlopeWallHeight &amp;&amp; offset &gt; 0) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y -= offset - Mathf.Sign(offset); tr.y -= offset - Mathf.Sign(offset); bl.y -= offset - Mathf.Sign(offset); bl.x += 1.0f; tr.x += 1.0f; PositionState s = new PositionState(); if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) { position.y -= offset; bottomLeft.y -= offset; topRight.y -= offset; state.pushesBottomTile = true; state.pushesBottomSlope = true; } } } return false; }</pre><p>Now we need to do everything analogically for the <code class="inline">CollidesWithTileLeft</code> function. The final version of it should take the following form.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f)); float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; TileCollisionType slopeCollisionType = TileCollisionType.Empty; for (int y = bottomLeftTile.y; y &lt;= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y); switch (tileCollisionType) { default://slope Vector2 tileCenter = mMap.GetMapTilePosition(bottomLeftTile.x, y); float leftTileEdge = (tileCenter.x - Map.cTileSize / 2); float rightTileEdge = (leftTileEdge + Map.cTileSize); float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2); oldSlopeOffset = slopeOffset; var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x - 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); slopeOffset = Mathf.Abs(offset.freeUp) &lt; Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; if (Mathf.Abs(slopeOffset) &gt;= Constants.cSlopeWallHeight || (slopeOffset &lt; 0 &amp;&amp; state.pushesBottomTile) || (slopeOffset &gt; 0 &amp;&amp; state.pushesTopTile)) { state.pushesLeftTile = true; state.leftTile = new Vector2i(bottomLeftTile.x, y); return true; } else if (Mathf.Abs(slopeOffset) &gt; Mathf.Abs(oldSlopeOffset)) { slopeCollisionType = tileCollisionType; state.leftTile = new Vector2i(bottomLeftTile.x, y); } else slopeOffset = oldSlopeOffset; break; case TileCollisionType.Empty: break; } } if (slopeCollisionType != TileCollisionType.Empty &amp;&amp; slopeOffset != 0) { if (slopeOffset &gt; 0 &amp;&amp; slopeOffset &lt; Constants.cSlopeWallHeight) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y += slopeOffset - Mathf.Sign(slopeOffset); tr.y += slopeOffset - Mathf.Sign(slopeOffset); bl.y += slopeOffset - Mathf.Sign(slopeOffset); PositionState s = new PositionState(); if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) { state.pushesLeftTile = true; state.pushesLeftSlope = true; return true; } else if (move) { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesBottomTile = true; state.pushesBottomSlope = true; } } else if (slopeOffset &lt; 0 &amp;&amp; slopeOffset &gt; -Constants.cSlopeWallHeight) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y += slopeOffset - Mathf.Sign(slopeOffset); tr.y += slopeOffset - Mathf.Sign(slopeOffset); bl.y += slopeOffset - Mathf.Sign(slopeOffset); PositionState s = new PositionState(); if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) { state.pushesLeftTile = true; state.pushesLeftSlope = true; return true; } else if (move) { position.y += slopeOffset; bottomLeft.y += slopeOffset; topRight.y += slopeOffset; state.pushesTopTile = true; state.pushesTopSlope = true; } } } if (mSticksToSlope &amp;&amp; state.pushedBottomTile &amp;&amp; move) { var nextX = mMap.GetMapTileXAtPoint(topRight.x - 1.5f); var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1; var prevPos = mMap.GetMapTilePosition(new Vector2i(topRightTile.x, bottomLeftTile.y)); var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY)); var prevCollisionType = mMap.GetCollisionType(new Vector2i(topRightTile.x, bottomLeftTile.y)); var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY)); int x1 = (int)Mathf.Clamp((topRight.x - 1.0f - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); int x2 = (int)Mathf.Clamp((topRight.x - 1.5f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType); int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType); var offset = slopeHeight + Map.cTileSize - nextSlopeHeight; if (offset &lt; Constants.cSlopeWallHeight &amp;&amp; offset &gt; 0) { Vector2 pos = position, tr = topRight, bl = bottomLeft; pos.y -= offset - Mathf.Sign(offset); tr.y -= offset - Mathf.Sign(offset); bl.y -= offset - Mathf.Sign(offset); bl.x -= 1.0f; tr.x -= 1.0f; PositionState s = new PositionState(); if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) { position.y -= offset; bottomLeft.y -= offset; topRight.y -= offset; state.pushesBottomTile = true; state.pushesBottomSlope = true; } } } return false; }</pre><p>That's it. The code should be able to handle all manners of untranslated slopes.</p><figure class="post_image"><img alt="Animation of character moving on slope" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/slopes_untranslatedv2.gif"></figure><h4>Handle Translation Types</h4><p>Before we start handling translated tiles, let's make a few functions that will return whether a particular <code class="inline">TileCollisionType</code> is translated in a particular way. Our collision type enum is structured in this way:</p><pre class="brush: plain noskimlinks noskimwords">public enum TileCollisionType { Empty = 0, //normal tiles Full, OneWayPlatform, SlopesStart, //starting point for slopes Slope45, //basic version of the slope Slope45FX, //slope flipped on the X axis Slope45FY, //slope flipped on the Y axis Slope45FXY, //slope flipped on the X and Y axes Slope45F90, //slope rotated 90 degrees Slope45F90X, //slope rotated and flipped on X axis Slope45F90Y, //slope rotated and flipped on Y axis Slope45F90XY, //slope rotated and flipped on both axes ... }</pre><p>We can use these patterns to tell just by the value of the enum how is a particular collision type translated. Let's start by identifying flip on the X axis.</p><pre class="brush: csharp noskimlinks noskimwords">public static bool IsFlippedX(TileCollisionType type) { }</pre><p>First, let's get the slope id. We'll do that by calculating the offset from the first defined slope tile to the one we want to identify.</p><pre class="brush: csharp noskimlinks noskimwords">public static bool IsFlippedX(TileCollisionType type) { int typeId = (int)type - (int)TileCollisionType.SlopesStart + 1; }</pre><p>We have eight kinds of translations, so now all we need is get the remainder of dividing the <code class="inline">typeId</code> by 8.<br></p><pre class="brush: plain noskimlinks noskimwords">public static bool IsFlippedX(TileCollisionType type) { int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; }</pre><p>So now the translations have an assigned number for them.</p><pre class="brush: plain noskimlinks noskimwords">Slope45, //0 Slope45FX, //1 Slope45FY, //2 Slope45FXY, //3 Slope45F90, //4 Slope45F90X, //5 Slope45F90Y, //6 Slope45F90XY, //7</pre><p>The flip on the X axis is present in the types equal to 1, 3, 5, and 7, so if it's equal to one of those then the function should return true, otherwise return false.</p><pre class="brush: csharp noskimlinks noskimwords">public static bool IsFlippedX(TileCollisionType type) { int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; switch (typeId) { case 1: case 3: case 5: case 7: return true; } return false; }</pre><p>In the same way, let's create a function which tells whether a type is flipped on the Y axis.</p><pre class="brush: csharp noskimlinks noskimwords">public static bool IsFlippedY(TileCollisionType type) { int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; switch (typeId) { case 2: case 3: case 6: case 7: return true; } return false; }</pre><p>And finally, if the collision type is rotated.</p><pre class="brush: csharp noskimlinks noskimwords">public static bool IsFlipped90(TileCollisionType type) { int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; return (typeId &gt; 3); }</pre><p>That's all that we need.</p><h4>Transform the Offset</h4><p>Let's go back to the Slopes class and make our <code class="inline">GetOffset</code> function support the translated tiles.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { int posX, posY, sizeX, sizeY; float leftTileEdge = tileCenter.x - Map.cTileSize / 2; float rightTileEdge = leftTileEdge + Map.cTileSize; float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; float topTileEdge = bottomTileEdge + Map.cTileSize; SlopeOffsetI offset; posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); if (topTileEdge &lt; topY) { if (offset.freeDown &lt; 0) offset.freeDown -= (int)(topY - topTileEdge); offset.collidingTop = offset.freeDown; } if (bottomTileEdge &gt; bottomY) { if (offset.freeUp &gt; 0) offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY); offset.collidingBottom = offset.freeUp; } return offset; }</pre><p>As usual, since we don't have cached data for translated slopes, we'll be translating the object's position and size so the result is identical as if the tile has been translated. Let's start with the flip on the X axis. All we need to do here is flip the object along the center of the tile.</p><pre class="brush: csharp noskimlinks noskimwords">if (IsFlippedX(tileCollisionType)) { posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1); } else { posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); }</pre><p>Similarly for the flip on the Y axis.</p><pre class="brush: csharp noskimlinks noskimwords">if (IsFlippedY(tileCollisionType)) { posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1); } else { posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); }</pre><p>Now in case we flipped the tile on the y axis, the offsets we received are actually swapped. Let's translate them so they actually work the same way as the offsets of the untranslated tile, which means up is up and down is down!</p><pre class="brush: csharp noskimlinks noskimwords">if (IsFlippedY(tileCollisionType)) { int tmp = offset.freeDown; offset.freeDown = -offset.freeUp; offset.freeUp = -tmp; tmp = offset.collidingTop; offset.collidingTop = -offset.collidingBottom; offset.collidingBottom = -tmp; }</pre><p>Now let's handle the 90-degree rotation.</p><pre class="brush: plain noskimlinks noskimwords">if (!IsFlipped90(tileCollisionType)) { if (IsFlippedX(tileCollisionType)) { posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1); } else { posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); } if (IsFlippedY(tileCollisionType)) { posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1); } else { posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); } offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); if (IsFlippedY(tileCollisionType)) { int tmp = offset.freeDown; offset.freeDown = -offset.freeUp; offset.freeUp = -tmp; tmp = offset.collidingTop; offset.collidingTop = -offset.collidingBottom; offset.collidingBottom = -tmp; } } else { }</pre><p>Here everything should be rotated by 90 degrees, so instead of basing our <code class="inline">posX</code> and <code class="inline">sizeX</code> on the left and right edges of the object, we'll be basing them on the top and bottom.</p><pre class="brush: csharp noskimlinks noskimwords">if (IsFlippedY(tileCollisionType)) { posX = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(topY - (bottomTileEdge + posX), 0.0f, Map.cTileSize - 1); } else { posX = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp((topTileEdge - posX) - bottomY, 0.0f, Map.cTileSize - 1); } if (IsFlippedX(tileCollisionType)) { posY = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp((rightTileEdge - posY) - leftX, 0.0f, Map.cTileSize - 1); } else { posY = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(rightX - (leftTileEdge + posY), 0.0f, Map.cTileSize - 1); }</pre><p>Now we need to do a similar thing to what we did previously if the tile was flipped on the Y axis, but this time we need to do it for both the 90-degree rotation and the Y flip.</p><pre class="brush: csharp noskimlinks noskimwords">if (IsFlippedY(tileCollisionType)) { offset.collidingBottom = offset.collidingLeft; offset.freeDown = offset.freeLeft; offset.collidingTop = offset.collidingRight; offset.freeUp = offset.freeRight; } else { offset.collidingBottom = -offset.collidingRight; offset.freeDown = -offset.freeRight; offset.collidingTop = -offset.collidingLeft; offset.freeUp = -offset.freeLeft; }</pre><p>This is it. Since our final up and down offsets are adjusted to make sense in the world space, our out of tile bounds adjustments are still working properly.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetI GetOffsetHeight(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { int posX, posY, sizeX, sizeY; float leftTileEdge = tileCenter.x - Map.cTileSize / 2; float rightTileEdge = leftTileEdge + Map.cTileSize; float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; float topTileEdge = bottomTileEdge + Map.cTileSize; SlopeOffsetI offset; if (!IsFlipped90(tileCollisionType)) { if (IsFlippedX(tileCollisionType)) { posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1); } else { posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); } if (IsFlippedY(tileCollisionType)) { posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1); } else { posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); } offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); if (IsFlippedY(tileCollisionType)) { int tmp = offset.freeDown; offset.freeDown = -offset.freeUp; offset.freeUp = -tmp; tmp = offset.collidingTop; offset.collidingTop = -offset.collidingBottom; offset.collidingBottom = -tmp; } } else { if (IsFlippedY(tileCollisionType)) { posX = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(topY - (bottomTileEdge + posX), 0.0f, Map.cTileSize - 1); } else { posX = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp((topTileEdge - posX) - bottomY, 0.0f, Map.cTileSize - 1); } if (IsFlippedX(tileCollisionType)) { posY = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp((rightTileEdge - posY) - leftX, 0.0f, Map.cTileSize - 1); } else { posY = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(rightX - (leftTileEdge + posY), 0.0f, Map.cTileSize - 1); } offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); if (IsFlippedY(tileCollisionType)) { offset.collidingBottom = offset.collidingLeft; offset.freeDown = offset.freeLeft; offset.collidingTop = offset.collidingRight; offset.freeUp = offset.freeRight; } else { offset.collidingBottom = -offset.collidingRight; offset.freeDown = -offset.freeRight; offset.collidingTop = -offset.collidingLeft; offset.freeUp = -offset.freeLeft; } } if (topTileEdge &lt; topY) { if (offset.freeDown &lt; 0) offset.freeDown -= (int)(topY - topTileEdge); offset.collidingTop = offset.freeDown; } if (bottomTileEdge &gt; bottomY) { if (offset.freeUp &gt; 0) offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY); offset.collidingBottom = offset.freeUp; } return offset; }</pre><p>That's it—now we can use translated slopes as well. </p><figure class="post_image"><img alt="Animation of character moving on slope" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/slopes_translatedv2.gif"></figure><p>On the animation above, there are 45, 22, 15 and 11-degree slopes. Thanks to the 90-degree rotations, we also can get 79, 75 and 68-degree slopes without defining additional slope tiles. You can also see that the 79-degree slope is too steep to move on smoothly with our value of <code class="inline">cSlopeWallHeight</code>.</p><h3>Handle One-Way Platforms</h3><p>In all this hassle, we've broken our support for one-way platforms. We need to fix that, and extend the functionality to slopes as well. One-way platforms are as important or often even more important than the solid tiles, so we can't afford to miss them.</p><h4>Add the One-Way Types</h4><p>The first thing we need to do is to add new collision types for one-way platforms. We'll add them past the non-one-way collision types and also mark where they start, so later on we have an easy time telling whether a particular collision type is one-way or not.</p><pre class="brush: csharp noskimlinks noskimwords">public enum TileCollisionType { Empty = 0, Full, SlopesStart, ... Slope45, Slope45FX, Slope45FY, Slope45FXY, Slope45F90, Slope45F90X, Slope45F90Y, Slope45F90XY, //... OneWayStart, OneWaySlope45, OneWaySlope45FX, OneWaySlope45FY, OneWaySlope45FXY, OneWaySlope45F90, OneWaySlope45F90X, OneWaySlope45F90Y, OneWaySlope45F90XY, //... SlopeEnd = OneWaySlopeMid4RevF90XY, OneWayFull, OneWayEnd, Count, }</pre><p>Now all one-way platforms are between the <code class="inline">OneWayStart</code> and <code class="inline">OneWayEnd</code> enums, so we can easily create a function which will return this information.</p><pre class="brush: csharp noskimlinks noskimwords">public static bool IsOneWay(TileCollisionType type) { return ((int)type &gt; (int)TileCollisionType.OneWayStart &amp;&amp; (int)type &lt; (int)TileCollisionType.OneWayEnd); }</pre><p>The one-way variants of slopes should point to the same data that the non-one-way platforms do, so no worries of extending memory requirements further here.</p><pre class="brush: csharp noskimlinks noskimwords">case TileCollisionType.Slope45: slopesHeights[i] = slope45; slopesExtended[i] = Extend(slopesHeights[i]); posByHeightCaches[i] = CachePosByHeight(slopesHeights[i]); slopeHeightByPosAndSizeCaches[i] = CacheSlopeHeightByPosAndLength(slopesHeights[i]); slopeOffsets[i] = CacheSlopeOffsets(slopesExtended[i]); break; case TileCollisionType.Slope45FX: case TileCollisionType.Slope45FY: case TileCollisionType.Slope45FXY: case TileCollisionType.Slope45F90: case TileCollisionType.Slope45F90X: case TileCollisionType.Slope45F90XY: case TileCollisionType.Slope45F90Y: case TileCollisionType.OneWaySlope45: case TileCollisionType.OneWaySlope45FX: case TileCollisionType.OneWaySlope45FY: case TileCollisionType.OneWaySlope45FXY: case TileCollisionType.OneWaySlope45F90: case TileCollisionType.OneWaySlope45F90X: case TileCollisionType.OneWaySlope45F90XY: case TileCollisionType.OneWaySlope45F90Y: slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; posByHeightCaches[i] = posByHeightCaches[(int)TileCollisionType.Slope45]; slopeHeightByPosAndSizeCaches[i] = slopeHeightByPosAndSizeCaches[(int)TileCollisionType.Slope45]; slopeOffsets[i] = slopeOffsets[(int)TileCollisionType.Slope45]; break;</pre><h4>Cover the Additional Data</h4><p>Now let's add variables which will allow us to make an object ignore one-way platforms. One will be an object flag, which will basically be for setting permanent ignoring of one-way platforms—this will be useful for flying monsters and other objects which do not have any need for using the platforms, and another flag to temporarily disable collision with one-way platforms, just for the sake of falling through them.</p><p>The first variable will be inside the <code class="inline">MovingObject</code> class.</p><pre class="brush: csharp noskimlinks noskimwords">public bool mIgnoresOneWay = false; public bool mOnOneWayPlatform = false; public bool mSticksToSlope = true; public bool mIsKinematic = false;</pre><p>The second one is inside the <code class="inline">PositionState</code> structure.</p><pre class="brush: csharp noskimlinks noskimwords">public bool onOneWay; public bool tmpIgnoresOneWay;</pre><p>We'll also add another variable here which will hold the Y coordinate of the platform we want to skip.</p><pre class="brush: csharp noskimlinks noskimwords">public bool onOneWay; public bool tmpIgnoresOneWay; public int oneWayY;</pre><p>To make one-way platforms work, we'll simply be ignoring a single horizontal layer of platforms. As we enter another layer, that is our character's Y position has changed in the map coordinates, then we set the character to collide with the one-way platforms again.</p><h4>Modify the Collision Checks</h4><p>Let's go to our <code class="inline">CollidesWithTileBottom</code> function. First of all, as we iterate through tiles, let's check if it's a one-way platform, and if so, whether we should even consider colliding with this tile or not.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f)); bool isOneWay; for (int x = bottomleftTile.x; x &lt;= topRightTile.x; ++x) { var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y); isOneWay = Slopes.IsOneWay(tileCollisionType); if ((mIgnoresOneWay || state.tmpIgnoresOneWay) &amp;&amp; isOneWay) continue;</pre><p>We should collide with one-way platforms only if the distance to the top of the platform is less than the <code class="inline">cSlopeWallHeightConstant</code>, so we can actually come on top of it. Let's add this to the condition already laid out, and we also need to assign proper values to <code class="inline">state.onOneWay</code> and <code class="inline">state.oneWayY</code>.</p><pre class="brush: csharp noskimlinks noskimwords">if (((sf.freeUp &gt;= 0 &amp;&amp; sf.collidingBottom == sf.freeUp) || (mSticksToSlope &amp;&amp; state.pushedBottom &amp;&amp; sf.freeUp - sf.collidingBottom &lt; Constants.cSlopeWallHeight &amp;&amp; sf.freeUp &gt;= sf.collidingBottom)) &amp;&amp; !(isOneWay &amp;&amp; Mathf.Abs(sf.collidingBottom) &gt;= Constants.cSlopeWallHeight)) { state.onOneWay = isOneWay; state.oneWayY = bottomleftTile.y; state.pushesBottomTile = true; state.bottomTile = new Vector2i(x, bottomleftTile.y); position.y += sf.collidingBottom; topRight.y += sf.collidingBottom; bottomLeft.y += sf.collidingBottom; return true; }</pre><p>For the <code class="inline">CollidesWithTileTop</code> function, we simply ignore one-way platforms.</p><pre class="brush: csharp noskimlinks noskimwords"> public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); for (int x = bottomleftTile.x; x &lt;= topRightTile.x; ++x) { var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y); if (Slopes.IsOneWay(tileCollisionType)) continue;</pre><p>For the horizontal collision check, there will be a bit more work. First off, let's create two additional booleans at the beginning, which will serve as information about whether the currently processed tile is one-way, and whether the tile from the previous iteration has been a one-way platform.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; bool wasOneWay = false, isOneWay; TileCollisionType slopeCollisionType = TileCollisionType.Empty;</pre><p>Now we're interested in iterating through a one-way platform if we're moving along it. We can't really collide with one-way platforms from right or left, but if the character moves along a slope that's also a one-way platform, then it needs to be handled in the same way that a normal slope would.</p><pre class="brush: plain noskimlinks noskimwords">public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; bool wasOneWay = false, isOneWay; TileCollisionType slopeCollisionType = TileCollisionType.Empty; for (int y = bottomLeftTile.y; y &lt;= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); isOneWay = Slopes.IsOneWay(tileCollisionType); if (isOneWay &amp;&amp; (!move || mIgnoresOneWay || state.tmpIgnoresOneWay || y != bottomLeftTile.y)) continue;</pre><p>Now make sure we can't collide with a slope as if it was a wall.</p><pre class="brush: csharp noskimlinks noskimwords">if (!isOneWay &amp;&amp; (Mathf.Abs(slopeOffset) &gt;= Constants.cSlopeWallHeight || (slopeOffset &lt; 0 &amp;&amp; state.pushesBottomTile) || (slopeOffset &gt; 0 &amp;&amp; state.pushesTopTile))) { state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; }</pre><p>And if that's not the case and the offset is small enough to climb it, then remember that we're moving along a one-way platform now.</p><pre class="brush: csharp noskimlinks noskimwords">if (!isOneWay &amp;&amp; (Mathf.Abs(slopeOffset) &gt;= Constants.cSlopeWallHeight || (slopeOffset &lt; 0 &amp;&amp; state.pushesBottomTile) || (slopeOffset &gt; 0 &amp;&amp; state.pushesTopTile))) { state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; } else if (Mathf.Abs(slopeOffset) &gt; Mathf.Abs(oldSlopeOffset)) { wasOneWay = isOneWay; slopeCollisionType = tileCollisionType; state.rightTile = new Vector2i(topRightTile.x, y); }</pre><p>Now what's left here is to make sure that every time we change the position state we also need to update the <code class="inline">onOneWay</code> variable.</p><pre class="brush: csharp noskimlinks noskimwords">state.onOneWay = wasOneWay;</pre><h4>Jumping Down</h4><p>We need to stop ignoring the one-way platforms once we change the Y position in the map coordinates. We're going to set up our condition after the movement on the Y axis in the Move function. We need to add it at the end of the second case.</p><pre class="brush: csharp noskimlinks noskimwords">else if (move.y != 0.0f &amp;&amp; move.x == 0.0f) { MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); if (step.y &gt; 0.0f) state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); else state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); if (!mIgnoresOneWay &amp;&amp; state.tmpIgnoresOneWay &amp;&amp; mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY) state.tmpIgnoresOneWay = false; }</pre><p>And also at the end of the third case.</p><pre class="brush: csharp noskimlinks noskimwords">else { float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); float vertAccum = 0.0f; while (!foundObstacleX &amp;&amp; !foundObstacleY &amp;&amp; (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); move.x -= step.x; while (!foundObstacleY &amp;&amp; move.y != 0.0f &amp;&amp; (Mathf.Abs(vertAccum) &gt;= 1.0f || move.x == 0.0f)) { move.y -= step.y; vertAccum -= step.y; MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); } } if (step.x &gt; 0.0f) state.pushesLeftTile = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); else state.pushesRightTile = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); if (step.y &gt; 0.0f) state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); else state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); if (!mIgnoresOneWay &amp;&amp; state.tmpIgnoresOneWay &amp;&amp; mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY) state.tmpIgnoresOneWay = false; }</pre><p>That should do it. Now the only thing we need to do for a character to drop from a one-way platform is to set its <code class="inline">tmpIgnoresOneWay</code> to true.</p><pre class="brush: csharp noskimlinks noskimwords">if (KeyState(KeyInput.GoDown)) mPS.tmpIgnoresOneWay = true;</pre><p>Let's see how this looks in action.</p><figure class="post_image"><img alt="New animation of character moving on slope" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/one_way_slopesv2.gif"></figure><h2>Summary</h2><p>Whew, that was a lot of work, but it was worth it. The result is very flexible and robust. We can define any kind of slope thanks to our handling of collision bitmaps, translate the tiles, and turn them into one-way platforms. </p><p>This implementation still isn't optimized, and I'm sure I've missed a lot of opportunities for that handed by our new one-pixel integration method. I'm also pretty sure that a lot of additional collision checks could be skipped, so if you improve this implementation then let me know in the comments section! </p><p>Thanks for sticking with me this far, and I hope this tutorial is of use to you!</p> 2017-07-20T13:00:53.000Z 2017-07-20T13:00:53.000Z Daniel Branicki tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-28472 Basic 2D Platformer Physics, Part 7: Slopes Groundwork <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/final_image/part_8_finalv2.gif" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><h2>Demo</h2><p>The demo shows the end result of the slope implementation. Use WASD to move the character. Right mouse button creates a tile. You can use the scroll wheel or the arrow keys to select a tile you want to place. The sliders change the size of the player's character.</p><p>The demo has been published under Unity 5.5.2f1, and the source code is also compatible with this version of Unity.</p><h2>Slopes</h2><p>Slopes add a lot of versatility to a game, both in terms of possible interactions with the game's terrain and in visual variance, but their implementation can be very complex, especially if we'd like to support a vast number of slope types.</p><p>As was true for the previous parts in the series, we'll be continuing our work from the moment we left off the last part, even though we'll be reworking a big chunk of the code we've already written. What we'll need from the start is a working moving character and a tilemap. </p><p>You can download the project files from the previous part and write the code along with this tutorial.</p><h3>Changes in Movement Integration</h3><p>Since making the slopes work is pretty difficult, it'd be nice if we could actually make things easier in some aspects. Some time ago I stumbled upon a blog post on how <a href="http://www.mattmakesgames.com/" rel="external" target="_blank">Matt Thorson</a> handles the physics in <a href="http://mattmakesgames.tumblr.com/post/127890619821/towerfall-physics" rel="external" target="_blank">his games</a>. Basically, in this method the movement is always made in 1px intervals. If a movement for a particular frame is larger than one pixel, then the movement vector is split into many 1px movements, and after each one the conditions for collision with the terrain are checked. </p><p>This saves us the headache of trying to find obstacles along the line of movement at once, and instead, we can do it iteratively. This makes the implementation simpler, but unfortunately it also increases the number of collision checks performed, so it might be inappropriate for games where there are many moving objects, especially high-resolution games where naturally the speed at which the objects are moving is higher. The plus side is that even though there will be more collision checks, each check will be much simpler since it knows that the character moves by a single pixel each time.</p><h3>Slopes Data</h3><p>Let's start defining the data that we'll need to represent the slopes. First of all, we'll need a height map of a slope, which will define its shape. Let's start with a classic 45-degree slope.</p><figure class="post_image"><img alt="Slopes Data" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/slope45.png"></figure><p>Let's also define another slope shape; this one will serve as more of a bump on the ground than anything else.</p><figure class="post_image"><img alt="Another slope shape" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/slope_mid1.png"></figure><p>Of course we will want to use variants of these slopes, depending where we'd like to place them. For example, in the case of our defined 45-degree slope, it will fit nicely if there's a solid block to its right, but if the solid block is on its left then we'd like to use a flipped version of the tile we defined. We'll need to be able to flip the slopes on the X axis and Y axis as well as rotate them by 90 degrees to be able to access all the variants of a predefined slope.</p><p>Let's look at what the transformations of the 45-degree slope look like.</p><figure class="post_image"><img alt="Transformations of the 45 degree slope" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/transforms1v2.png"></figure><p>As you can see, in this case we can get all the variants using flips. We don't really need to rotate the slope by 90 degrees, but let's see how things look for the second slope we defined earlier.</p><figure class="post_image"><img alt="Rotating slopes" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/transforms2.png"></figure><p>In this case, the 90-degree rotation transform makes it possible to place the slope on the wall.</p><h3>Calculate Offsets</h3><p>Let's use our defined data to calculate the offsets which will need to be applied to the object that is overlapping with a tile. The offset will carry the information about:</p><ul> <li>how much an object needs to move up/down/left/right in order not to collide with a tile</li> <li>how much an object needs to move to be right next to the top/bottom/left/right surface of a slope</li> </ul><figure class="post_image"><img alt="Calculating Offsets" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/overlap.png"></figure><p>The green parts of the above image are the parts where the object overlaps with the empty parts of the tile, and the yellow squares indicate the area in which the object overlaps with the slope.</p><p>Now, let's start to see how we'd calculate the offset for case number 1. </p><p>The object does not collide with any part of the slope. That means we don't really need to move it out of collision, so the first part of our offset will be set to 0.</p><p>For the second part of the offset, if we want the bottom of the object to be touching the slope, we would need to move it 3 pixels down. If we wanted the object's right side to touch the slope, we would need to move it 3 pixels to the right. For the object's left side to touch the right edge of the slope, we'd need to move it 16 pixels to the right. Similarly, if we wanted the top edge of the object to touch the slope, we'd need to move the object 16 pixels down.<br></p><p>Now, why would we need the information of how much distance there is between the object's edge and the slope? This data will be very useful to us when we want an object to stick to the slope. </p><p>So, for example, let's say an object moves left on our 45-degree slope. If it moves fast enough it will end up in the air, and then eventually it will fall on the slope again, and so on. If we want it to remain on the slope, each time it moves left, we'll want to push it down so it remains in touch with the slope. The below animation shows the difference between having slope sticking enabled or disabled for a character.</p><figure class="post_image"><img alt="Animation of moving down a slope" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/slope_sticking_d.gif"></figure><p>We'll be caching a lot of data here—basically, we want to calculate an offset for every possible overlap with a tile. This means that for every position and for every overlap size, we'll have a quick reference of how much to move an object. Note that we cannot cache the final offsets because we can't cache an offset for every possible AABB, but it's easy to adjust the offset knowing the AABB's overlap with the slope's tile.</p><h4>Defining Tiles</h4><p>We'll be defining all the slope data in a static Slopes class.</p><pre class="brush: csharp noskimlinks noskimwords">public static class Slopes { }</pre><p>First of all, let's handle the heightmaps. Let's define a few of them to process later on.</p><pre class="brush: csharp noskimlinks noskimwords">public static readonly sbyte[] empty = new sbyte { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; public static readonly sbyte[] full = new sbyte { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; public static readonly sbyte[] slope45 = new sbyte { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; public static readonly sbyte[] slopeMid1 = new sbyte { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 };</pre><p>Let's add the test tile types for the defined slopes.</p><pre class="brush: csharp noskimlinks noskimwords">public enum TileType { Empty, Block, OneWay, TestSlopeMid1, TestSlopeMid1FX, TestSlopeMid1FY, TestSlopeMid1FXY, TestSlopeMid1F90, TestSlopeMid1F90X, TestSlopeMid1F90Y, TestSlopeMid1F90XY, TestSlope45, TestSlope45FX, TestSlope45FY, TestSlope45FXY, TestSlope45F90, TestSlope45F90X, TestSlope45F90Y, TestSlope45F90XY, Count }</pre><p>Let's also create another enumeration for tile collision type. This will be useful for assigning the same collision type to different tiles, for example a grassy 45-degree slope or stone 45-degree slope.</p><pre class="brush: csharp noskimlinks noskimwords">public enum TileCollisionType { Empty, Block, OneWay, SlopeMid1, SlopeMid1FX, SlopeMid1FY, SlopeMid1FXY, SlopeMid1F90, SlopeMid1F90X, SlopeMid1F90Y, SlopeMid1F90XY, Slope45, Slope45FX, Slope45FY, Slope45FXY, Slope45F90, Slope45F90X, Slope45F90Y, Slope45F90XY, Count }</pre><p>Now let's create an array which will hold all the tiles' heightmaps. This array will be indexed by the <code class="inline">TileCollisionType</code> enumeration.</p><pre class="brush: csharp noskimlinks noskimwords">public static readonly sbyte[] empty = new sbyte { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; public static readonly sbyte[] full = new sbyte { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; public static readonly sbyte[] slope45 = new sbyte { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; public static readonly sbyte[] slopeMid1 = new sbyte { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 }; public static sbyte[][] slopesHeights;</pre><h4>Processing the Slopes</h4><p>Before we start calculating the offsets, we'll want to unfold our heightmaps into full collision bitmaps. This will make it easy to determine whether an AABB is colliding with a tile and also will enable more complex tile shapes if that's what we need. Let's create an array for those bitmaps.<br></p><pre class="brush: csharp noskimlinks noskimwords">public static readonly sbyte[] empty = new sbyte { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; public static readonly sbyte[] full = new sbyte { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; public static readonly sbyte[] slope45 = new sbyte { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; public static readonly sbyte[] slopeMid1 = new sbyte { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 }; public static sbyte[][] slopesHeights; public static sbyte[][][] slopesExtended;</pre><p>Now let's create a function which will extend the heightmap into the bitmap.</p><pre class="brush: csharp noskimlinks noskimwords">public static sbyte[][] Extend(sbyte[] slope) { sbyte[][] extended = new sbyte[Map.cTileSize][]; for (int x = 0; x &lt; Map.cTileSize; ++x) { extended[x] = new sbyte[Map.cTileSize]; for (int y = 0; y &lt; Map.cTileSize; ++y) extended[x][y] = System.Convert.ToSByte(y &lt; slope[x]); } return extended; }</pre><p>Nothing complicated here—if a particular position on the tile is solid, we set it to 1; if it's not, it's set to 0.</p><p>Now let's create our <code class="inline">Init</code> function, which will eventually do all the caching work we need to have done on the slopes.</p><pre class="brush: csharp noskimlinks noskimwords">public static void Init() { }</pre><p>Let's create the container arrays here.</p><pre class="brush: plain noskimlinks noskimwords">public static void Init() { slopesHeights = new sbyte[(int)TileCollisionType.Count][]; slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; }</pre><p>Now let's make every tile collision type point to the corresponding cached data.</p><pre class="brush: csharp noskimlinks noskimwords">for (int i = 0; i &lt; (int)TileCollisionType.Count; ++i) { switch ((TileCollisionType)i) { case TileCollisionType.Empty: slopesHeights[i] = empty; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.Full: slopesHeights[i] = full; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.Slope45: slopesHeights[i] = slope45; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.Slope45FX: case TileCollisionType.Slope45FY: case TileCollisionType.Slope45FXY: case TileCollisionType.Slope45F90: case TileCollisionType.Slope45F90X: case TileCollisionType.Slope45F90XY: case TileCollisionType.Slope45F90Y: slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; break; case TileCollisionType.SlopeMid1: slopesHeights[i] = slopeMid1; slopesExtended[i] = Extend(slopesHeights[i]); break; case TileCollisionType.SlopeMid1FX: case TileCollisionType.SlopeMid1FY: case TileCollisionType.SlopeMid1FXY: case TileCollisionType.SlopeMid1F90: case TileCollisionType.SlopeMid1F90X: case TileCollisionType.SlopeMid1F90XY: case TileCollisionType.SlopeMid1F90Y: slopesHeights[i] = slopesHeights[(int)TileCollisionType.SlopeMid1]; slopesExtended[i] = slopesExtended[(int)TileCollisionType.SlopeMid1]; break; } }</pre><h4>Offset Structure</h4><p>Now we can define our offset structure.</p><pre class="brush: csharp noskimlinks noskimwords">public struct SlopeOffsetSB { public sbyte freeLeft, freeRight, freeDown, freeUp, collidingLeft, collidingRight, collidingBottom, collidingTop; public SlopeOffsetSB(sbyte _freeLeft, sbyte _freeRight, sbyte _freeDown, sbyte _freeUp, sbyte _collidingLeft, sbyte _collidingRight, sbyte _collidingBottom, sbyte _collidingTop) { freeLeft = _freeLeft; freeRight = _freeRight; freeDown = _freeDown; freeUp = _freeUp; collidingLeft = _collidingLeft; collidingRight = _collidingRight; collidingBottom = _collidingBottom; collidingTop = _collidingTop; } }</pre><p>As explained before, the <code class="inline">freeLeft</code>, <code class="inline">freeRight</code>, <code class="inline">freeDown</code>, and <code class="inline">freeUp</code> variables correspond to the offset that needs to be applied so the object is no longer colliding with the slope, while the <code class="inline">collidingLeft</code>, <code class="inline">collidingRight</code>, <code class="inline">collidingTop</code>, and <code class="inline">collidingBottom</code> are the distance that the object needs to be shifted to touch the slope while not overlapping it.</p><p>It's time to create our heavy-duty caching function, but just before we do it, let's create a container which will hold all that data.</p><pre class="brush: csharp noskimlinks noskimwords">public static sbyte[][] slopesHeights; public static sbyte[][][] slopesExtended; public static SlopeOffsetSB[][][][][] slopeOffsets;</pre><p>And create the array in the <code class="inline">Init</code> function.</p><pre class="brush: csharp noskimlinks noskimwords">slopesHeights = new sbyte[(int)TileCollisionType.Count][]; slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][];</pre><h4>Memory Issues</h4><p>As you can see, this array has plenty of dimensions, and each new tile type will actually require quite a lot of memory. For every X position in the tile, for every Y position in the tile, for every possible Width in the tile and for every possible Height, there will be a separate offset value calculation. </p><p>Since the tiles we are using are 16x16, this means that the amount of data needed for each tile type will be 16*16*16*16*8 bytes, which equals 512 kB. This is a lot of data, but still manageable, and of course if caching this amount of information is unfeasible, we'll need to either switch to calculating the offsets in real time, probably using a more efficient method than the one we're using for caching, or optimize our data. </p><p>Right now, if the tile size in our game was bigger, say 32x32, each tile type would occupy 8 MB, and if we used 64x64, then it would be 128MB. These amounts seem way too big to be useful, especially if we want to have quite a few slope types in the game. A sensible solution to this seems to be splitting the big collision tiles into smaller ones. Note that it is just each newly defined slope that requires more space—the transformations use the same data.</p><h4>Checking Collisions Within a Tile</h4><p>Before we start calculating the offsets, we need to know if an object at a particular position will collide with the solid parts of the tile. Let's create this function first.</p><pre class="brush: csharp noskimlinks noskimwords">public static bool Collides(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) { for (int x = posX; x &lt;= posX + w &amp;&amp; x &lt; Map.cTileSize; ++x) { for (int y = posY; y &lt;= posY + h &amp;&amp; y &lt; Map.cTileSize; ++y) { if (slopeExtended[x][y] == 1) return true; } } return false; }</pre><p>The function takes the collision bitmap, the position of the overlap, and the overlap size. The position is the bottom left pixel of the object, and the size is the 0-based width and height. By 0-based, I mean that width of 0 means that the object is actually 1 pixel wide, and width equal to 15 means that the object is 16 pixels wide. The function is very simple—if any of the object's pixels overlap with a slope, then we return true, otherwise we return false.</p><h4>Calculate the Offsets</h4><p>Now let's start calculating the offsets.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetSB GetOffset(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) { }</pre><p>Again, to calculate the offset, we'll need the collision bitmap, position and size of the overlap. Let's start by declaring the offset values.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetSB GetOffset(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) { sbyte freeUp = 0, freeDown = 0, collidingTop = 0, collidingBottom = 0; sbyte freeLeft = 0, freeRight = 0, collidingLeft = 0, collidingRight = 0; }</pre><p>Now let's calculate how much we need to move the object to make it not collide with the slope. To do that, while the object is colliding with the slope we need to keep moving it up and checking for collision until there's no overlap with the solid parts of the tile.</p><figure class="post_image"><img alt="No overlaps" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/offset_calc.gif"></figure><p>Above is the illustration of how we calculate the offset. In the first case, since the object is touching the top bound of the tile, instead of just moving it up we also need to decrease its height. That's because if any part of the AABB moves outside the tile bounds, we are no longer interested in it. Similarly, offsets are calculated for all other directions, so for the above example the offsets would be:</p><ul> <li>4 for the up offset</li> <li>-4 for the left offset</li> <li>-16 for the down offset—that's the maximum distance because basically if we move the object down, we need to move it all the way out of the bounds of the tile to stop colliding with the slope</li> <li>16 for the right offset</li> </ul><p>Let's start by declaring the temporary variable for the height of the object. As mentioned above, this will change depending on how high we'll be moving the object.</p><pre class="brush: csharp noskimlinks noskimwords">sbyte movH = h;</pre><p>Now it's time for the main condition. As long as the object hasn't moved out of the tile bounds and it collides with the solid parts of the tile, we need to increase the <code class="inline">offsetUp</code>.</p><pre class="brush: csharp noskimlinks noskimwords">sbyte movH = h; while (movH &gt;= 0 &amp;&amp; posY + freeUp &lt; Map.cTileSize &amp;&amp; Collides(slopeExtended, posX, (sbyte)(posY + freeUp), w, movH)) { ++freeUp; }</pre><p>Finally, let's adjust the size of the object-tile overlapping area if the object moves outside the bounds of the tile.</p><pre class="brush: csharp noskimlinks noskimwords">sbyte movH = h; while (movH &gt;= 0 &amp;&amp; posY + freeUp &lt; Map.cTileSize &amp;&amp; Collides(slopeExtended, posX, (sbyte)(posY + freeUp), w, movH)) { if (posY + freeUp == Map.cTileSize) --movH; ++freeUp; }</pre><p>Now let's do the same thing for the left offset. Note that when we're moving the object left and the object is being moved out of the tile bounds, we don't really need to alter the position; instead, we just change the width of the overlap. This is illustrated on the right side of the animation illustrating the offset calculation.</p><pre class="brush: csharp noskimlinks noskimwords">movW = w; while (movW &gt;= 0 &amp;&amp; posX + freeLeft &gt;= 0 &amp;&amp; Collides(slopeExtended, (sbyte)(posX + freeLeft), posY, movW, h)) { if (posX + freeLeft == 0) --movW; else --freeLeft; }</pre><p>But here, since we weren't moving the <code class="inline">freeLeft</code> offset along the way as we were decreasing the width, we need to convert the reduced size into the offset.</p><pre class="brush: csharp noskimlinks noskimwords">movW = w; while (movW &gt;= 0 &amp;&amp; posX + freeLeft &gt;= 0 &amp;&amp; Collides(slopeExtended, (sbyte)(posX + freeLeft), posY, movW, h)) { if (posX + freeLeft == 0) --movW; else --freeLeft; } freeLeft -= (sbyte)(w - movW);</pre><p>Now let's do the same thing for the down and right offsets.</p><pre class="brush: csharp noskimlinks noskimwords">movH = h; while (movH &gt;= 0 &amp;&amp; posY + freeDown &gt;= 0 &amp;&amp; Collides(slopeExtended, posX, (sbyte)(posY + freeDown), w, movH)) { if (posY + freeDown == 0) --movH; else --freeDown; } freeDown -= (sbyte)(h - movH); sbyte movW = w; while (movW &gt;= 0 &amp;&amp; posY + freeRight &lt; Map.cTileSize &amp;&amp; Collides(slopeExtended, (sbyte)(posX + freeRight), posY, movW, h)) { if (posX + freeRight == Map.cTileSize) --movW; ++freeRight; } </pre><p>Alright, we've calculated the first part of the offset—that is how much we should move the object for it to stop colliding with the slope. Now it's time to figure out the offsets which are supposed to move the object right next to the solid parts of the tile. </p><p>Notice that if we need to move the object out of collision, we're already doing that, because we stop right after the collision is no more.</p><figure class="post_image"><img alt="Move the object out of collision" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/overlap.png"></figure><p>In the case on the right, the up offset is 4, but it is also the offset that we need to move the object for its bottom edge to sit on a solid pixel. The same goes for the other sides.</p><pre class="brush: csharp noskimlinks noskimwords">if (freeUp == 0) { } else collidingBottom = freeUp;</pre><p>Now the case on the left is where we need to find the offsets ourselves. If we want to find the <code class="inline">collidingBottom</code> offset there, we need to move the object 3 pixels down. The calculations needed here are similar to previous ones, but instead this time we'll be looking for when the object will collide with the slope, and then moving while reducing the offset by one, so it barely touches the solid pixels instead of overlapping them.</p><pre class="brush: csharp noskimlinks noskimwords">if (freeUp == 0) { while ( posY + collidingBottom &gt;= 0 &amp;&amp; !Collides(slopeExtended, posX, (sbyte)(posY + collidingBottom), w, h)) --collidingBottom; collidingBottom += 1; } else { collidingBottom = freeUp; }</pre><p>If <code class="inline">freeUp</code> is equal the 0, free down must be equal to 0 as well, so we can throw in the calculations for <code class="inline">collidingTop</code> under the same brackets. Again, these calculations are analogous to what we've been doing so far.</p><pre class="brush: csharp noskimlinks noskimwords">if (freeUp == 0) { while (posY + h + collidingTop &lt; Map.cTileSize &amp;&amp; !Collides(slopeExtended, posX, (sbyte)(posY + collidingTop), w, h)) ++collidingTop; collidingTop -= 1; while ( posY + collidingBottom &gt;= 0 &amp;&amp; !Collides(slopeExtended, posX, (sbyte)(posY + collidingBottom), w, h)) --collidingBottom; collidingBottom += 1; } else { collidingBottom = freeUp; collidingTop = freeDown; }</pre><p>Let's do the same for the left and right offsets.</p><pre class="brush: csharp noskimlinks noskimwords">if (freeRight == 0) { while (posX + w + collidingRight &lt; Map.cTileSize &amp;&amp; !Collides(slopeExtended, (sbyte)(posX + collidingRight), posY, w, h)) ++collidingRight; collidingRight -= 1; while (posX + collidingLeft &gt;= 0 &amp;&amp; !Collides(slopeExtended, (sbyte)(posX + collidingLeft), posY , w, h)) --collidingLeft; collidingLeft += 1; } else { collidingLeft = freeRight; collidingRight = freeLeft; }</pre><h4>Caching the Data</h4><p>Now that all the offsets are calculated, we can return the offset for this particular data set.</p><pre class="brush: csharp noskimlinks noskimwords"> return new SlopeOffsetSB(freeLeft, freeRight, freeDown, freeUp, collidingLeft, collidingRight, collidingBottom, collidingTop);</pre><p>Let's create a container for all our cached data.</p><pre class="brush: csharp noskimlinks noskimwords">public static sbyte[][] slopesHeights; public static sbyte[][][] slopesExtended; public static SlopeOffsetSB[][][][][] slopeOffsets;</pre><p>Initialize the array.</p><pre class="brush: csharp noskimlinks noskimwords">slopesHeights = new sbyte[(int)TileCollisionType.Count][]; slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][];</pre><p>And finally, create the caching function.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetSB[][][][] CacheSlopeOffsets(sbyte[][] slopeExtended) { var offsetCache = new SlopeOffsetSB[Map.cTileSize][][][]; for (int x = 0; x &lt; Map.cTileSize; ++x) { offsetCache[x] = new SlopeOffsetSB[Map.cTileSize][][]; for (int y = 0; y &lt; Map.cTileSize; ++y) { offsetCache[x][y] = new SlopeOffsetSB[Map.cTileSize][]; for (int w = 0; w &lt; Map.cTileSize; ++w) { offsetCache[x][y][w] = new SlopeOffsetSB[Map.cTileSize]; for (int h = 0; h &lt; Map.cTileSize; ++h) { offsetCache[x][y][w][h] = GetOffset(slopeExtended, (sbyte)x, (sbyte)y, (sbyte)w, (sbyte)h); } } } } return offsetCache; }</pre><p>The function itself is very simple, so it's very easy to see how much data it caches to satisfy our requirements!</p><p>Now make sure to cache the offsets for each tile collision type.</p><pre class="brush: plain noskimlinks noskimwords">case TileCollisionType.Slope45: slopesHeights[i] = slope45; slopesExtended[i] = Extend(slopesHeights[i]); slopeOffsets[i] = CacheSlopeOffsets(slopesExtended[i]); break; case TileCollisionType.Slope45FX: case TileCollisionType.Slope45FY: case TileCollisionType.Slope45FXY: case TileCollisionType.Slope45F90: case TileCollisionType.Slope45F90X: case TileCollisionType.Slope45F90XY: case TileCollisionType.Slope45F90Y: slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; slopeOffsets[i] = slopeOffsets[(int)TileCollisionType.Slope45]; break;</pre><p>And that's it, our main caching function is finished!</p><h3>Calculating the World Space Offset</h3><p>Now let's use the cached data to make a function which will return an offset for a character which exists in a world space.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { }</pre><p>The offset that we'll be returning is not the same struct we used for the cached data, since the world space offsets can end up being bigger than the limits of the single byte. The structure is basically the same thing, but using integers.</p><p>The parameters are as follows:</p><ul> <li>the world space center of the tile</li> <li>the left, right, bottom and top edges of the AABB we want to receive the offset for</li> <li>the type of tile we want to receive the offset for</li> </ul><p>First, we need to figure out how the AABB overlaps with a slope tile. We need to know where the overlap starts (the bottom left corner), and also how much the overlap extends over the tile. </p><p>To calculate this, let's first declare the variables.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { int posX, posY, sizeX, sizeY; SlopeOffsetI offset; }</pre><p>Now let's calculate the edges of the tile in the world space.</p><pre class="brush: csharp noskimlinks noskimwords">float leftTileEdge = tileCenter.x - Map.cTileSize / 2; float rightTileEdge = leftTileEdge + Map.cTileSize; float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; float topTileEdge = bottomTileEdge + Map.cTileSize;</pre><p>Now this should be quite easy. There are two main categories of cases we can find here. First is that the overlap is within the tile bounds.</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/offset_input_2.png"></figure><p>The dark blue pixel is the position of the overlap, and the height and width are marked with the blue tiles. Here things are pretty straightforward, so calculating the position and the size of the overlap doesn't require any additional actions.<br></p><p>The second category of cases looks as follows, and in the game we'll mostly be dealing with those cases:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/offset_input.png"></figure><p>Let's look at an example situation pictured above. As you can see, the AABB extends well beyond the tile, but what we need to figure out is the position and size of the overlap within the tile itself, so we can retrieve our cached offset value. Right now we don't really care about anything that lies beyond the tile bounds. This will require us to clamp the overlap position and size to the tile's bounds.</p><p>Position x is equal to the offset between the left edge of the AABB and the left edge of the tile. If AABB is to the left of the tile's left edge, the position needs to be clamped to 0. To get the overlap width, we need to subtract the AABB's right edge from the overlap's x position, which we already calculated. </p><p>The values for the Y axis are calculated in the same way.</p><pre class="brush: csharp noskimlinks noskimwords">posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1);</pre><p>Now we can retrieve the cached offsets for the overlap.</p><pre class="brush: csharp noskimlinks noskimwords">offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]);</pre><h4>Adjust the Offset</h4><p>Before we return the offset, we might need to adjust it. Consider the following situation.</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/big_overlap.png"></figure><p>Let's see how our cached offset for such an overlap would look. When caching, we were only concerned about the overlap within the tile bounds, so in this case, the up offset would be equal to 9. You can see that if we moved the overlap area within the tile bounds 9 pixels up, it would cease to collide with the slope, but if we move the whole AABB, then the area which is below the tile bounds will move into the collision.</p><p>Basically, what we need to do here is adjust the up offset by the number of pixels the AABB extends below the tile bounds.</p><pre class="brush: csharp noskimlinks noskimwords">if (bottomTileEdge &gt; bottomY) { if (offset.freeUp &gt; 0) offset.freeUp += (int)bottomTileEdge - (int)bottomY; offset.collidingBottom = offset.freeUp; }</pre><p>The same thing needs to be done for all of the other offsets—left, right, and down—except that for now we'll skip handling the left and right offsets in this manner since it is not necessary to do so.</p><pre class="brush: csharp noskimlinks noskimwords">if (topTileEdge &lt; topY) { if (offset.freeDown &lt; 0) offset.freeDown -= (int)(topY - topTileEdge); offset.collidingTop = offset.freeDown; } if (bottomTileEdge &gt; bottomY) { if (offset.freeUp &gt; 0) offset.freeUp += (int)bottomTileEdge - (int)bottomY; offset.collidingBottom = offset.freeUp; }</pre><p>Once we're done, we can return the adjusted offset. The finished function should look like this.</p><pre class="brush: csharp noskimlinks noskimwords">public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) { int posX, posY, sizeX, sizeY; float leftTileEdge = tileCenter.x - Map.cTileSize / 2; float rightTileEdge = leftTileEdge + Map.cTileSize; float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; float topTileEdge = bottomTileEdge + Map.cTileSize; SlopeOffsetI offset; posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); if (topTileEdge &lt; topY) { if (offset.freeDown &lt; 0) offset.freeDown -= (int)(topY - topTileEdge); offset.collidingTop = offset.freeDown; } if (bottomTileEdge &gt; bottomY) { if (offset.freeUp &gt; 0) offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY); offset.collidingBottom = offset.freeUp; } return offset; }</pre><p>Of course, it's not completely done yet. Later on we'll also be handling the tile transformations here, so the offset is returned appropriately depending on whether the tile has been flipped on the XY axes or rotated 90 degrees. For now, though, we'll be playing only with the non-transformed tiles.</p><h2>Implementing One-Pixel Step Physics</h2><h3>Overview</h3><p>Moving the objects by one pixel will make it quite easy to handle a lot of things, especially collisions against slopes for fast objects, but even though we're going to check for collision each pixel we move, we should move in a specific pattern to ensure accuracy. This pattern will be dependent on the object's speed.<br></p><figure class="post_image"><img alt="Checking 1-pixel collisions" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/28472/image/ground_tilemap_check_fail_2.png"></figure><p>On the picture above, you can see that if we blindly move the object first all the pixels it needs to move horizontally, and after that vertically, the arrow would end up colliding with a solid block that's not really on its course. The order of movement should be based on the ratio of the vertical to horizontal speed; this way we'll know how many pixels we need to move vertically for each pixel moved horizontally.</p><h3>Define the Data</h3><p>Let's move to our moving object class and define a few new variables.</p><p>First of all, our main <code class="inline">mPosition</code> variable will be holding only the integer numbers, and we'll be keeping another variable called <code class="inline">mRemainder</code> to keep the value after the floating point.</p><pre class="brush: csharp noskimlinks noskimwords">public Vector2 mPosition; public Vector2 mRemainder;</pre><p>Next, we'll add a few new position status variables to indicate whether the character is currently on the slope. At this point, it will be good if we pack all the position status into a single structure.</p><pre class="brush: csharp noskimlinks noskimwords">[Serializable] public struct PositionState { public bool pushesRight; public bool pushesLeft; public bool pushesBottom; public bool pushesTop; public bool pushedTop; public bool pushedBottom; public bool pushedRight; public bool pushedLeft; public bool pushedLeftObject; public bool pushedRightObject; public bool pushedBottomObject; public bool pushedTopObject; public bool pushesLeftObject; public bool pushesRightObject; public bool pushesBottomObject; public bool pushesTopObject; public bool pushedLeftTile; public bool pushedRightTile; public bool pushedBottomTile; public bool pushedTopTile; public bool pushesLeftTile; public bool pushesRightTile; public bool pushesBottomTile; public bool pushesTopTile; public bool onOneWayPlatform; public Vector2i leftTile; public Vector2i rightTile; public Vector2i topTile; public Vector2i bottomTile; public void Reset() { leftTile = rightTile = topTile = bottomTile = new Vector2i(-1, -1); pushesRight = false; pushesLeft = false; pushesBottom = false; pushesTop = false; pushedTop = false; pushedBottom = false; pushedRight = false; pushedLeft = false; pushedLeftObject = false; pushedRightObject = false; pushedBottomObject = false; pushedTopObject = false; pushesLeftObject = false; pushesRightObject = false; pushesBottomObject = false; pushesTopObject = false; pushedLeftTile = false; pushedRightTile = false; pushedBottomTile = false; pushedTopTile = false; pushesLeftTile = false; pushesRightTile = false; pushesBottomTile = false; pushesTopTile = false; onOneWayPlatform = false; } }</pre><p>Now let's declare an instance of the struct for the object.</p><pre class="brush: csharp noskimlinks noskimwords">public PositionState mPS;</pre><p>Another variable that we'll need is slope sticking.</p><pre class="brush: csharp noskimlinks noskimwords">public bool mSticksToSlope;</pre><h3>Basic Implementation</h3><p>Let's start by creating the basic collision checking functions; these will not handle the slopes yet.</p><h4>Collision Checks</h4><p>Let's start with the right side.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { }</pre><p>The parameters used here are the current position of the object, its top right and bottom left corners, and the position state. First of all, let's calculate the top right and top left tile for our object.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));</pre><p>Now let's iterate through all the tiles along the object's right edge.</p><pre class="brush: csharp noskimlinks noskimwords">for (int y = bottomLeftTile.y; y &lt;= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); }</pre><p>Now, depending on the collision tile, we react appropriately.</p><pre class="brush: csharp noskimlinks noskimwords">switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; }</pre><p>As you can see, for now we'll skip handling the slopes; we just want to get the basic setup done before we delve into that.</p><p>Overall, the function for now should look like this:</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); for (int y = bottomLeftTile.y; y &lt;= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesRightTile = true; state.rightTile = new Vector2i(topRightTile.x, y); return true; } } return false; }</pre><p>We'll do the same for all the other three directions: left, up, and down.</p><pre class="brush: csharp noskimlinks noskimwords">public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f)); for (int y = bottomLeftTile.y; y &lt;= topRightTile.y; ++y) { var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesLeftTile = true; state.leftTile = new Vector2i(bottomLeftTile.x, y); return true; } } return false; } public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); for (int x = bottomleftTile.x; x &lt;= topRightTile.x; ++x) { var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.pushesTopTile = true; state.topTile = new Vector2i(x, topRightTile.y); return true; } } return false; } public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) { Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f)); for (int x = bottomleftTile.x; x &lt;= topRightTile.x; ++x) { var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y); switch (tileCollisionType) { default://slope break; case TileCollisionType.Empty: break; case TileCollisionType.Full: state.onOneWayPlatform = false; state.pushesBottomTile = true; state.bottomTile = new Vector2i(x, bottomleftTile.y); return true; } } return false; }</pre><h4>Moving Functions</h4><p>Now that we have this covered, we can start creating two functions responsible for movement. One will handle the movement horizontally, and another will handle the vertical movement.</p><pre class="brush: csharp noskimlinks noskimwords">public void MoveX(ref Vector2 position, ref bool foundObstacleX, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { }</pre><p>The arguments we'll use in this function are the current position, a boolean which indicates whether we find an obstacle along the way or not, an offset which defines how much we need to move, a step which is a value we move the object with each iteration, the AABB's lower left and upper right vertices, and finally the position state.</p><p>Basically, what we want to do here is move the object by a step so many times, so that the steps sum up to the offset. Of course, if we meet an obstacle, we need to stop moving as well.</p><pre class="brush: csharp noskimlinks noskimwords">while (!foundObstacleX &amp;&amp; offset != 0.0f) { }</pre><p>With each iteration, we subtract the step from the offset, so the offset eventually becomes zero, and we know we moved as many pixels as we needed to.</p><pre class="brush: csharp noskimlinks noskimwords">while (!foundObstacleX &amp;&amp; offset != 0.0f) { offset -= step; }</pre><p>With each step, we want to check whether we collide with a tile. If we're moving right, we want to check if we collide with a wall on the right; if we're moving left, we want to check for obstacles on the left.</p><pre class="brush: csharp noskimlinks noskimwords">while (!foundObstacleX &amp;&amp; offset != 0.0f) { offset -= step; if (step &gt; 0.0f) foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); else foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); }</pre><p>If we didn't find an obstacle, we can move the object.</p><pre class="brush: csharp noskimlinks noskimwords">while (!foundObstacleX &amp;&amp; offset != 0.0f) { offset -= step; if (step &gt; 0.0f) foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); else foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); if (!foundObstacleX) { position.x += step; topRight.x += step; bottomLeft.x += step; } }</pre><p>Finally, after we move, we check for collisions up and down, because we could slide right under or above a block. This is just to update the position state to be accurate.</p><pre class="brush: csharp noskimlinks noskimwords">public void MoveX(ref Vector2 position, ref bool foundObstacleX, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { while (!foundObstacleX &amp;&amp; offset != 0.0f) { offset -= step; if (step &gt; 0.0f) foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); else foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); if (!foundObstacleX) { position.x += step; topRight.x += step; bottomLeft.x += step; CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); } } }</pre><p>The <code class="inline">MoveY</code> function works similarly.</p><pre class="brush: csharp noskimlinks noskimwords">public void MoveY(ref Vector2 position, ref bool foundObstacleY, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { while (!foundObstacleY &amp;&amp; offset != 0.0f) { offset -= step; if (step &gt; 0.0f) foundObstacleY = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); else foundObstacleY = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); if (!foundObstacleY) { position.y += step; topRight.y += step; bottomLeft.y += step; CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); } } }</pre><h4>Consolidate the Movement</h4><p>Now that we have functions responsible for vertical and horizontal movement, we can create the main function responsible for movement. </p><pre class="brush: csharp noskimlinks noskimwords">public void Move(Vector2 offset, Vector2 speed, ref Vector2 position, ref Vector2 remainder, AABB aabb, ref PositionState state) { }</pre><p>The function takes the value of how much to move the object, the object's current speed, its current position together with the floating point remainder, the object's AABB, and the position state.</p><p>The first thing we'll do here is add the offset to the remainder, so that in the remainder we have the full value of how much our character should move.</p><pre class="brush: csharp noskimlinks noskimwords"> remainder += offset;</pre><p>Since we'll be calling the <code class="inline">MoveX</code> and <code class="inline">MoveY</code> functions from this one, we'll need to pass the top right and bottom left corners of the AABB, so let's calculate them now.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 topRight = aabb.Max(); Vector2 bottomLeft = aabb.Min();</pre><p>We also need to get the step vector. It will be used as a direction in which we'll be moving our object.</p><pre class="brush: csharp noskimlinks noskimwords">var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y));</pre><p>Now let's see how many pixels we actually need to move. We need to round the remainder, because we are always going to move by an integer number, and then we need to subtract that value from the remainder.</p><pre class="brush: csharp noskimlinks noskimwords">var move = new Vector2(Mathf.Round(remainder.x), Mathf.Round(remainder.y)); remainder -= move;</pre><p>Now let's split the movement into four cases, depending on our move vector values. If the move vector's x and y values are equal to 0, there is no movement to be made, so we can just return.</p><pre class="brush: csharp noskimlinks noskimwords">if (move.x == 0.0f &amp;&amp; move.y == 0.0f) return;</pre><p>If only the y value is 0, we're going to move only horizontally.</p><pre class="brush: csharp noskimlinks noskimwords">if (move.x == 0.0f &amp;&amp; move.y == 0.0f) return; else if (move.x != 0.0f &amp;&amp; move.y == 0.0f) MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state);</pre><p>If only the x value is 0, we're going to move only vertically.<br></p><pre class="brush: csharp noskimlinks noskimwords">if (move.x == 0.0f &amp;&amp; move.y == 0.0f) return; else if (move.x != 0.0f &amp;&amp; move.y == 0.0f) MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state); else if (move.y != 0.0f &amp;&amp; move.x == 0.0f) MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); else { }</pre><p>If we need to move both on the x and y axes, we need to move in a pattern that was described earlier. First off, let's calculate the speed ratio.</p><pre class="brush: csharp noskimlinks noskimwords">float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x);</pre><p>Let's also declare the vertical accumulator which will hold how many pixels we need to move vertically with each loop.</p><pre class="brush: csharp noskimlinks noskimwords">float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); float vertAccum = 0.0f;</pre><p>The condition to stop moving will be that we either met an obstacle on any of the axes or the object was moved by the whole move vector.</p><pre class="brush: csharp noskimlinks noskimwords">float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); float vertAccum = 0.0f; while (!foundObstacleX &amp;&amp; !foundObstacleY &amp;&amp; (move.x != 0.0f || move.y != 0.0f)) { }</pre><p>Now let's calculate how many pixels vertically we should move the object.</p><pre class="brush: csharp noskimlinks noskimwords">while (!foundObstacleX &amp;&amp; !foundObstacleY &amp;&amp; (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; }</pre><p>For the movement, we first move one step horizontally.</p><pre class="brush: csharp noskimlinks noskimwords">while (!foundObstacleX &amp;&amp; !foundObstacleY &amp;&amp; (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); move.x -= step.x; } </pre><p>And after this we can move vertically. Here we know that we need to move the object by the value contained in the <code class="inline">vertAccum</code>, but in case of any inaccuracies, if we moved all the way on the X axis, we also need to move all the way on the Y axis.</p><pre class="brush: csharp noskimlinks noskimwords">while (!foundObstacleX &amp;&amp; !foundObstacleY &amp;&amp; (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); move.x -= step.x; while (!foundObstacleY &amp;&amp; move.y != 0.0f &amp;&amp; (Mathf.Abs(vertAccum) &gt;= 1.0f || move.x == 0.0f)) { move.y -= step.y; vertAccum -= step.y; MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); } } </pre><p>All in all, the function should look like this:</p><pre class="brush: csharp noskimlinks noskimwords">public void Move(Vector2 offset, Vector2 speed, ref Vector2 position, ref Vector2 remainder, AABB aabb, ref PositionState state) { remainder += offset; Vector2 topRight = aabb.Max(); Vector2 bottomLeft = aabb.Min(); bool foundObstacleX = false, foundObstacleY = false; var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y)); var move = new Vector2(Mathf.Round(remainder.x), Mathf.Round(remainder.y)); remainder -= move; if (move.x == 0.0f &amp;&amp; move.y == 0.0f) return; else if (move.x != 0.0f &amp;&amp; move.y == 0.0f) MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state); else if (move.y != 0.0f &amp;&amp; move.x == 0.0f) MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); else { float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); float vertAccum = 0.0f; while (!foundObstacleX &amp;&amp; !foundObstacleY &amp;&amp; (move.x != 0.0f || move.y != 0.0f)) { vertAccum += Mathf.Sign(step.y) * speedRatio; MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); move.x -= step.x; while (!foundObstacleY &amp;&amp; move.y != 0.0f &amp;&amp; (Mathf.Abs(vertAccum) &gt;= 1.0f || move.x == 0.0f)) { move.y -= step.y; vertAccum -= step.y; MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); } } } }</pre><p>Now we can use the functions we've built to compose our main <code class="inline">UpdatePhysics</code> function.</p><h4>Build the Physics Update Function</h4><p>First of all, we want to update the position state, so all of the previous frame's data goes to the adequate variables, and the current frame's data is reset.</p><pre class="brush: csharp noskimlinks noskimwords">public void UpdatePhysics() { mPS.pushedBottom = mPS.pushesBottom; mPS.pushedRight = mPS.pushesRight; mPS.pushedLeft = mPS.pushesLeft; mPS.pushedTop = mPS.pushesTop; mPS.pushedBottomTile = mPS.pushesBottomTile; mPS.pushedLeftTile = mPS.pushesLeftTile; mPS.pushedRightTile = mPS.pushesRightTile; mPS.pushedTopTile = mPS.pushesTopTile; mPS.pushesBottom = mPS.pushesLeft = mPS.pushesRight = mPS.pushesTop = mPS.pushesBottomTile = mPS.pushesLeftTile = mPS.pushesRightTile = mPS.pushesTopTile = mPS.pushesBottomObject = mPS.pushesLeftObject = mPS.pushesRightObject = mPS.pushesTopObject = mPS.onOneWay = false; }</pre><p>Now let's update the collision state of our object. We do this so that before we move our object, we have updated data on whether it's on the ground or is pushing any other tiles. Normally the previous frame's data would still be up to date if the terrain was unmodifiable and other objects wouldn't be able to move this one, but here we assume that any of this could happen.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 topRight = mAABB.Max(); Vector2 bottomLeft = mAABB.Min(); CollidesWithTiles(ref mPosition, ref topRight, ref bottomLeft, ref mPS);</pre><p><code class="inline">CollidesWithTiles</code> simply calls all the collision functions we've written.</p><pre class="brush: csharp noskimlinks noskimwords">public void CollidesWithTiles(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) { CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); }</pre><p>Then update the speed.</p><pre class="brush: csharp noskimlinks noskimwords">mOldSpeed = mSpeed; if (mPS.pushesBottomTile) mSpeed.y = Mathf.Max(0.0f, mSpeed.y); if (mPS.pushesTopTile) mSpeed.y = Mathf.Min(0.0f, mSpeed.y); if (mPS.pushesLeftTile) mSpeed.x = Mathf.Max(0.0f, mSpeed.x); if (mPS.pushesRightTile) mSpeed.x = Mathf.Min(0.0f, mSpeed.x);</pre><p>And update the position. First off, let's save the old one.</p><pre class="brush: csharp noskimlinks noskimwords">mOldPosition = mPosition;</pre><p>Calculate the new one.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 newPosition = mPosition + mSpeed * Time.deltaTime;</pre><p>Calculate the offset between the two.</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 offset = newPosition - mPosition;</pre><p>Now, in case the offset is non-zero, we can call our Move function.</p><pre class="brush: csharp noskimlinks noskimwords">if (offset != Vector2.zero) Move(offset, mSpeed, ref mPosition, ref mRemainder, mAABB, ref mPS);</pre><p>Finally, update the object's AABB and the position state.</p><pre class="brush: csharp noskimlinks noskimwords">mAABB.Center = mPosition; mPS.pushesBottom = mPS.pushesBottomTile; mPS.pushesRight = mPS.pushesRightTile; mPS.pushesLeft = mPS.pushesLeftTile; mPS.pushesTop = mPS.pushesTopTile;</pre><p>That's it! This system now replaces the older one, the results should be the same, although the way we do it is quite a bit different.</p><h2>Summary</h2><p>That's it for laying down the groundwork for the slopes, so what's left is to fill up those gaps in our collision checks! We've done most of our caching work here and eliminated a lot of geometrical complexities by implementing the one-pixel movement integration. </p><p>This will make the slope implementation a breeze, compared to what we'd need to do otherwise. We'll be finishing the job in the next part of the series. </p><p>Thanks for reading!</p> 2017-07-18T13:00:21.000Z 2017-07-18T13:00:21.000Z Daniel Branicki tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-28655 Creating a Hexagonal Minesweeper <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/final_image/hexmine-phaser-finished.png" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><p>In this tutorial, I will try to introduce the interesting world of hexagonal tile-based games using the easiest of approaches. You will learn how to convert a two-dimensional array data to a corresponding hexagonal level layout on screen and vice versa. Using the information gained, we will be creating a hexagonal minesweeper game in two different hexagonal layouts.</p><p>This will get you started with exploring simple hexagonal board games and puzzle games and will be a good starting point to learn more complicated approaches like the axial or cubic hexagonal coordinate systems.</p><h2> <span class="sectionnum">1.</span> Hexagonal Tiles and Layouts</h2><p>In the current generation of casual gaming, we don't see many games which use a hexagonal tile-based approach. Those we come across are usually puzzle games, board games, or strategy games. Also, most of our requirements are met by the square grid approach or isometric approach. This leads to the natural question: "Why do we need a different and obviously complicated hexagonal approach?" Let's find out.</p><h3>Advantages of the Hexagonal Approach</h3><p>So what makes the hexagonal tile-based approach relevant, since we already have other approaches learned and perfected? Let me list some of the reasons.</p><ul> <li>Smaller number of neighbour tiles: When compared to a square grid, which will have eight neighbour tiles, a hexagonal tile will only have six neighbours. This reduces computations for complicated algorithms.</li> <li>All neighbour tiles are at the same distance: For a square grid, the four diagonal neighbours are far away when compared to the horizontal or vertical neighbours. Neighbours being at equal distances is a great relief when we are calculating heuristics and reduces the overhead of using two different methods to calculate something depending on the neighbour.</li> <li>Uniqueness: These days, millions of casual games are coming out and are competing for the player's time. Great games are failing to get an audience, and one thing that can be guaranteed to grab a player's attention is uniqueness. A game using a hexagonal approach will visually stand out from the rest, and the game will seem more interesting to a crowd who are bored with all the conventional gameplay mechanics. </li> </ul><p>I would say the last reason should be enough for you to master this new approach. Adding that unique gameplay element over your game logic could make all the difference and enable you to make a great game. </p><p>The other reasons are purely technical and would only come into effect once you are dealing with complicated algorithms or larger tile sets. There are many other aspects also which can be listed as advantages of the hexagonal approach, but most of them will depend on the player's personal interest.</p><h3>Layouts</h3><p>A hexagon is a polygon with six sides, and a hexagon with all sides having the same length is called a regular hexagon. For theory purposes, we will consider our hexagonal tiles to be regular hexagons, but they could be squashed or elongated in practice. </p><p>The interesting thing is that a hexagon can be placed in two different ways: the pointy corners could be aligned vertically or horizontally. When pointy tops are aligned vertically, it is called a <em>horizontal</em> layout, and when they are aligned horizontally, it is called a <em>vertical</em> layout. You may think that the names are misnomers with respect to the explanation provided. This is not the case as the naming is not done based on the pointy corners but the way a grid of tiles gets laid out. The image below shows the different tile alignments and corresponding layouts.</p><figure class="post_image"><img alt="The vertical and horizontal hexagonal tile grid layout" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-tilelayout.png"></figure><p>The choice of layout entirely depends on your game's visuals and gameplay. Yet your choice does not end here as each of these layouts could be implemented in two different ways.</p><p>Let's consider a horizontal hexagonal grid layout. Alternative rows of the grid would need to be horizontally offset by <code class="inline">hexTileWidth/2</code>. This means we could choose to offset either the odd rows or the even rows. If we also display the corresponding <em>row</em>, <em>column</em> values, these variants would look like the image below.</p><figure class="post_image"><img alt="Horizontal hexagonal layout with even odd offsets" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-tilehorizontallayout.png"></figure><p>Similarly, the vertical layout could be implemented in two variations while offsetting alternative columns by <code class="inline">hexTileHeight/2</code> as shown below.</p><figure class="post_image"><img alt="Vertical hexagonal layout showing even odd variations" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-tileverticallayout.png"></figure><h2> <span class="sectionnum">2.</span> Implementing Hexagonal Layouts</h2><p><em>From here on onwards, please start referring to the source code provided along with this tutorial for better understanding</em>. </p><p>The images above, with the rows and columns displayed, make it easier to visualise a direct correlation with a two-dimensional array which stores the level data. Let's say we have a simple two-dimensional array <code class="inline">levelData</code> as below.</p><pre class="brush: javascript noskimlinks noskimwords">var levelData= [[0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0] ]</pre><p>To make it easier to visualise, I will show the intended result here in both vertical and horizontal variations.</p><figure class="post_image"><img alt="simple hexagonal grid based on level data array" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-simplelayouts.png"></figure><p>Let's start with horizontal layout, which is the image on the left side. In each row, if taken individually, the neighbour tiles are horizontally offset by <code class="inline">hexTileWidth</code>. Alternative rows are horizontally offset by a value of <code class="inline">hexTileWidth/2</code>. The vertical height difference between each row is <code class="inline">hexTileHeight*3/4</code>. </p><p>To understand how we arrived at such a value for the height offset, we need to consider the fact that the top and bottom triangular portions of a horizontally laid out hexagon are exactly <code class="inline">hexTileHeight/4</code>. </p><p>This means that the hexagon has a rectangular <code class="inline">hexTileHeight/2</code> portion in the middle, a triangular <code class="inline">hexTileHeight/4</code> portion on top, and an inverted triangular <code class="inline">hexTileHeight/4</code> portion on the bottom. This information is enough to create the code necessary to lay out the hexagonal grid on screen.</p><pre class="brush: javascript noskimlinks noskimwords">var verticalOffset=hexTileHeight*3/4; var horizontalOffset=hexTileWidth; var startX; var startY; var startXInit=hexTileWidth/2; var startYInit=hexTileHeight/2; var hexTile; for (var i = 0; i &lt; levelData.length; i++) { if(i%2!==0){ startX=2*startXInit; }else{ startX=startXInit; } startY=startYInit+(i*verticalOffset); for (var j = 0; j &lt; levelData.length; j++) { if(levelData[i][j]!=-1){ hexTile= new HexTile(game, startX, startY, 'hex',false,i,j,levelData[i][j]); hexGrid.add(hexTile); } startX+=horizontalOffset; } }</pre><p>With the <code class="inline">HexTile</code> prototype, I have added some additional functionalities to the <code class="inline">Phaser.Sprite</code> prototype which enables it to display the <code class="inline">i</code> and <code class="inline">j</code> values. The code essentially places a new hexagonal tile <code class="inline">Sprite</code> at <code class="inline">startX</code> and <code class="inline">startY</code>. This code can be changed to display the even offset variant just by removing an operator in the <code class="inline">if</code> condition like this: <code class="inline">if(i%2===0)</code>.</p><p>For a vertical layout (the image on the right half), neighbour tiles in every column are vertically offset by <code class="inline">hexTileHeight</code>. Each alternate column is vertically offset by <code class="inline">hexTileHeight/2</code>. Applying the logic which we applied for vertical offset for the horizontal layout, we can see that the horizontal offset for the vertical layout between neighbour tiles in a row is <code class="inline">hexTileWidth*3/4</code>. The corresponding code is below.</p><pre class="brush: javascript noskimlinks noskimwords">var verticalOffset=hexTileHeight; var horizontalOffset=hexTileWidth*3/4; var startX; var startY; var startXInit=hexTileWidth/2; var startYInit=hexTileHeight/2; var hexTile; for (var i = 0; i &lt; levelData.length; i++) { startX=startXInit; startY=2*startYInit+(i*verticalOffset); for (var j = 0; j &lt; levelData.length; j++) { if(j%2!==0){ startY=startY+startYInit; }else{ startY=startY-startYInit; } if(levelData[i][j]!=-1){ hexTile= new HexTile(game, startX, startY, 'hex', true,i,j,levelData[i][j]); hexGrid.add(hexTile); } startX+=horizontalOffset; } }</pre><p>In the same way as with the horizontal layout, we can switch to the even offset variant just by removing the <code class="inline">!</code> operator in the top <code class="inline">if</code> condition. I am using a Phaser <code class="inline">Group</code> to collect all the <code class="inline">hexTiles</code> named <code class="inline">hexGrid</code>. For simplicity, I am using the centre point of the hexagonal tile image as an anchor, or else we would need to consider the image offsets as well. </p><p>One thing to notice is that the tile width and tile height values in the horizontal layout are not equal to the tile width and tile height values in the vertical layout. But when using the same image for both layouts, we could just rotate the tile image 90 degrees and swap the values of tile width and tile height.</p><h2> <span class="sectionnum">3.</span> Finding the Array Index of a Hexagonal Tile</h2><p>The array to screen placement logic was interestingly straightforward, but the reverse is not so easy. Consider that we need to find the array index of the hexagonal tile on which we have tapped. The code to achieve this is not pretty, and it is usually arrived at by some trial and error. </p><p>If we consider the horizontal layout, it may seem that the middle rectangular portion of the hexagonal tile can easily help us figure out the <code class="inline">j</code> value as it is just a matter of dividing the <code class="inline">x</code> value by <code class="inline">hexTileWidth</code> and taking the integer value. But unless we know the <code class="inline">i</code> value, we don't know if we are on an odd or even row. An approximate value of <code class="inline">i</code> can be found by dividing the y value by <code class="inline">hexTileHeight*3/4</code>. </p><p>Now come the complicated parts of the hexagonal tile: the top and bottom triangular portions. The image below will help us understand the problem at hand.</p><figure class="post_image"><img alt="a horizontally laid hexagonal tile split into regions" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-tilesplit.png"></figure><p>The regions 2, 3, 5, 6, 8, and 9 together form one tile. The most complicated part is to find if the tapped position is in 1/2 or 3/4 or 7/8 or 9/10. For this, we need to consider all the individual triangular regions and check against them using the slope of the slanted edge. </p><p>This slope can be found from the height and width of each triangular region, which respectively are <code class="inline">hexTileHeight/4</code> and <code class="inline">hexTileWidth/2</code>. Let me show you the function which does this.</p><pre class="brush: javascript noskimlinks noskimwords">function findHexTile(){ var pos=game.input.activePointer.position; pos.x-=hexGrid.x; pos.y-=hexGrid.y; var xVal = Math.floor((pos.x)/hexTileWidth); var yVal = Math.floor((pos.y)/(hexTileHeight*3/4)); var dX = (pos.x)%hexTileWidth; var dY = (pos.y)%(hexTileHeight*3/4); var slope = (hexTileHeight/4)/(hexTileWidth/2); var caldY=dX*slope; var delta=hexTileHeight/4-caldY; if(yVal%2===0){ //correction needs to happen in triangular portions &amp; the offset rows if(Math.abs(delta)&gt;dY){ if(delta&gt;0){//odd row bottom right half xVal--; yVal--; }else{//odd row bottom left half yVal--; } } }else{ if(dX&gt;hexTileWidth/2){// available values don't work for even row bottom right half if(dY&lt;((hexTileHeight/2)-caldY)){//even row bottom right half yVal--; } }else{ if(dY&gt;caldY){//odd row top right &amp; mid right halves xVal--; }else{//even row bottom left half yVal--; } } } pos.x=yVal; pos.y=xVal; return pos; }</pre><p>First, we find <code class="inline">xVal</code> and <code class="inline">yVal</code> the same way we would do for a square grid. Then we find the remaining horizontal (<code class="inline">dX</code>) and vertical (<code class="inline">dY</code>) values after removing the tile multiplier offset. Using these values, we try to figure out if the point is within any of the complicated triangular regions. </p><p>If found, we make corresponding changes to the initial values of <code class="inline">xVal</code> and <code class="inline">yVal</code>. As I have said earlier, the code is not pretty and not straightforward. The easiest way to understand this would be to call <code class="inline">findHexTile</code> on mouse move, and then put <code class="inline">console.log</code> inside each of those conditions and move the mouse over various regions within one hexagonal tile. This way, you can see how each intra-hexagonal region is handled.</p><p>The code changes for the vertical layout are shown below.</p><pre class="brush: javascript noskimlinks noskimwords">function findHexTile(){ var pos=game.input.activePointer.position; pos.x-=hexGrid.x; pos.y-=hexGrid.y; var xVal = Math.floor((pos.x)/(hexTileWidth*3/4)); var yVal = Math.floor((pos.y)/(hexTileHeight)); var dX = (pos.x)%(hexTileWidth*3/4); var dY = (pos.y)%(hexTileHeight); var slope = (hexTileHeight/2)/(hexTileWidth/4); var caldX=dY/slope; var delta=hexTileWidth/4-caldX; if(xVal%2===0){ if(dX&gt;Math.abs(delta)){// even left }else{//odd right if(delta&gt;0){//odd right bottom xVal--; yVal--; }else{//odd right top xVal--; } } }else{ if(delta&gt;0){ if(dX&lt;caldX){//even right top xVal--; }else{//odd mid yVal--; } }else{//current values wont help for even right bottom if(dX&lt;((hexTileWidth/2)-caldX)){//even right bottom xVal--; } } } pos.x=yVal; pos.y=xVal; return pos; }</pre><h2> <span class="sectionnum">4. </span>Finding Neighbours</h2><p>Now that we've found the tile on which we have tapped, let's find all six neighbouring tiles. This is a very easy problem to solve once we visually analyse the grid. Let's consider the horizontal layout.</p><figure class="post_image"><img alt="odd and even rows of horizontal layout when a middle tile has ij set to 0" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-neighbours.png"></figure><p>The image above shows the odd and even <em>rows</em> of a horizontally laid out hexagonal grid when a middle tile has the value of <code class="inline">0</code> for both <code class="inline">i</code> and <code class="inline">j</code>. From the image, it becomes clear that if the row is odd, then for a tile at <code class="inline">i,j</code> the neighbours are <code class="inline">i, j-1</code>, <code class="inline">i-1,j-1</code>, <code class="inline">i-1,j</code>, <code class="inline">i,j+1</code>, <code class="inline">i+1,j</code>, and <code class="inline">i+1,j-1</code>. When the row is even, then for a tile at <code class="inline">i,j</code> the neighbours are <code class="inline">i, j-1</code>, <code class="inline">i-1,j</code>, <code class="inline">i-1,j+1</code>, <code class="inline">i,j+1</code>, <code class="inline">i+1,j+1</code>, and <code class="inline">i+1,j</code>. This could be manually calculated easily.</p><p>Let's analyse a similar image for the odd and even <em>columns</em> of a vertically aligned hexagonal grid.</p><figure class="post_image"><img alt="odd and even columns of vertical layout when a middle tile has ij set to 0" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-neighbours-vertical.png"></figure><p>When we have an odd column, a tile at <code class="inline">i,j</code> will have <code class="inline">i,j-1</code>, <code class="inline">i-1,j-1</code>, <code class="inline">i-1,j</code>, <code class="inline">i-1,j+1</code>, <code class="inline">i,j+1</code>, and <code class="inline">i+1,j</code> as neighbours. Similarly, for an even column, the neighbours are <code class="inline">i+1,j-1</code>, <code class="inline">i,j-1</code>, <code class="inline">i-1,j</code>, <code class="inline">i,j+1</code>, <code class="inline">i+1,j+1</code>, and <code class="inline">i+1,j</code>. </p><h2> <span class="sectionnum">5.</span> Hexagonal Minesweeper</h2><p>With the above knowledge, we can try to make a hexagonal minesweeper game in the two different layouts. Let's break down the features of a minesweeper game.</p><ol> <li>There will be N number of mines hidden inside the grid.</li> <li>If we tap on a tile with a mine, the game is over.</li> <li>If we tap on a tile which has a neighbouring mine, it will display the number of mines immediately around it.</li> <li>If we tap on a mine without any neighbouring mines, it would lead to the revealing of all the connected tiles which do not have mines.</li> <li>We can tap and hold to mark a tile as a mine.</li> <li>The game is finished when we reveal all tiles without mines.</li> </ol><p>We can easily store a value in the <code class="inline">levelData</code> array to indicate a mine. The same method can be used to populate the value of nearby mines on the neighbouring tiles' array index. </p><p>On game start, we will randomly populate the <code class="inline">levelData</code> array with N number of mines. After this, we will update the values for all the neighbouring tiles. We will use a recursive method to chain reveal all the connected blank tiles when the player taps on a tile which doesn't have a mine as neighbour.</p><h3>Level Data</h3><p>We need to create a nice looking hexagonal grid, as shown in the image below.</p><figure class="post_image"><img alt="horizontal hexagonal minesweeper grid" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28655/image/hexmine-phaser-level.png"></figure><p>This can be done by only displaying a portion of the <code class="inline">levelData</code> array. If we use <code class="inline">-1</code> as the value for a non-usable tile and <code class="inline">0</code> as the value for a usable tile, then our <code class="inline">levelData</code> for achieving the above result will look like this.</p><pre class="brush: javascript noskimlinks noskimwords">//horizontal tile shaped level var levelData= [[-1,-1,-1,0,0,0,0,0,0,0,-1,-1,-1], [-1,-1,0,0,0,0,0,0,0,0,-1,-1,-1], [-1,-1,0,0,0,0,0,0,0,0,0,-1,-1], [-1,0,0,0,0,0,0,0,0,0,0,-1,-1], [-1,0,0,0,0,0,0,0,0,0,0,0,-1], [0,0,0,0,0,0,0,0,0,0,0,0,-1], [0,0,0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0,0,-1], [-1,0,0,0,0,0,0,0,0,0,0,0,-1], [-1,0,0,0,0,0,0,0,0,0,0,-1,-1], [-1,-1,0,0,0,0,0,0,0,0,0,-1,-1], [-1,-1,0,0,0,0,0,0,0,0,-1,-1,-1], [-1,-1,-1,0,0,0,0,0,0,0,-1,-1,-1]];</pre><p>While looping through the array, we would only add hexagonal tiles when the <code class="inline">levelData</code> has a value of <code class="inline">0</code>. For the vertical alignment, the same <code class="inline">levelData</code> can be used, but we would need to <em>transpose</em> the array. Here is a nifty method which can do this for you.</p><pre class="brush: javascript noskimlinks noskimwords">levelData=transpose(levelData); //... function transpose(a) { return Object.keys(a).map( function (c) { return a.map(function (r) { return r[c]; }); } ); }</pre><h3>Adding Mines and Updating Neighbours</h3><p>By default, our <code class="inline">levelData</code> has only two values, <code class="inline">-1</code> and <code class="inline">0</code>, of which we would be using only the area with <code class="inline">0</code>. To indicate that a tile contains a mine, we can use the value of <code class="inline">10</code>. </p><p>A blank hexagonal tile can have a maximum of six mines near it as it has six neighbouring tiles. We can store this information also in the <code class="inline">levelData</code> once we have added all the mines. Essentially, a <code class="inline">levelData</code> index having a value of <code class="inline">10</code> has a mine, and if it contains any values from <code class="inline">0</code> to <code class="inline">6</code>, that indicates the number of neighbouring mines. After populating mines and updating neighbours, if an array element is still <code class="inline">0</code>, it indicates that it is a blank tile without any neighbouring mines. </p><p>We can use the following methods for our purposes.</p><pre class="brush: javascript noskimlinks noskimwords">function addMines(){ var tileType=0; var tempArray=[]; var newPt=new Phaser.Point(); for (var i = 0; i &lt; levelData.length; i++) { for (var j = 0; j &lt; levelData.length; j++) { tileType=levelData[i][j]; if(tileType===0){ newPt=new Phaser.Point(); newPt.x=i; newPt.y=j; tempArray.push(newPt); } } } for (var i = 0; i &lt; numMines; i++) { newPt=Phaser.ArrayUtils.removeRandomItem(tempArray); levelData[newPt.x][newPt.y]=10;//10 is mine updateNeighbors(newPt.x,newPt.y); } } function updateNeighbors(i,j){//update neighbors around this mine var tileType=0; var tempArray=getNeighbors(i,j); var tmpPt; for (var k = 0; k &lt; tempArray.length; k++) { tmpPt=tempArray[k]; tileType=levelData[tmpPt.x][tmpPt.y]; levelData[tmpPt.x][tmpPt.y]=tileType+1; } }</pre><p>For every mine added in <code class="inline">addMines</code>, we are incrementing the array value stored in all of its neighbours. The <code class="inline">getNeighbors</code> method won't return a tile which is outside our effective area or if it contains a mine.</p><h3>Tap Logic</h3><p>When the player taps on a tile, we need to find the corresponding array element using the <code class="inline">findHexTile</code> method explained earlier. If the tile index is within our effective area, then we just compare the value at the array index to find if it is a mine or blank tile.</p><pre class="brush: javascript noskimlinks noskimwords">function onTap(){ var tile= findHexTile(); if(!checkforBoundary(tile.x,tile.y)){ if(checkForOccuppancy(tile.x,tile.y)){ if(levelData[tile.x][tile.y]==10){ //console.log('boom'); var hexTile=hexGrid.getByName("tile"+tile.x+"_"+tile.y); if(!hexTile.revealed){ hexTile.reveal(); //game over } } }else{ var hexTile=hexGrid.getByName("tile"+tile.x+"_"+tile.y); if(!hexTile.revealed){ if(levelData[tile.x][tile.y]===0){ //console.log('recursive reveal'); recursiveReveal(tile.x,tile.y); }else{ //console.log('reveal'); hexTile.reveal(); revealedTiles++; } } } } infoTxt.text='found '+revealedTiles +' of '+blankTiles; }</pre><p>We keep track of the total number of blank tiles using the variable <code class="inline">blankTiles</code> and the number of tiles revealed using <code class="inline">revealedTiles</code>. Once they are equal, we have won the game. </p><p>When we tap on a tile with an array value of <code class="inline">0</code>, we need to recursively reveal the region with all the connected blank tiles. This is done by the function <code class="inline">recursiveReveal</code>, which receives the tile indices of the tapped tile.</p><pre class="brush: javascript noskimlinks noskimwords">function recursiveReveal(i,j){ var newPt=new Phaser.Point(i,j); var hexTile; var tempArray=[newPt]; var neighbors; while (tempArray.length){ newPt=tempArray; var neighbors=getNeighbors(newPt.x,newPt.y); while(neighbors.length){ newPt=neighbors.shift(); hexTile=hexGrid.getByName("tile"+newPt.x+"_"+newPt.y); if(!hexTile.revealed){ hexTile.reveal(); revealedTiles++; if(levelData[newPt.x][newPt.y]===0){ tempArray.push(newPt); } } } newPt=tempArray.shift();//it seemed one point without neighbor sometimes escapes the iteration without getting revealed, catch it here hexTile=hexGrid.getByName("tile"+newPt.x+"_"+newPt.y); if(!hexTile.revealed){ hexTile.reveal(); revealedTiles++; } } }</pre><p>In this function, we find the neighbours of each tile and reveal that tile's value, meanwhile adding neighbour tiles to an array. We keep repeating this with the next element in the array until the array is empty. The recursion stops when we meet array elements containing a mine, which is ensured by the fact that <code class="inline">getNeighbors</code> won't return a tile with a mine.</p><h3>Marking and Revealing Tiles<br> </h3><p>You must have noticed that I am using <code class="inline">hexTile.reveal()</code>, which is made possible by creating a <code class="inline">HexTile</code> prototype which keeps most of the attributes related to our hexagonal tile. I use the <code class="inline">reveal</code> function to display the tile value text and set the tile's colour. Similarly, the <code class="inline">toggleMark</code> function is used to mark the tile as a mine when we tap and hold. <code class="inline">HexTile</code> also has a <code class="inline">revealed</code> attribute which tracks whether it is tapped and revealed or not.</p><pre class="brush: javascript noskimlinks noskimwords">HexTile.prototype.reveal=function(){ this.tileTag.visible=true; this.revealed=true; if(this.type==10){ this.tint='0xcc0000'; }else{ this.tint='0x00cc00'; } } HexTile.prototype.toggleMark=function(){ if(this.marked){ this.marked=false; this.tint='0xffffff'; }else{ this.marked=true; this.tint='0x0000cc'; } }</pre><p>Check out the hexagonal minesweeper with horizontal orientation below. Tap to reveal tiles, and tap-hold to mark mines. There is no game over as of now, but if you reveal a value of <code class="inline">10</code>, then it is <em>hasta la vista baby!</em></p><p><iframe src="//jsfiddle.net/juwalbose/qb1s5L9p/embedded/js,html,css,result/dark/" width="630" height="300" frameborder="0" allowfullscreen="allowfullscreen"></iframe></p><h3>Changes for the Vertical Version<br> </h3><p>As I am using the same image of hexagonal tile for both orientations, I rotate the Sprite for the vertical alignment. The below code in the <code class="inline">HexTile</code> prototype does this.</p><pre class="brush: javascript noskimlinks noskimwords">if(isVertical){ this.rotation=Math.PI/2; }</pre><p>The minesweeper logic remains the same for the vertically aligned hexagonal grid with the difference for <code class="inline">findHextile</code> and <code class="inline">getNeighbors</code> logic which now need to accommodate the alignment difference. As mentioned earlier, we also need to use the transpose of the level array with corresponding layout loop.</p><p>Check out the vertical version below.</p><p><iframe src="//jsfiddle.net/juwalbose/b4aavdxg/embedded/js,html,css,result/dark/" width="630" height="300" frameborder="0" allowfullscreen="allowfullscreen"></iframe></p><p>The rest of the code in the source is simple and straightforward. I would like you to try and add the missing restart, game win, and game over functionality.</p><h2>Conclusion</h2><p>This approach of a hexagonal tile-based game using a two-dimensional array is more of a layman's approach. More interesting and functional approaches involve altering the coordinate system to different types using equations. </p><p>The most important ones are axial coordinates and cubic coordinates. There will be a follow-up tutorial series which will discuss these approaches. Meanwhile, I would recommend reading Amit's incredibly thorough article on <a href="http://www.redblobgames.com/grids/hexagons/" rel="external" target="_blank">hexagonal grids</a>. </p> 2017-05-31T12:00:27.000Z 2017-05-31T12:00:27.000Z Juwal Bose tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-28503 Updated Primer for Creating Isometric Worlds, Part 2 <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28503/final_image/isometric-advanced-finished.png" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><p>In this final part of the tutorial series, we'll build on the first tutorial and learn about implementing pickups, triggers, level swapping, pathfinding, path following, level scrolling, isometric height, and isometric projectiles.<br></p><h2> <span class="sectionnum">1.</span> Pickups</h2><p>Pickups are items that can be collected within the level, normally by simply walking over them—for example, coins, gems, cash, ammo, etc.</p><p>Pickup data can be accommodated right into our level data as below:</p><pre class="brush: javascript noskimlinks noskimwords">[ [1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0,0,8,0,1], [1,0,0,0,0,1], [1,1,1,1,1,1] ]</pre><p>In this level data, we use <code>8</code> to denote a pickup on a grass tile (<code>1</code> and <code>0</code> represent walls and walkable tiles respectively, as before). This could be a single tile image with a grass tile overlaid with the pickup image. Going by this logic, we will need two different tile states for every tile which has a pickup, i.e. one with pickup and one without to be shown after the pickup gets collected.</p><p>Typical isometric art will have multiple walkable tiles—suppose we have 30. The above approach means that if we have N pickups, we will need N x 30 tiles in addition to the 30 original tiles, as each tile will need to have one version with pickups and one without. This is not very efficient; instead, we should try to dynamically create these combinations. </p><p>To solve this, we could use the same method we used to place the hero in the first tutorial. Whenever we come across a pickup tile, we will place a grass tile first and then place the pickup on top of the grass tile. This way, we just need N pickup tiles in addition to 30 walkable tiles, but we would need number values to represent each combination in the level data. To solve the need for N x 30 representation values, we can keep a separate <code class="inline">pickupArray</code> to exclusively store the pickup data apart from the <code class="inline">levelData</code>. The completed level with the pickup is shown below:</p><figure class="post_image"><img alt="Isometric level with coin pickup" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28503/image/isometric-advanced-pickups.png"></figure><p>For our example, I am keeping things simple and not using an additional array for pickups.</p><h3>Picking Up Pickups</h3><p>Detecting pickups is done in the same way as detecting collision tiles, but <em>after</em> moving the character.</p><pre class="brush: javascript noskimlinks noskimwords">if(onPickupTile()){ pickupItem(); } function onPickupTile(){//check if there is a pickup on hero tile return (levelData[heroMapTile.y][heroMapTile.x]==8); } </pre><p>In the function <code>onPickupTile()</code>, we check whether the <code class="inline">levelData</code> array value at the <code class="inline">heroMapTile</code> coordinate is a pickup tile or not. The number in the <code class="inline">levelData</code> array at that tile coordinate denotes the type of pickup. We check for collisions before moving the character but need to check for pickups afterwards, because in the case of collisions the character should not occupy the spot if it is already occupied by the collision tile, but in case of pickups the character is free to move over it.</p><p>Another thing to note is that the collision data usually never changes, but the pickup data changes whenever we pick up an item. (This usually just involves changing the value in the <code class="inline">levelData</code> array from, say, <code>8</code> to <code>0</code>.)</p><p>This leads to a problem: what happens when we need to restart the level, and thus reset all pickups back to their original positions? We do not have the information to do this, as the <code class="inline">levelData</code> array has been changed as the player picked up items. The solution is to use a duplicate array for the level while in play and to keep the original <code class="inline">levelData</code> array intact. For instance, we use <code>levelData</code> and <code>levelDataLive[]</code>, clone the latter from the former at the start of the level, and only change <code>levelDataLive[]</code> during play.</p><p>For the example, I am spawning a random pickup on a vacant grass tile after each pickup and incrementing the <code class="inline">pickupCount</code>. The <code class="inline">pickupItem</code> function looks like this.</p><pre class="brush: javascript noskimlinks noskimwords">function pickupItem(){ pickupCount++; levelData[heroMapTile.y][heroMapTile.x]=0; //spawn next pickup spawnNewPickup(); }</pre><p>You should notice that we check for pickups whenever the character is on that tile. This can happen multiple times within a second (we check only when the user moves, but we may go round and round within a tile), but the above logic won't fail; since we set the <code class="inline">levelData</code> array data to <code>0</code> the first time we detect a pickup, all subsequent <code>onPickupTile()</code> checks will return <code>false</code> for that tile. Check out the interactive example below:</p><iframe src="//jsfiddle.net/juwalbose/gvso9w0m/embedded" width="630" height="500"></iframe><h2> <span class="sectionnum">2.</span> Trigger Tiles</h2><p>As the name suggests, trigger tiles cause something to happen when the player steps on them or presses a key when on them. They might teleport the player to a different location, open a gate, or spawn an enemy, to give a few examples. In a sense, pickups are just a special form of trigger tiles: when the player steps on a tile containing a coin, the coin disappears and their coin counter increases.</p><p>Let's look at how we could implement a door that takes the player to a different level. The tile next to the door will be a trigger tile; when the player presses the <b>x</b> key, they'll proceed to the next level.</p><figure class="post_image"><img alt="Isometric level with doors trigger tiles" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28503/image/isometric-advanced-levelswap-fixed.png"></figure><p>To change levels, all we need to do is swap the current <code class="inline">levelData</code> array with that of the new level, and set the new <code class="inline">heroMapTile</code> position and direction for the hero character. Suppose there are two levels with doors to allow passing between them. Since the ground tile next to the door will be the trigger tile in both levels, we can use this as the new position for the character when they appear in the level.</p><p>The implementation logic here is the same as for pickups, and again we use the <code class="inline">levelData</code> array to store trigger values. For our example, <code class="inline">2</code> denotes a door tile, and the value beside it is the trigger. I have used <code class="inline">101</code> and <code class="inline">102</code> with the basic convention that any tile with a value greater than 100 is a trigger tile and the value minus 100 can be the level which it leads to:</p><pre class="brush: javascript noskimlinks noskimwords">var level1Data= [[1,1,1,1,1,1], [1,1,0,0,0,1], [1,0,0,0,0,1], [2,102,0,0,0,1], [1,0,0,0,1,1], [1,1,1,1,1,1]]; var level2Data= [[1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0,0,0,101,2], [1,0,1,0,0,1], [1,1,1,1,1,1]];</pre><p>The code for checking for a trigger event is shown below:</p><pre class="brush: javascript noskimlinks noskimwords">var xKey=game.input.keyboard.addKey(Phaser.Keyboard.X); xKey.onUp.add(triggerListener);// add a Signal listener for up event function triggerListener(){ var trigger=levelData[heroMapTile.y][heroMapTile.x]; if(trigger&gt;100){//valid trigger tile trigger-=100; if(trigger==1){//switch to level 1 levelData=level1Data; }else {//switch to level 2 levelData=level2Data; } for (var i = 0; i &lt; levelData.length; i++) { for (var j = 0; j &lt; levelData.length; j++) { trigger=levelData[i][j]; if(trigger&gt;100){//find the new trigger tile and place hero there heroMapTile.y=j; heroMapTile.x=i; heroMapPos=new Phaser.Point(heroMapTile.y * tileWidth, heroMapTile.x * tileWidth); heroMapPos.x+=(tileWidth/2); heroMapPos.y+=(tileWidth/2); } } } } }</pre><p>The function <code>triggerListener()</code> checks whether the trigger data array value at the given coordinate is greater than 100. If so, we find which level we need to switch to by subtracting 100 from the tile value. The function finds the trigger tile in the new <code class="inline">levelData</code>, which will be the spawn position for our hero. I have made the trigger to be activated when <strong>x</strong> is <em>released</em>; if we just listen for the key being pressed then we end up in a loop where we swap between levels as long as the key is held down, since the character always spawns in the new level on top of a trigger tile.</p><p>Here is a working demo. Try picking up items by walking over them and swapping levels by standing next to doors and hitting <strong>x</strong>.</p> <iframe src="//jsfiddle.net/juwalbose/qe0v3L1s/embedded" width="630" height="500"></iframe> <h2> <span class="sectionnum">3.</span> Projectiles</h2><p>A <em>projectile</em> is something that moves in a particular direction with a particular speed, like a bullet, a magic spell, a ball, etc. Everything about the projectile is the same as the hero character, apart from the height: rather than rolling along the ground, projectiles often float above it at a certain height. A bullet will travel above the waist level of the character, and even a ball may need to bounce around.</p><p>One interesting thing to note is that isometric height is the same as height in a 2D side view, although smaller in value. There are no complicated conversions involved. If a ball is 10 pixels above the ground in Cartesian coordinates, it could be 10 or 6 pixels above the ground in isometric coordinates. (In our case, the relevant axis is the y-axis.)</p><p>Let's try to implement a ball bouncing in our walled grassland. As a touch of realism, we'll add a shadow for the ball. All we need to do is to add the bounce height value to the isometric Y value of our ball. The jump height value will change from frame to frame depending on the gravity, and once the ball hits the ground we'll flip the current velocity along the y-axis.</p><p>Before we tackle bouncing in an isometric system, we'll see how we can implement it in a 2D Cartesian system. Let's represent the jump power of the ball with a variable <code>zValue</code>. Imagine that, to begin with, the ball has a jump power of 100, so <code>zValue = 100</code>. </p><p>We'll use two more variables: <code>incrementValue</code>, which starts at <code>0</code>, and <code>gravity</code>, which has a value of <code class="inline">-1</code>. Each frame, we subtract <code>incrementValue</code> from <code>zValue</code>, and subtract <code>gravity</code> from <code>incrementValue</code> in order to create a dampening effect. When <code>zValue</code> reaches <code>0</code>, it means the ball has reached the ground; at this point, we flip the sign of <code>incrementValue</code> by multiplying it by <code>-1</code>, turning it into a positive number. This means that the ball will move upwards from the next frame, thus bouncing.</p><p>Here's how that looks in code:</p><pre class="brush: javascript noskimlinks noskimwords">if(game.input.keyboard.isDown(Phaser.Keyboard.X)){ zValue=100; } incrementValue-=gravity; zValue-=incrementValue; if(zValue&lt;=0){ zValue=0; incrementValue*=-1; }</pre><p>The code remains the same for the isometric view as well, with the slight difference that you can use a lower value for <code class="inline">zValue</code> to start with. See below how the <code class="inline">zValue</code> is added to the isometric <code class="inline">y</code> value of the ball while rendering.</p><pre class="brush: javascript noskimlinks noskimwords">function drawBallIso(){ var isoPt= new Phaser.Point();//It is not advisable to create points in update loop var ballCornerPt=new Phaser.Point(ballMapPos.x-ball2DVolume.x/2,ballMapPos.y-ball2DVolume.y/2); isoPt=cartesianToIsometric(ballCornerPt);//find new isometric position for hero from 2D map position gameScene.renderXY(ballShadowSprite,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture gameScene.renderXY(ballSprite,isoPt.x+borderOffset.x+ballOffset.x, isoPt.y+borderOffset.y-ballOffset.y-zValue, false);//draw hero to render texture }</pre><p>Check out the interactive example below:</p> <iframe src="//jsfiddle.net/juwalbose/cx02Lh9t/embedded" width="630" height="500"></iframe> <p>Do understand that the role played by the shadow is a very important one which adds to the realism of this illusion. Also, note that we're now using the two screen coordinates (x and y) to represent three dimensions in isometric coordinates—the y-axis in screen coordinates is also the z-axis in isometric coordinates. This can be confusing!</p><h2> <span class="sectionnum">4.</span> Finding and Following a Path</h2><p>Pathfinding and path following are fairly complicated processes. There are various approaches using different algorithms for finding the path between two points, but as our <code class="inline">levelData</code> is a 2D array, things are easier than they might otherwise be. We have well-defined and unique nodes which the player can occupy, and we can easily check whether they are walkable.<br></p><h3>Related Posts</h3><ul> <li><a href="http://www.policyalmanac.org/games/aStarTutorial.htm" rel="external" target="_blank">A* Pathfinding for Beginners</a></li> <li><a href="https://gamedev.tutsplus.com/tutorials/implementation/goal-based-vector-field-pathfinding/" rel="external" target="_blank">Goal-Based Vector Field Pathfinding</a></li> <li><a href="https://gamedev.tutsplus.com/tutorials/implementation/speed-up-a-star-pathfinding-with-the-jump-point-search-algorithm/" rel="external" target="_blank">Speed Up A* Pathfinding With the Jump Point Search Algorithm</a></li> <li><a href="https://gamedev.tutsplus.com/tutorials/implementation/understanding-steering-behaviors-path-following/" rel="external" target="_blank">The "Path Following" Steering Behavior</a></li> </ul><p>A detailed overview of pathfinding algorithms is outside of the scope of this article, but I will try to explain the most common way it works: the shortest path algorithm, of which A* and Dijkstra's algorithms are famous implementations.</p><p>We aim to find nodes connecting a starting node and an ending node. From the starting node, we visit all eight neighbouring nodes and mark them all as visited; this core process is repeated for each newly visited node, recursively. </p><p>Each thread tracks the nodes visited. When jumping to neighbouring nodes, nodes that have already been visited are skipped (the recursion stops); otherwise, the process continues until we reach the ending node, where the recursion ends and the full path followed is returned as a node array. Sometimes the end node is never reached, in which case the pathfinding fails. We usually end up finding multiple paths between the two nodes, in which case we take the one with the smallest number of nodes.</p><h3>Pathfinding</h3><p>It is unwise to reinvent the wheel when it comes to well-defined algorithms, so we would use existing solutions for our path-finding purposes. To use Phaser, we need a JavaScript solution, and the one I have chosen is <a href="http://easystarjs.com/" rel="external" target="_blank">EasyStarJS</a>. We initialise the path-finding engine as below.</p><pre class="brush: javascript noskimlinks noskimwords">easystar = new EasyStar.js(); easystar.setGrid(levelData); easystar.setAcceptableTiles(); easystar.enableDiagonals();// we want path to have diagonals easystar.disableCornerCutting();// no diagonal path when walking at wall corners</pre><p>As our <code class="inline">levelData</code> has only <code class="inline">0</code> and <code class="inline">1</code>, we can directly pass it in as the node array. We set the value of <code class="inline">0</code> as the walkable node. We enable diagonal walking capability but disable this when walking close to the corners of non-walkable tiles. </p><p>This is because, if enabled, the hero may cut into the non-walkable tile while doing a diagonal walk. In such a case, our collision detection will not allow the hero to pass through. Also, please be advised that in the example I have completely removed the collision detection as that is no longer necessary for an AI-based walk example. </p><p>We will detect the tap on any free tile inside the level and calculate the path using the <code class="inline">findPath</code> function. The callback method <code class="inline">plotAndMove</code> receives the node array of the resulting path. We mark the <code class="inline">minimap</code> with the newly found path.</p><pre class="brush: javascript noskimlinks noskimwords">game.input.activePointer.leftButton.onUp.add(findPath) function findPath(){ if(isFindingPath || isWalking)return; var pos=game.input.activePointer.position; var isoPt= new Phaser.Point(pos.x-borderOffset.x,pos.y-borderOffset.y); tapPos=isometricToCartesian(isoPt); tapPos.x-=tileWidth/2;//adjustment to find the right tile for error due to rounding off tapPos.y+=tileWidth/2; tapPos=getTileCoordinates(tapPos,tileWidth); if(tapPos.x&gt;-1&amp;&amp;tapPos.y&gt;-1&amp;&amp;tapPos.x&lt;7&amp;&amp;tapPos.y&lt;7){//tapped within grid if(levelData[tapPos.y][tapPos.x]!=1){//not wall tile isFindingPath=true; //let the algorithm do the magic easystar.findPath(heroMapTile.x, heroMapTile.y, tapPos.x, tapPos.y, plotAndMove); easystar.calculate(); } } } function plotAndMove(newPath){ destination=heroMapTile; path=newPath; isFindingPath=false; repaintMinimap(); if (path === null) { console.log("No Path was found."); }else{ path.push(tapPos); path.reverse(); path.pop(); for (var i = 0; i &lt; path.length; i++) { var tmpSpr=minimap.getByName("tile"+path[i].y+"_"+path[i].x); tmpSpr.tint=0x0000ff; //console.log("p "+path[i].x+":"+path[i].y); } } }</pre><figure class="post_image"><img alt="Isometric level with the newly found path highlighted in minimap" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28503/image/isometric-advanced-pathfinding-fixed.png"></figure><h3>Path Following</h3><p>Once we have the path as a node array, we need to make the character follow it.</p><p>Say we want to make the character walk to a tile that we click on. We first need to look for a path between the node that the character currently occupies and the node where we clicked. If a successful path is found, then we need to move the character to the first node in the node array by setting it as the destination. Once we get to the destination node, we check whether there are any more nodes in the node array and, if so, set the next node as the destination—and so on until we reach the final node.</p><p>We will also change the direction of the player based on the current node and the new destination node each time we reach a node. Between nodes, we just walk in the required direction until we reach the destination node. This is a very simple AI, and in the example this is done in the method <code class="inline">aiWalk</code> shown partially below.</p><pre class="brush: javascript noskimlinks noskimwords">function aiWalk(){ if(path.length==0){//path has ended if(heroMapTile.x==destination.x&amp;&amp;heroMapTile.y==destination.y){ dX=0; dY=0; isWalking=false; return; } } isWalking=true; if(heroMapTile.x==destination.x&amp;&amp;heroMapTile.y==destination.y){//reached current destination, set new, change direction //wait till we are few steps into the tile before we turn stepsTaken++; if(stepsTaken&lt;stepsTillTurn){ return; } console.log("at "+heroMapTile.x+" ; "+heroMapTile.y); //centralise the hero on the tile heroMapSprite.x=(heroMapTile.x * tileWidth)+(tileWidth/2)-(heroMapSprite.width/2); heroMapSprite.y=(heroMapTile.y * tileWidth)+(tileWidth/2)-(heroMapSprite.height/2); heroMapPos.x=heroMapSprite.x+heroMapSprite.width/2; heroMapPos.y=heroMapSprite.y+heroMapSprite.height/2; stepsTaken=0; destination=path.pop();//whats next tile in path if(heroMapTile.x&lt;destination.x){ dX = 1; }else if(heroMapTile.x&gt;destination.x){ dX = -1; }else { dX=0; } if(heroMapTile.y&lt;destination.y){ dY = 1; }else if(heroMapTile.y&gt;destination.y){ dY = -1; }else { dY=0; } if(heroMapTile.x==destination.x){ dX=0; }else if(heroMapTile.y==destination.y){ dY=0; } //...... } }</pre><p>We <em>do</em> need to filter out valid click points by determining whether we've clicked within the walkable area, rather than a wall tile or other non-walkable tile.</p><p>Another interesting point for coding the AI: we do not want the character to turn to face the next tile in the node array as soon as he has arrived in the current one, as such an immediate turn results in our character walking on the borders of tiles. Instead, we should wait until the character is a few steps inside the tile before we look for the next destination. It is also better to manually place the hero in the middle of the current tile just before we turn, to make it all feel perfect.</p><p>Check out the working demo below:</p> <iframe src="//jsfiddle.net/juwalbose/pu0gt7nc/embedded" width="630" height="500"></iframe> <h2> <span class="sectionnum">5.</span> Isometric Scrolling</h2><p>When the level area is much larger than the available screen area, we will need to make it <em>scroll</em>.</p><figure class="post_image"><img alt="Isometric level with 12x12 visible area" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28503/image/isometric-advanced-scrolling.png"></figure><p>The visible screen area can be considered as a smaller rectangle within the larger rectangle of the complete level area. Scrolling is, essentially, just moving the inner rectangle inside the larger one. Usually, when such scrolling happens, the position of the hero remains the same with respect to the screen rectangle, commonly at the screen center. Interestingly, all we need to implement scrolling is to track the corner point of the inner rectangle.</p><p>This corner point, which we represent in Cartesian coordinates, will fall within a tile in the level data. For scrolling, we increment the x- and y-position of the corner point in Cartesian coordinates. Now we can convert this point to isometric coordinates and use it to draw the screen. </p><p>The newly converted values, in isometric space, need to be the corner of our screen too, which means they are the new <code>(0, 0)</code>. So, while parsing and drawing the level data, we subtract this value from the isometric position of each tile, and can determine if the tile's new position falls within the screen. </p><p>Alternatively, we can decide we are going to draw only an <em>X x Y</em> isometric tile grid on screen to make the drawing loop efficient for larger levels. </p><p>We can express this in steps as so:<br></p><ul> <li>Update Cartesian corner point's x- and y-coordinates.</li> <li>Convert this to isometric space.</li> <li>Subtract this value from the isometric draw position of each tile.</li> <li>Draw only a limited predefined number of tiles on screen starting from this new corner.</li> <li>Optional: Draw the tile only if the new isometric draw position falls within the screen.</li> </ul><pre class="brush: javascript noskimlinks noskimwords">var cornerMapPos=new Phaser.Point(0,0); var cornerMapTile=new Phaser.Point(0,0); var visibleTiles=new Phaser.Point(6,6); //... function update(){ //... if (isWalkable()) { heroMapPos.x += heroSpeed * dX; heroMapPos.y += heroSpeed * dY; //move the corner in opposite direction cornerMapPos.x -= heroSpeed * dX; cornerMapPos.y -= heroSpeed * dY; cornerMapTile=getTileCoordinates(cornerMapPos,tileWidth); //get the new hero map tile heroMapTile=getTileCoordinates(heroMapPos,tileWidth); //depthsort &amp; draw new scene renderScene(); } } function renderScene(){ gameScene.clear();//clear the previous frame then draw again var tileType=0; //let us limit the loops within visible area var startTileX=Math.max(0,0-cornerMapTile.x); var startTileY=Math.max(0,0-cornerMapTile.y); var endTileX=Math.min(levelData.length,startTileX+visibleTiles.x); var endTileY=Math.min(levelData.length,startTileY+visibleTiles.y); startTileX=Math.max(0,endTileX-visibleTiles.x); startTileY=Math.max(0,endTileY-visibleTiles.y); //check for border condition for (var i = startTileY; i &lt; endTileY; i++) { for (var j = startTileX; j &lt; endTileX; j++) { tileType=levelData[i][j]; drawTileIso(tileType,i,j); if(i==heroMapTile.y&amp;&amp;j==heroMapTile.x){ drawHeroIso(); } } } } function drawHeroIso(){ var isoPt= new Phaser.Point();//It is not advisable to create points in update loop var heroCornerPt=new Phaser.Point(heroMapPos.x-hero2DVolume.x/2+cornerMapPos.x,heroMapPos.y-hero2DVolume.y/2+cornerMapPos.y); isoPt=cartesianToIsometric(heroCornerPt);//find new isometric position for hero from 2D map position gameScene.renderXY(sorcererShadow,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture gameScene.renderXY(sorcerer,isoPt.x+borderOffset.x+heroWidth, isoPt.y+borderOffset.y-heroHeight, false);//draw hero to render texture } function drawTileIso(tileType,i,j){//place isometric level tiles var isoPt= new Phaser.Point();//It is not advisable to create point in update loop var cartPt=new Phaser.Point();//This is here for better code readability. cartPt.x=j*tileWidth+cornerMapPos.x; cartPt.y=i*tileWidth+cornerMapPos.y; isoPt=cartesianToIsometric(cartPt); //we could further optimise by not drawing if tile is outside screen. if(tileType==1){ gameScene.renderXY(wallSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y-wallHeight, false); }else{ gameScene.renderXY(floorSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y, false); } }</pre><p>Please note that the corner point is incremented in the <em>opposite</em> direction to the hero's position update as he moves. This makes sure that the hero stays where he is with respect to the screen. Check out this example (use arrows to scroll, tap to increase the visible grid).</p> <iframe src="//jsfiddle.net/juwalbose/duzbpbky/embedded" width="630" height="500"></iframe> <p>A couple of notes:</p><ul> <li>While scrolling, we may need to draw additional tiles at the screen borders, or else we may see tiles disappearing and appearing at the screen extremes.</li> <li>If you have tiles that take up more than one space, then you will need to draw more tiles at the borders. For example, if the largest tile in the whole set measures X by Y, then you will need to draw X more tiles to the left and right and Y more tiles to the top and bottom. This makes sure that the corners of the bigger tile will still be visible when scrolling in or out of the screen.</li> <li>We still need to make sure that we don't have blank areas on the screen while we are drawing near the borders of the level.</li> <li>The level should only scroll until the most extreme tile gets drawn at the corresponding screen extreme—after this, the character should continue moving in screen space <em>without</em> the level scrolling. For this, we will need to track all four corners of the inner screen rectangle, and throttle the scrolling and player movement logic accordingly. Are you up for the challenge to try implementing that for yourself?</li> </ul><h2>Conclusion</h2><p>This series is particularly aimed at beginners trying to explore isometric game worlds. Many of the concepts explained have alternate approaches which are a bit more complicated, and I have deliberately chosen the easiest ones. </p><p>They may not fulfil most of the scenarios you may encounter, but the knowledge gained can be used to build upon these concepts to create more complicated solutions. For example, the simple depth sorting implemented will break when we have multi-storied levels and platform tiles moving from one story to the other. </p><p>But that is a tutorial for another time. </p> 2017-05-12T13:00:10.000Z 2017-05-12T13:00:10.000Z Juwal Bose tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-28392 An Updated Primer for Creating Isometric Worlds, Part 1 <figure class="final-product final-product--image"><img data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/final_image/Isometric-phaser-finished.png" alt="Final product image"><figcaption>What You'll Be Creating</figcaption></figure><p>We have all played our fair share of amazing<em> isometric games</em>, be it the original Diablo, or Age of Empires or Commandos. The first time you came across an isometric game, you may have wondered if it was a <em>2D game</em> or a <em>3D game</em> or something completely different. The world of isometric games has its mystical attraction for game developers as well. Let us try to unravel the mystery of isometric projection and try to create a simple isometric world in this tutorial.</p><p>This tutorial is an updated version of my <a href="https://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511" target="_self">existing tutorial on creating isometric worlds</a>. The original tutorial used Flash with ActionScript and is still relevant for Flash or <a href="http://www.openfl.org/" rel="external" target="_blank">OpenFL</a> developers. In this new tutorial I have decided to use <a href="https://phaser.io/" rel="external" target="_blank">Phaser</a> with JS code, thereby creating interactive HTML5 output instead of SWF output. </p><p>Please be advised that this is not a Phaser development tutorial, but we are just using Phaser to easily communicate the core concepts of creating an isometric scene. Besides, there are much better and easier ways to create isometric content in Phaser, such as the <a href="http://rotates.org/phaser/iso/" rel="external" target="_blank">Phaser Isometric Plugin</a>. </p><p>For the sake of simplicity, we will use the tile-based approach to create our isometric scene.</p><h2> <span class="sectionnum">1.</span> Tile-Based Games</h2><p>In 2D games using the tile-based approach, each visual element is broken down into smaller pieces, called tiles, of a standard size. These tiles will be arranged to form the game world according to pre-determined level data—usually a two-dimensional array.<br></p><h3>Related Posts</h3><ul><li><a href="http://www.gotoandplay.it/_articles/2004/02/tonypa.php" rel="external" target="_blank">Tony Pa's tile-based tutorials</a></li></ul><p>Usually tile-based games use either a <em>top-down</em> view or a <em>side view</em> for the game scene. Let us consider a standard top-down 2D view with two tiles—a <em>grass tile</em> and a <em>wall tile</em>—as shown here:</p><figure class="post_image"><img alt="Green and Maroon tiles" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-2dtiles-new.png"></figure><p>Both of these tiles are square images of the same size, hence the <em>tile height</em> and <em>tile width</em> are the same. Let us consider a game level which is a grassland enclosed on all sides by walls. In such a case, the level data represented with a two-dimensional array will look like this:</p><pre class="brush: plain noskimlinks noskimwords">[ [1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,0,0,0,1], [1,0,0,0,0,1], [1,0,0,0,0,1], [1,1,1,1,1,1] ]</pre><p>Here, <code class="inline">0</code> denotes a grass tile and <code>1</code> denotes a wall tile. Arranging the tiles according to the level data will produce our walled grassland as shown in the image below:</p><figure class="post_image"><img alt="Top view level - grass area surrounded by walls" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-2dlevel-new.png"></figure><p>We can go a bit further by adding corner tiles and separate vertical and horizontal wall tiles, requiring five additional tiles, which leads us to our updated level data:</p><pre class="brush: plain noskimlinks noskimwords">[ [3,1,1,1,1,4], [2,0,0,0,0,2], [2,0,0,0,0,2], [2,0,0,0,0,2], [2,0,0,0,0,2], [6,1,1,1,1,5] ]</pre><p>Check out the image below, where I have marked the tiles with their corresponding tile numbers in the level data:</p><figure class="post_image"><img alt="Better top view level with corner tiles and tile numbers" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-2dcomplex-level-new.png"></figure><p>Now that we have understood the concept of the tile-based approach, let me show you how we can use a straightforward 2D grid pseudo code to render our level:</p><pre class="brush: javascript noskimlinks noskimwords">for (i, loop through rows) for (j, loop through columns) x = j * tile width y = i * tile height tileType = levelData[i][j] placetile(tileType, x, y)</pre><p>If we use the above tile images then the tile width and tile height are equal (and the same for all tiles), and will match the tile images' dimensions. So the tile width and tile height for this example are both 50 px, which makes up the total level size of 300 x 300 px—that is, six rows and six columns of tiles measuring 50 x 50 px each.</p><p>As discussed earlier, in a normal tile-based approach, we either implement a top-down view or a side view; for an isometric view, we need to implement the <em>isometric projection</em>.</p><h2> <span class="sectionnum">2.</span> Isometric Projection</h2><p>The best technical explanation of what <em>isometric projection</em> means, as far as I'm aware, is from <a href="http://flarerpg.org/tutorials/isometric_intro/">this article by Clint Bellanger</a>:</p><blockquote>We angle our camera along two axes (swing the camera 45 degrees to one side, then 30 degrees down). This creates a diamond (rhombus) shaped grid where the grid spaces are twice as wide as they are tall. This style was popularized by strategy games and action RPGs. If we look at a cube in this view, three sides are visible (top and two facing sides).</blockquote><p>Although it sounds a bit complicated, actually implementing this view is very easy. What we need to understand is the relation between 2D space and the isometric space—that is, the relation between the level data and the view; the transformation from top-down <em>Cartesian</em> coordinates to isometric coordinates. The image below shows the visual transformation:</p><figure class="post_image"><img alt="Side by side view of top down and isometric grids" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-transformation-new.jpg"></figure><h3>Placing Isometric Tiles</h3><p>Let me try to simplify the relationship between level data stored as a 2D array and the isometric view—that is, how we transform Cartesian coordinates into isometric coordinates. We will try to create the isometric view for our now-famous walled grassland. The 2D view implementation of the level was a straightforward iteration with two loops, placing square tiles offsetting each with the fixed tile height and tile width values. For the isometric view, the pseudo code remains the same, but the <code>placeTile()</code> function changes.</p><p>The original function just draws the tile images at the provided coordinates <code class="inline">x</code> and <code class="inline">y</code>, but for an isometric view we need to calculate the corresponding isometric coordinates. The equations to do this are as follows, where <code>isoX</code> and <code>isoY</code> represent isometric x- and y-coordinates, and <code>cartX</code> and <code>cartY</code> represent Cartesian x- and y-coordinates:</p><pre class="brush: javascript noskimlinks noskimwords">//Cartesian to isometric: isoX = cartX - cartY; isoY = (cartX + cartY) / 2;</pre><pre class="brush: javascript noskimlinks noskimwords">//Isometric to Cartesian: cartX = (2 * isoY + isoX) / 2; cartY = (2 * isoY - isoX) / 2; </pre><p>Yes, that is it. These simple equations are the magic behind isometric projection. Here are Phaser helper functions which can be used to convert from one system to another using the very convenient <code class="inline">Point</code> class:</p><pre class="brush: javascript noskimlinks noskimwords">function cartesianToIsometric(cartPt){ var tempPt=new Phaser.Point(); tempPt.x=cartPt.x-cartPt.y; tempPt.y=(cartPt.x+cartPt.y)/2; return (tempPt); }</pre><pre class="brush: javascript noskimlinks noskimwords">function isometricToCartesian(isoPt){ var tempPt=new Phaser.Point(); tempPt.x=(2*isoPt.y+isoPt.x)/2; tempPt.y=(2*isoPt.y-isoPt.x)/2; return (tempPt); } </pre><p>So we can use the <code class="inline">cartesianToIsometric</code> helper method to convert the incoming 2D coordinates into isometric coordinates inside the <code class="inline">placeTile</code> method. Apart from this, the rendering code remains the same, but we need to have new images for the tiles. We cannot use the old square tiles used for our top-down rendering. The image below shows the new isometric grass and wall tiles along with the rendered isometric level:</p><figure class="post_image"><img alt="Isometric level walled grassland along with the isometric tiles used" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-isolevel-new.png"></figure><p>Unbelievable, isn't it? Let's see how a typical 2D position gets converted to an isometric position:</p><pre class="brush: javascript noskimlinks noskimwords">2D point = [100, 100]; // isometric point will be calculated as below isoX = 100 - 100; // = 0 isoY = (100 + 100) / 2; // = 100 Iso point == [0, 100];</pre><p>Similarly, an input of <code>[0, 0]</code> will result in <code>[0, 0]</code>, and <code>[10, 5]</code> will give <code>[5, 7.5]</code>.</p><p>For our walled grassland, we can determine a walkable area by checking whether the array element is <code>0</code> at that coordinate, thereby indicating grass. For this we need to determine the array coordinates. We can find the tile's coordinates in the level data from its Cartesian coordinates using this function:</p><pre class="brush: javascript noskimlinks noskimwords">function getTileCoordinates(cartPt, tileHeight){ var tempPt=new Phaser.Point(); tempPt.x=Math.floor(cartPt.x/tileHeight); tempPt.y=Math.floor(cartPt.y/tileHeight); return(tempPt); }</pre><p>(Here, we essentially assume that tile height and tile width are equal, as in most cases.) </p><p>Hence, from a pair of screen (isometric) coordinates, we can find tile coordinates by calling:</p><pre class="brush: javascript noskimlinks noskimwords">getTileCoordinates(isometricToCartesian(screen point), tile height);</pre><p>This screen point could be, say, a mouse click position or a pick-up position.</p><h3>Registration Points</h3><p>In Flash, we could set arbitrary points for a graphic as its centre point or <code class="inline">[0,0]</code>. The Phaser equivalent is <code class="inline">Pivot</code>. When you place the graphic at say <code class="inline">[10,20]</code>, then this <code class="inline">Pivot</code> point will get aligned with <code class="inline">[10,20]</code>. By default, the top left corner of a graphic is considered its <code class="inline">[0,0]</code> or <code class="inline">Pivot</code>. If you try to create the above level using the code provided, then you will not get the displayed result. Instead, you will get a flat land without the walls, like below:</p><figure class="post_image"><img alt="The issue with wall tiles when rendered normally" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-flat-level-new.png"></figure><p>This is because the tile images are of different sizes and we are not addressing the height attribute of the wall tile. The below image shows the different tile images that we use with their bounding boxes and a white circle where their default [0,0] is:</p><figure class="post_image"><img alt="How to properly align the different tiles along with their registration points" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-registration-points-new.png"></figure><p>See how the hero gets misaligned when drawing using the default pivots. Also notice how we lose the height of the wall tile if drawn using default pivots. The image on the right shows how they need to be properly aligned so that the wall tile gets its height and the hero gets placed in the middle of the grass tile. This issue can be solved in different ways.</p><ol> <li>Make all tiles in the same image size with the graphic aligned properly within the image. This creates a lot of empty areas within each tile graphic.</li> <li>Set pivot points manually for each tile so that they align properly.</li> <li>Draw tiles with specific offsets so that they align properly.</li> </ol><p>For this tutorial, I have chosen to use the third method so that this works even with a framework without the ability to set pivot points.</p><h2> <span class="sectionnum">3.</span> Moving in Isometric Coordinates</h2><p>We will never try to move our character or projectile in isometric coordinates directly. Instead, we will manipulate our game world data in Cartesian coordinates and just use the above functions for updating those on the screen. For example, if you want to move a character forward in the positive y-direction, you can simply increment its <code>y</code> property in 2D coordinates and then convert the resulting position to isometric coordinates:</p><pre class="brush: javascript noskimlinks noskimwords">y = y + speed; placetile(cartesianToIsometric(new Phaser.Point(x, y)))</pre><p>This will be a good time to review all the new concepts that we have learned so far and to try and create a working example of something moving in an isometric world. You can find the necessary image assets in the <code class="inline">assets</code> folder of the source git repository.</p><h3>Depth Sorting</h3><p>If you tried to move the ball image in our walled garden then you would come across the problems with <em>depth sorting</em>. In addition to normal placement, we will need to take care of <em>depth sorting</em> for drawing the isometric world, if there are moving elements. Proper depth sorting makes sure that items closer to the screen are drawn on top of items farther away.</p><p>The simplest depth sorting method is simply to use the Cartesian y-coordinate value, as mentioned in <a href="https://gamedev.tutsplus.com/tutorials/implementation/cheap-and-easy-isometric-levels/">this Quick Tip</a>: the further up the screen the object is, the earlier it should be drawn. This may work well for very simple isometric scenes, but a better way will be to redraw the isometric scene once a movement happens, according to the tile's array coordinates. Let me explain this concept in detail with our pseudo code for level drawing:</p><pre class="brush: javascript noskimlinks noskimwords">for (i, loop through rows) for (j, loop through columns) x = j * tile width y = i * tile height tileType = levelData[i][j] placetile(tileType, x, y)</pre><p>Imagine our item or character is on the tile <code class="inline">[1,1]</code>—that is, the topmost green tile in the isometric view. In order to properly draw the level, the character needs to be drawn after drawing the corner wall tile, both the left and right wall tiles, and the ground tile, like below:</p><figure class="post_image"><img alt="Hero standing on the corner tile" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-depth-sort-new.png"></figure><p>If we follow our draw loop as per the pseudo code above, we will draw the middle corner wall first, and then will continue to draw all the walls in the top right section until it reaches the right corner. </p><p>Then, in the next loop, it will draw the wall on the left of the character, and then the grass tile on which the character is standing. Once we determine this is the tile which occupies our character, we will draw the character <em>after</em> drawing the grass tile. This way, if there were walls on the three free grass tiles connected to the one on which the character is standing, those walls will overlap the character, resulting in proper depth sorted rendering.</p><h2> <span class="sectionnum">4.</span> Creating the Art</h2><p>Isometric art can be pixel art, but it doesn't have to be. When dealing with isometric pixel art, <a href="http://www.gotoandplay.it/_articles/2004/10/tcgtipa.php">RhysD's guide</a> tells you almost everything you need to know. Some theory can be found <a href="http://en.wikipedia.org/wiki/Isometric_graphics_in_video_games_and_pixel_art">on Wikipedia</a> as well.</p><p>When creating isometric art, the general rules are:</p><ul> <li>Start with a blank isometric grid and adhere to pixel-perfect precision.</li> <li>Try to break art into single isometric tile images.</li> <li>Try to make sure that each tile is either <em>walkable</em> or <em>non-walkable</em>. It will be complicated if we need to accommodate a single tile that contains both walkable and non-walkable areas.</li> <li>Most tiles will need to seamlessly tile in one or more directions.</li> <li>Shadows can be tricky to implement, unless we use a layered approach where we draw shadows on the ground layer and then draw the hero (or trees, or other objects) on the top layer. If the approach you use is not multi-layered, make sure shadows fall to the front so that they won't fall on, say, the hero when he stands behind a tree.</li> <li>In case you need to use a tile image larger than the standard isometric tile size, try to use a dimension which is a multiple of the iso tile size. It is better to have a layered approach in such cases, where we can split the art into different pieces based on its height. For example, a tree can be split into three pieces: the root, the trunk, and the foliage. This makes it easier to sort depths as we can draw pieces in corresponding layers which correspond with their heights.</li> </ul><p>Isometric tiles that are larger than the single tile dimensions will create issues with depth sorting. Some of the issues are discussed in these links:</p><h4><strong>Related Posts</strong></h4><ul> <li><a href="http://stackoverflow.com/questions/11166667/isometric-depth-sorting-issue-with-big-objects">Bigger tiles</a></li> <li><a href="http://gamedev.stackexchange.com/questions/44966/isometric-drawing-order-with-larger-than-single-tile-images-drawing-order-algo">Splitting and Painter's algorithm</a></li> <li><a href="http://www.openspace-engine.com/support/tutorials/mapStructure">OpenSpace's post on effective ways of splitting up larger tiles</a></li> </ul><h2> <span class="sectionnum">5.</span> Isometric Characters</h2><p>First we will need to fix how many directions of motion are permitted in our game—usually, games will provide four-way movement or eight-way movement. Check out the image below to understand the correlation between the 2D space and isometric space:</p><figure class="post_image"><img alt="The directions of motion in top view and isometric view" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-directions-new.png"></figure><p>Please note that a character would be moving vertically up when we press the <strong>up arrow</strong> key in a top-down game, but for an isometric game the character will move at a 45-degree angle towards the top right corner. </p><p>For a top-down view, we could create one set of character animations facing in one direction, and simply rotate them for all the others. For isometric character art, we need to re-render each animation in each of the permitted directions—so for eight-way motion, we need to create eight animations for each action. </p><p>For ease of understanding, we usually denote the directions as North, North-West, West, South-West, South, South-East, East, and North-East. The character frames below show idle frames starting from South-East and going clockwise:</p><figure class="post_image"><img alt="The different frames of the character facing the different directions" data-src="https://cms-assets.tutsplus.com/uploads/users/1605/posts/28392/image/isometric-phaser-character-frames-new.png"></figure><p>We will place characters in the same way that we placed tiles. The movement of a character is accomplished by calculating the movement in Cartesian coordinates and then converting to isometric coordinates. Let us assume we are using the keyboard to control the character.</p><p>We will set two variables, <code>dX</code> and <code>dY</code>, based on the directional keys pressed. By default, these variables will be <code>0</code> and will be updated as per the chart below, where <code>U</code>, <code>D</code>, <code>R</code>, and <code>L</code> denote the <strong>Up</strong>, <strong>Down</strong>, <strong>Right</strong>, and <strong>Left</strong> arrow keys, respectively. A value of <code>1</code> under a key represents that the key is being pressed; <code>0</code> implies that the key is not being pressed.</p><pre class="brush: plain noskimlinks noskimwords"> Key Pos U D R L dX dY ================ 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 0 -1 0 0 1 0 1 0 0 0 0 1 -1 0 1 0 1 0 1 1 1 0 0 1 -1 1 0 1 1 0 1 -1 0 1 0 1 -1 -1</pre><p>Now, using the values of <code>dX</code> and <code>dY</code>, we can update the Cartesian coordinates like so:</p><pre class="brush: plain noskimlinks noskimwords">newX = currentX + (dX * speed); newY = currentY + (dY * speed);</pre><p>So <code>dX</code> and <code>dY</code> stand for the change in the x- and y-positions of the character, based on the keys pressed. We can easily calculate the new isometric coordinates, as we've already discussed:</p><pre class="brush: javascript noskimlinks noskimwords">Iso = cartesianToIsometric(new Phaser.Point(newX, newY))</pre><p>Once we have the new isometric position, we need to <em>move</em> the character to this position. Based on the values we have for <code>dX</code> and <code>dY</code>, we can decide which direction the character is facing and use the corresponding character art. Once the character is moved, please don't forget to repaint the level with the proper depth sorting as the tile coordinates of the character may have changed.</p><h3>Collision Detection</h3><p>Collision detection is done by checking whether the tile at the newly calculated position is a non-walkable tile. So, once we find the new position, we don't immediately move the character there, but first check to see what tile occupies that space.</p><pre class="brush: javascript noskimlinks noskimwords">tile coordinate = getTileCoordinates(isometricToCartesian(current position), tile height); if (isWalkable(tile coordinate)) { moveCharacter(); } else { //do nothing; }</pre><p>In the function <code>isWalkable()</code>, we check whether the level data array value at the given coordinate is a walkable tile or not. We must take care to update the direction in which the character is facing—<em>even if he does not move</em>, as in the case of him hitting a non-walkable tile.</p><p>Now this may sound like a proper solution, but it will only work for items without volume. This is because we are only considering a single point, which is the midpoint of the character, to calculate collision. What we really need to do is to find all the four corners of the character from its available 2D midpoint coordinate and calculate collisions for all of those. If any corner is falling inside a non-walkable tile, then we should not move the character.</p><h3>Depth Sorting With Characters</h3><p>Consider a character and a tree tile in the isometric world, and they <em>both have the same image sizes</em>, however unrealistic that sounds.</p><p>To properly understand depth sorting, we must understand that whenever the character's x- and y-coordinates are less than those of the tree, the tree overlaps the character. Whenever the character's x- and y-coordinates are greater than that of the tree, the character overlaps the tree. When they have the same x-coordinate, then we decide based on the y-coordinate alone: whichever has the higher y-coordinate overlaps the other. When they have the same y-coordinate then we decide based on the x-coordinate alone: whichever has the higher x-coordinate overlaps the other.</p><p>As explained earlier, a simplified version of this is to just sequentially draw the levels starting from the farthest tile—that is, <code>tile</code>—and then draw all the tiles in each row one by one. If a character occupies a tile, we draw the ground tile first and then render the character tile. This will work fine, because the character cannot occupy a wall tile.</p><h2> <span class="sectionnum">6.</span> Demo Time!</h2><p>This is a demo in Phaser. Click to focus on the interactive area and use your arrow keys to move the character. You may use two arrow keys to move in the diagonal directions.</p> <iframe src="//jsfiddle.net/juwalbose/w1tnu9qc/embedded/result/dark/" width="630" height="500" frameborder="0" allowfullscreen="allowfullscreen"></iframe> <p>You can find the complete source for the demo in the source repository for this tutorial.</p> 2017-05-11T13:00:54.000Z 2017-05-11T13:00:54.000Z Juwal Bose tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-25673 How to Use Tile Bitmasking to Auto-Tile Your Level Layouts <p>Crafting a visually appealing and varied tileset is a time consuming process, but the results are often worth it. However, even after creating the art, you still have to piece it all together within your level! </p><p>You can place each tile, one by one, by hand—or, you can automate the process by using <em>bitmasking</em>, so you only need to draw the shape of the terrain.</p><h2>What is Tile Bitmasking?</h2><figure class="post_image"><img alt="What is tile bitmasking" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/BeforeAfter.png"></figure><p>Tile bitmasking is a method for automatically selecting the appropriate sprite from a defined tileset. This allows you to place a generic placeholder tile everywhere you want a particular type of terrain to appear instead of hand placing a potentially enormous selection of various tiles. </p><p>See this video for a demonstration:</p><figure data-video-embed="true" data-original-url="https://www.youtube.com/watch?v=YhX-i3GutrI" class="embedded-video"> <iframe src="//www.youtube.com/embed/YhX-i3GutrI?rel=0" frameborder="0" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" allowfullscreen="allowfullscreen"></iframe> </figure><p>(You can <a href="https://github.com/tutsplus/Tile-Bitmasking" target="_self">download the demos and source files from the GitHub repo</a>.)</p><p>When dealing with multiple types of terrain, the number of different variations can exceed 300 or more tiles. Drawing this many different sprites is definitely a time-consuming process, but tile bitmasking ensures that the act of placing these tiles is quick and efficient.<br></p><p>With a static implementation of bitmasking, maps are generated at runtime. With a few small tweaks, you can expand bitmasking to allow for dynamic tiles that change during gameplay. In this tutorial, we will cover the basics of tile bitmasking while working our way towards more complicated implementations that use corner tiles and multiple terrain types.<br></p><h2><b>How Tile Bitmasking Works</b></h2><h3>Overview</h3><p>Tile bitmasking is all about calculating a numerical value and assigning a specific sprite based on that value. Each tile looks at its neighboring tiles to determine which sprite from the set to assign to itself. </p><p>Every sprite in a tileset is numbered, and the bitmasking process returns a number corresponding to the position of a sprite in the tileset. At runtime, the bitmasking procedure is performed, and every tile is updated with the appropriate sprite.</p><figure class="post_image"><img alt="Example terrain sprite sheet for tile bitmasking" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/SpriteSheetOnly.png"></figure><p>The sprite sheet above consists of terrain tiles with all of the possible border configurations. The numbers on each tile represent the bitmasking value, which we will learn how to calculate in the next section. For now, it's important to understand how the bitmasking value relates to the terrain tileset. The sprites are ordered sequentially so that a bitmasking value of <code class="inline">0</code> returns the first sprite, all the way to a value of <code class="inline">15</code> which returns the 16<sup>th</sup> sprite. </p><h3>Calculating the Bitmasking Value</h3><p>Calculating this value is relatively simple. In this example, we are assuming a single terrain type with no corner pieces. </p><p>Each tile checks for the existence of tiles to the North, West, East, and South, and each check returns a Boolean, where <code class="inline">0</code> represents an empty space and <code class="inline">1</code> signifies the presence of another terrain tile. </p><p>This Boolean result is then multiplied by the binary directional value and added to the running total of the bitmasking value—it's easier to understand with some examples:</p><h4>4-bit Directional Values</h4><ul> <li>North = 2<sup class="inline">0</sup> = 1</li> <li>West = 2<sup class="inline">1</sup> = 2</li> <li>East = 2<sup class="inline">2</sup> = 4</li> <li>South = 2<sup class="inline">3</sup> = 8</li> </ul><figure class="post_image"><img alt="Tile bitmasking example" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/1-fixed.png"></figure><p>The green square in the figure above represents the terrain tile we are calculating. We start by checking for a tile to the North. There is no tile to the North, so the Boolean check returns a value of <code class="inline">0</code>. We multiply 0 by the directional value for North, 2<sup class="inline">0</sup> = 1, giving us <code class="inline">1*0 = 0</code>. </p><p>For a terrain tile surrounded entirely by empty space, every Boolean check returns <code class="inline">0</code>, resulting in the 4-bit binary number <code class="inline">0000</code> or <code class="inline">1*0 + 2*0 + 4*0 + 8*0 = 0</code>. There are 16 total possible combinations, from 0 to 15, so the 1<sup>st</sup> sprite in the tileset will be used to represent this type of terrain tile with a value of <code class="inline">0</code>.</p><figure class="post_image"><img alt="Tile bitmasking example" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/2-fixed.png"></figure><figure class="post_image">A terrain tile bordered by a single tile to the North returns a binary value of <code class="inline">0001</code>, or <code class="inline">1*1 + 2*0 + 4*0 + 8*0 = 1</code>. The 2<sup>nd</sup> sprite in the tileset will be used to represent this type of terrain with a value of <code class="inline">1</code>.</figure><figure class="post_image"><img alt="Tile bitmasking example" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/3-fixed.png"></figure><p>A terrain tile bordered by a tile to the North and a tile to the East returns a binary value of <code class="inline">0101</code>, or <code class="inline">1*1 + 2*0 + 4*1 + 8*0 = 5</code>. The 6th sprite in the tileset will be used to represent this type of terrain with a value of <code class="inline">5</code>.</p><figure class="post_image"><img alt="Tile bitmasking example" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/4-fixed.png"></figure><p>A terrain tile bordered by a tile to the East and a tile to the West returns a binary value of <code class="inline">0110</code>, or <code class="inline">1*0 + 2*1 + 4*1 + 8*0 = 6</code>. The 7th sprite in the tileset will be used to represent this type of terrain with a value of <code class="inline">6</code>.</p><h3>Assigning Sprites to Tiles</h3><p>After calculating a tile's bitmasking value, we assign the appropriate sprite from the tileset. This final step can be performed in real time as the map loads, or the result can be saved and loaded into your tile editor of choice for further editing.<br></p><figure class="post_image"><img alt="Tile bitmasking how tiles are assigned based on the terrains shape" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/BLUE-SNAKE-FIXED.png"></figure><p>The figure on the left represents a 4-bit, single-terrain tileset as it would appear sequentially on a tile sheet. The figure on the right depicts how the tiles look in-game after they are placed using the bitmasking procedure. Each tile is marked with its bitmasking value to show the relationship between a tile’s order on the tile sheet and its position in the game. </p><p>As an example, let’s examine the tile in the upper-right corner of the figure on the right. This tile is bordered by tiles to the West and Souh. The Boolean check returns a binary value of <code class="inline">1010</code>, or <code class="inline">1*0 + 2*1 + 4*0 + 8*1 = 10</code>. This value corresponds to the 11<sup>th</sup> sprite in the tile sheet.</p><h3>Tileset Complexity</h3><p>The number of required directional Boolean checks depends on the intended complexity of your tileset. By ignoring corner pieces, you can use this simplified 4-bit solution that only requires four directional binary checks. </p><p>But what happens when you want to create more visually appealing terrain? You will need to deal with the existence of corner tiles, which increases the amount of sprites from 16 to 48. The following 8-bit bitmasking example requires eight Boolean directional checks per tile.</p><h2><b>8-Bit Bitmasking with Corner Tiles</b></h2><p>For this example, we are creating a top-down tileset that depicts grassy terrain near the ocean. In this case, our ocean exists on a layer underneath the terrain tiles. This allows us to use a single-terrain solution, while still maintaining the illusion that two terrain types are colliding. </p><p>Once the game is running and the bitmasking procedure is complete, the sprites will never change. This is a seamless, static implementation of bitmasking where everything takes place before the player ever sees the tiles.</p><h3><b>Introducing Corner Tiles</b></h3><p>We want the terrain to be more visually interesting than the previous 4-bit solution, so corner pieces are required. This extra bit of visual complexity requires an exponential amount of additional work for the artist, programmer, and the game itself. By expanding on what we learned from the 4-bit solution, we can quickly understand how to approach the 8-bit solution.</p><figure class="post_image"><img alt="A sprite sheet of an example tileset" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/CondensedTilemap8bit.png"></figure><p>Here is the complete sprite sheet for our ocean-side terrain tiles. Do you notice anything peculiar about the number of tiles? The 4-bit example from earlier resulted in 2<sup class="inline">4</sup> = 16 tiles, so this 8-bit example should surely result in 2<sup class="inline">8</sup> = 256 tiles, yet there are clearly fewer than that there. </p><p>While it’s true that this 8-bit bitmasking procedure results in 256 possible binary values, not every combination requires an entirely unique tile. The following example will help explain how 256 combinations can be represented by only 48 tiles.</p><h4>8-bit Directional Values</h4><ul> <li>North West = 2<sup class="inline">0</sup> = 1</li> <li>North = 2<sup class="inline">1</sup> = 2</li> <li>North East = 2<sup class="inline">2</sup> = 4</li> <li>West = 2<sup class="inline">3</sup> = 8</li> <li>East = 2<sup class="inline">4</sup> = 16</li> <li>South West = 2<sup class="inline">5</sup> = 32</li> <li>South= 2<sup class="inline">6</sup> = 64</li> <li>South East = 2<sup class="inline">7</sup> = 128</li> </ul><figure class="post_image"><img alt="Tile bitmasking 8-bit example" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/1b.png"></figure><p>Now we're making <em>eight</em> Boolean directional checks. The center tile above is bordered by tiles to the North, North-East, and East, so this Boolean check returns a binary value of <code class="inline">00010110</code> or <code class="inline">1*0 + 2*1 + 4*1 + 8*0 + 16*1 + 32*0 + 64*0 + 128*0 = 22</code>.</p><figure class="post_image"><img alt="Tile bitmasking 8-bit example" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/182-to-22.png"></figure><p>The tile on the left above is similar to the previous tile, but now it is also bordered by tiles to the South West and South East. This Boolean directional check <em>should</em> return a binary value of <code class="inline">10110110</code>, or <code class="inline">1*0 + 2*1 + 4*1 + 8*0 + 16*1 + 32*1 + 64*0 + 128*1 = 182</code>. </p><p>This value is different from the previous tile, but both tiles would actually be visually identical, so it becomes redundant. </p><p>To eliminate the redundancies, we add an extra condition to our Boolean directional check: when checking for the presence of bordering <em>corner</em> tiles, we also have to check for neighboring tiles in the four cardinal directions (directly North, East, South, or West). </p><p>For example, the tile to the North-East is neighbored by existing tiles, whereas the tiles to the South-West and South-East are not. This means that the South-West and South-East tiles are not included in the bitmasking calculation. </p><p>With this new condition, this Boolean check returns a binary value of <code class="inline">00010110</code> or <code class="inline">1*0 + 2*1 + 4*1 + 8*0 + 16*1 + 32*0 + 64*0 + 128*0 = 22</code> just like before. Now you can see how the 256 combinations can be represented by only 48 tiles.</p><h3><b>Tile Order</b></h3><p>Another problem you may notice is that the values calculated by the 8-bit bitmasking procedure no longer correlate to the sequential order of the tiles in the sprite sheet. There are only 48 tiles, but our possible calculated values range from 0 to 255, so we can no longer use the calculated value as a direct reference when grabbing the appropriate sprite. </p><p>What we need, therefore, is a data structure to contain the list of calculated values and their corresponding tile values. How you want to implement this is up to you, but remember that the order in which you check for surrounding tiles dictates the order in which your tiles should be placed in the sprite sheet. </p><p>For this example, we check for bordering tiles in the following order: North-West, North, North-East, West, East, South-West, South, South-East. </p><p>Below is the complete set of bitmasking values as they relate to the positions of tiles in our sprite sheet (feel free to use these values in your project to save time):</p><pre class="brush: plain noskimlinks noskimwords">{ 2 = 1, 8 = 2, 10 = 3, 11 = 4, 16 = 5, 18 = 6, 22 = 7, 24 = 8, 26 = 9, 27 = 10, 30 = 11, 31 = 12, 64 = 13, 66 = 14, 72 = 15, 74 = 16, 75 = 17, 80 = 18, 82 = 19, 86 = 20, 88 = 21, 90 = 22, 91 = 23, 94 = 24, 95 = 25, 104 = 26, 106 = 27, 107 = 28, 120 = 29, 122 = 30, 123 = 31, 126 = 32, 127 = 33, 208 = 34, 210 = 35, 214 = 36, 216 = 37, 218 = 38, 219 = 39, 222 = 40, 223 = 41, 248 = 42, 250 = 43, 251 = 44, 254 = 45, 255 = 46, 0 = 47 }</pre><h2>Multiple Terrain Types</h2><p>All of our previous examples assume a single terrain type, but what if we introduce a second terrain to the equation? We need a <em>5-bit</em> bitmasking solution, and we need to define our two terrain types. We also need to assign a value to the center tile that is only counted under specific conditions. Remember that we are no longer accounting for "empty space" as in the previous examples; tiles must now be surrounded by another tile on all sides.</p><figure class="post_image"><img alt="Tile bitmasking 8-bit example with multiple terrain types" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/5bit-1a.png"></figure><p>The above figure shows an example with two terrain types and no corner tiles. Type 1 <em>always</em><strong> </strong>returns a value of <code class="inline">0</code> whenever it is detected during the directional check; the center tile value is calculated and used <em>only</em> if it is terrain type 2. </p><p>The center tile in the above example is surrounded by terrain type 2 to the North, West, and East, and by terrain type 1 to the South. The center tile is terrain type 1, so it is not counted. This Boolean check returns a binary value of  <code class="inline">00111</code>, or  <code class="inline">1*1 + 2*1 + 4*1 + 8*0 + 16*0 = 7</code>.</p><figure class="post_image"><img alt="Tile bitmasking 8-bit example with multiple terrain types" data-src="https://cms-assets.tutsplus.com/uploads/users/90/posts/25673/image/5bit-2a.png"></figure><p>In this example, our center tile is terrain type 2, so it will be counted in the calculation. The center tile is surrounded by terrain type 2 to the North and West. It is also surrounded by terrain type 1 to the East and South. This Boolean check returns a binary value of  <code class="inline">10011</code>, or  <code class="inline">1*1 + 2*1 + 4*0 + 8*0 + 16*1 = 19</code> .</p><h2>Dynamic Implementation</h2><p>The bitmasking calculation can also be performed during gameplay, allowing for real-time changes in tile placement and appearance. This is useful for destructible terrain as well as games that allow for crafting and building. The initial bitmasking procedure is mandatory for all tiles, but any additional dynamic calculations should only be performed when absolutely necessary. For example, a destroyed terrain tile would trigger the bitmasking calculation only for surrounding tiles.</p><h2><b>Conclusion</b></h2><p>Tile bitmasking is the perfect example of building a working system to aid you in game development. It is not something that directly impacts the player's experience; instead, this method of automating a time-consuming portion of level design provides a valuable benefit to the developer. To put it simply: tile bitmasking is a quick way to make the game do your dirty work, allowing you to focus on more important tasks.</p> 2016-02-03T15:30:57.000Z 2016-02-03T15:30:57.000Z Sonny Bone tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-25456 A* Pathfinding for 2D Grid-Based Platformers: Ledge Grabbing <p>In this part of our series on adapting the A* pathfinding algorithm to platformers, we'll introduce a new mechanic to the character: ledge grabbing. We'll also make appropriate changes to both the pathfinding algorithm and the bot AI, so they can make use of the improved mobility.<br></p><h2>Demo</h2><p>You can <a href="http://tutsplus.github.io/A-Star-Pathfinding-for-Platformers/Demos/LedgeGrabbing/WebPlayer/index.html" target="_self">play the Unity demo</a>, or <a href="http://tutsplus.github.io/A-Star-Pathfinding-for-Platformers/Demos/LedgeGrabbing/WebGL/index.html" target="_self">the WebGL version</a> (16MB), to see the final result in action. Use <strong>WASD</strong> to move the character, <strong>left-click</strong> on a spot to find a path you can follow to get there, <strong>right-click</strong> a cell to toggle the ground at that point, <strong>middle-click</strong> to place a one-way platform, and <strong>click-and-drag</strong> the sliders to change their values.</p><h2>Ledge Grabbing Mechanics</h2><h3>Controls Overview</h3><p>Let's first take a look at how the ledge grabbing mechanic works in the demo to get some insight into how we should change our pathfinding algorithm to take this new mechanic into account.</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/ledge_grab.gif"></figure><p>The controls for ledge grabbing are quite simple: if the character is right next to a ledge while falling, and the player presses the left or right directional key to move them towards that ledge, then when character is at the right position, it will grab the ledge.</p><p>Once the character is grabbing a ledge, the player has two options: they can either jump up or drop down. Jumping works as normal; the player presses the jump key and the jump's force is identical to the force applied when jumping from the ground. Dropping down is done by pressing the down button (<strong>S</strong>), or the directional keyn that points away from the ledge.</p><h3>Implementing the Controls</h3><p>Let's go over how the ledge grab controls work in the code. The first thing here to do is to detect whether the ledge is to the left or to the right of the character:</p><pre class="brush: plain noskimlinks noskimwords">bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize &lt; mPosition.x; bool ledgeOnRight = !ledgeOnLeft;</pre><p>We can use that information to determine whether the character is supposed to drop off the ledge. As you can see, to drop down, the player needs to either:<br></p><ul> <li>press the down button,</li> <li>press the left button when we're grabbing a ledge on the right, or</li> <li>press the right button when we're grabbing a ledge on the left.</li> </ul><pre class="brush: plain noskimlinks noskimwords">bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize &lt; mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] &amp;&amp; ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] &amp;&amp; ledgeOnLeft)) { }</pre><p>There's a small caveat here. Consider a situation when we're holding the down button and the right button, when the character is holding onto a ledge to the right. It'll result in the following situation:<br></p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/ledge_grab_problem1.gif"></figure><p>The problem here is that the character grabs the ledge immediately after it lets go of it. </p><p>A simple solution to this is to lock movement towards the ledge for a couple frames after we dropped off the ledge. That's what the following snippet does:</p><pre class="brush: csharp noskimlinks noskimwords">bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize &lt; mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] &amp;&amp; ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] &amp;&amp; ledgeOnLeft)) { if (ledgeOnLeft) mCannotGoLeftFrames = 3; else mCannotGoRightFrames = 3; }</pre><p>After this, we change the state of the character to <code class="inline">Jump</code>, which will handle the jump physics:</p><pre class="brush: plain noskimlinks noskimwords">bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize &lt; mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] &amp;&amp; ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] &amp;&amp; ledgeOnLeft)) { if (ledgeOnLeft) mCannotGoLeftFrames = 3; else mCannotGoRightFrames = 3; mCurrentState = CharacterState.Jump; }</pre><p>Finally, if the character didn't drop from the ledge we check whether the jump key has been pressed; if so, we set the jump's vertical speed and change the state:</p><pre class="brush: plain noskimlinks noskimwords">bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize &lt; mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] &amp;&amp; ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] &amp;&amp; ledgeOnLeft)) { if (ledgeOnLeft) mCannotGoLeftFrames = 3; else mCannotGoRightFrames = 3; mCurrentState = CharacterState.Jump; } else if (mInputs[(int)KeyInput.Jump]) { mSpeed.y = mJumpSpeed; mCurrentState = CharacterState.Jump; }</pre><h3>Detecting a Ledge Grab Point</h3><p>Let's look at how we determine whether a ledge can be grabbed. We use a few hotspots around the edge of the character:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/character_bounds.png"></figure><p>The yellow contour represents the character's bounds. The red segments represent the wall sensors; these are used to handle the character physics. The blue segments represent where our character can grab a ledge.</p><p>To determine whether the character can grab a ledge, our code constantly checks the side it is moving towards. It's looking for an empty tile at the top of the blue segment, and then a solid tile below it which the character can grab onto. </p><p>Note: ledge grabbing is locked off if the character is jumping up. This can be easily noticed in the demo and in the animation in the Controls Overview section.</p><p>The main problem with this method is that if our character falls at a high speed, it's easy to miss a window in which it can grab a ledge. We can solve this by looking up all the tiles starting from the previous frame's position to the current frame's in search of any empty tile above a solid one. If one such tile is found, then it can be grabbed.</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/ledge_grab_problem2.gif"></figure><p>Now we've cleared up how the ledge grabbing mechanic works, let's see how to incorporate it into our pathfinding algorithm.</p><h2>Pathfinder Changes</h2><h3>Make It Possible to Turn Ledge Grabbing On and Off</h3><p>First of all, let's add a new parameter to our <code class="inline">FindPath</code> function that indicates whether the pathfinder should consider grabbing ledges. We'll name it <code class="inline">useLedges</code>:</p><pre class="brush: csharp noskimlinks noskimwords">public List&lt;Vector2i&gt; FindPath(Vector2i start, Vector2i end, int characterWidth, int characterHeight, short maxCharacterJumpHeight, bool useLedges)</pre><h3>Detect Ledge Grab Nodes</h3><h4>Conditions</h4><p>Now we need to modify the function to detect whether a particular node can be used for ledge grabbing. We can do that after checking whether the node is an "on ground" node or an "at ceiling" node, because in either case it cannot be used for ledge grabbing.<br></p><pre class="brush: csharp noskimlinks noskimwords">if (onGround) newJumpLength = 0; else if (atCeiling) { if (mNewLocationX != mLocationX) newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2 + 1, jumpLength + 1); else newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 2); } else if (/*check whether there's a ledge grabbing node here */) { } else if (mNewLocationY &lt; mLocationY) {</pre><p>All right: now we need to figure out when a node should be considered a ledge grabbing node. For cliarity, here's a diagram that shows some example ledge grabbing positions:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/ledge_grab_node.png"></figure><p>...and here's how these might look in-game:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/funny.png"><figcaption>The top character sprites are stretched to show how this looks with <a href="https://gamedevelopment.tutsplus.com/tutorials/a-pathfinding-for-2d-grid-based-platformers-different-character-sizes--cms-24912" target="_self">characters of different sizes</a>.</figcaption></figure><p>The red cells represent the checked nodes; together with the green cells, they represent the character in our algorithm. The top two situations show a 2x2 character grabbing ledges on the left and right respectively. The bottom two show the same thing, but the character's size here is 1x3 instead of 2x2.</p><p>As you can see, it should be fairly easy to detect these cases in the algorithm. The conditions for the ledge grab node will be as follows:</p><ol> <li>There is a solid tile next to the top-right/top-left character tile.</li> <li>There is an empty tile above the found solid tile.</li> <li>There is no solid tile below the character (no need to grab ledges if on the ground).</li> </ol><p>Note that the third condition is already taken care of, since we check for the ledge grab node only if the character is not on ground.</p><p>First of all, let's check whether we actually want to detect ledge grabs:</p><pre class="brush: csharp noskimlinks noskimwords">else if (useLedges)</pre><p>Now let's check whether there is a tile to the right of the top-right character node:</p><pre class="brush: csharp noskimlinks noskimwords">else if (useLedges &amp;&amp; mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0)</pre><p>And then, if above that tile there is an empty space:</p><pre class="brush: csharp noskimlinks noskimwords">else if (useLedges &amp;&amp; mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 &amp;&amp; mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight] != 0)</pre><p>Now we need to do the same thing for the left side:</p><pre class="brush: csharp noskimlinks noskimwords">else if (useLedges &amp;&amp; ((mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 &amp;&amp; mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight] != 0) || (mGrid[mNewLocationX - 1, mNewLocationY + characterHeight - 1] == 0 &amp;&amp; mGrid[mNewLocationX - 1, mNewLocationY + characterHeight] != 0)))</pre><p>There's one more thing we can optionally do, which is disable finding the ledge grab nodes if the falling speed is too high, so the path doesn't return some extreme ledge grabbing positions which would be hard to follow by the bot:</p><pre class="brush: csharp noskimlinks noskimwords">else if (useLedges &amp;&amp; jumpLength &lt;= maxCharacterJumpHeight * 2 + 6 &amp;&amp; ((mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 &amp;&amp; mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight] != 0) || (mGrid[mNewLocationX - 1, mNewLocationY + characterHeight - 1] == 0 &amp;&amp; mGrid[mNewLocationX - 1, mNewLocationY + characterHeight] != 0))) { }</pre><p>After all this, we can be sure that the found node is a ledge grab node.<br></p><h4>Adding a Special Node</h4><p>What do we when we find a ledge grab node? We need to set its jump value. </p><p>Remember, the jump value is the number which represents which phase of the jump the character would be, if it reached this cell. If you need a recap on how the algorithm works, <a href="https://gamedevelopment.tutsplus.com/tutorials/how-to-adapt-a-pathfinding-to-a-2d-grid-based-platformer-theory--cms-24662" target="_self">take another look at the theory article</a>.</p><p>It seems that all we'd need to do is to set the jump value of the node to <code class="inline">0</code>, because from the ledge grabbing point the character can effectively reset a jump, as if it were on the ground—but there are a couple points to consider here. </p><ul> <li>First, it would be nice if we could tell at a glance whether the node is a ledge grab node or not: this will be immensely helpful when creating a bot behaviour and also when filtering the nodes. </li> <li>Second, usually jumping from the ground can be executed from whichever point would be most suitable on a particular tile, but when jumping from a ledge grab, the character is stuck to a particular position and unable to do anything but start falling or jump upwards.</li> </ul><p>Considering those caveats, we'll add a special jump value for the ledge grab nodes. It doesn't really matter what this value is, but it's a good idea to make it negative, since that will lower our chances of misinterpreting the node.</p><pre class="brush: csharp noskimlinks noskimwords">const short cLedgeGrabJumpValue = -9;</pre><p>Now let's assign this value when we detect a ledge grab node:</p><pre class="brush: csharp noskimlinks noskimwords">else if (useLedges &amp;&amp; jumpLength &lt;= maxCharacterJumpHeight * 2 + 6 &amp;&amp; ((mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 &amp;&amp; mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight] != 0) || (mGrid[mNewLocationX - 1, mNewLocationY + characterHeight - 1] == 0 &amp;&amp; mGrid[mNewLocationX - 1, mNewLocationY + characterHeight] != 0))) { newJumpLength = cLedgeGrabJumpValue; }</pre><p>Making <code class="inline">cLedgeGrabJumpValue</code> negative will have an effect on the node cost calculation—it will make the algorithm prefer to use ledges rather than skip them. There are two things to note here:</p><ol> <li>Ledge grab points offer a greater possibility of movement than any other in-air nodes, because the character can jump again by using them; from this point of view, it is a good thing that these nodes will be cheaper than others. </li> <li>Grabbing too many ledges often leads to unnatural movement, because usually players don't use ledge grabs unless they are necessary to reach somewhere.</li> </ol><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/ledge_grab_problem3.gif"></figure><p>In the animation above, you can see the difference between moving up when ledges are preferred and when they are not.</p><p>For now we'll leave the cost calculation as it is, but it is fairly easy to modify it, to make ledge nodes more expensive.</p><h4>Modify the Jump Value When Jumping or Dropping From a Ledge</h4><p>Now we need to adjust the jump values for the nodes that start from the ledge grab point. We need to do this because jumping from a ledge grab position is quite different than jumping from a ground. There's very little freedom when jumping from a ledge, because the character is fixed to a particular point. </p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/position_offset.png"></figure><p>When on the ground, the character can move freely left or right and jump at the most suitable moment.</p><p>First, let's set the case when the character drops down from a ledge grab:</p><pre class="brush: csharp noskimlinks noskimwords">else if (mNewLocationY &lt; mLocationY) { if (jumpLength == cLedgeGrabJumpValue) newJumpLength = (short)(maxCharacterJumpHeight * 2 + 4); else if (jumpLength % 2 == 0) newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 2); else newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 1); }</pre><p>As you can see, the new jump length is a bit bigger if the character dropped from a ledge: this way we compensate for the lack of manoeuvrability while grabbing a ledge, which will result in a higher vertical speed before the player can reach other nodes.</p><p>Next is the case where the character drops to one side from grabbing a ledge:</p><pre class="brush: csharp noskimlinks noskimwords">else if (!onGround &amp;&amp; mNewLocationX != mLocationX) { if (jumpLength == cLedgeGrabJumpValue) newJumpLength = (short)(maxCharacterJumpHeight * 2 + 3); else newJumpLength = (short)Mathf.Max(jumpLength + 1, 1); }</pre><p>All we need to do is to set the jump value to the falling value.</p><h4>Ignore More Nodes</h4><p>We need to add a couple of additional conditions for when we need to ignore nodes. </p><p>First of all, when we're jumping from a ledge grab position, we need to go up, not to the side. This works similarly to simply jumping from the ground. The vertical speed is much higher than the possible horizontal speed at this point, and we need to model this fact in the algorithm:</p><pre class="brush: csharp noskimlinks noskimwords">if (jumpLength == cLedgeGrabJumpValue &amp;&amp; mLocationX != mNewLocationX &amp;&amp; newJumpLength &lt; maxCharacterJumpHeight * 2) continue;</pre><p>If we want to allow dropping from the ledge to the opposite side like this:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/ledge_grab_4.gif"></figure><p>Then we need to edit the condition that doesn't allow horizontal movement when the jump value is odd. That's because, currently, our special ledge grab value is equal to <code class="inline">-9</code>, so it's only appropriate to exclude all negative numbers from this condition.</p><pre class="brush: csharp noskimlinks noskimwords">if (jumpLength &gt;= 0 &amp;&amp; jumpLength % 2 != 0 &amp;&amp; mLocationX != mNewLocationX) continue;</pre><h4>Update the Node Filter</h4><p>Finally, let's move on to node filtering. All we need to do here is to add a condition for ledge grabbing nodes, so that we don't filter them out. We simply need to check if the node's jump value is equal to <code class="inline">cLedgeGrabJumpValue</code>:</p><pre class="brush: csharp noskimlinks noskimwords">|| (fNodeTmp.JumpLength == cLedgeGrabJumpValue)</pre><p>The whole filtering looks like this now:</p><pre class="brush: csharp noskimlinks noskimwords">if ((mClose.Count == 0) || (mMap.IsOneWayPlatform(fNode.x, fNode.y - 1)) || (mGrid[fNode.x, fNode.y - 1] == 0 &amp;&amp; mMap.IsOneWayPlatform(fPrevNode.x, fPrevNode.y - 1)) || (fNodeTmp.JumpLength == 3) || (fNextNodeTmp.JumpLength != 0 &amp;&amp; fNodeTmp.JumpLength == 0) //mark jumps starts || (fNodeTmp.JumpLength == 0 &amp;&amp; fPrevNodeTmp.JumpLength != 0) //mark landings || (fNode.y &gt; mClose[mClose.Count - 1].y &amp;&amp; fNode.y &gt; fNodeTmp.PY) || (fNodeTmp.JumpLength == cLedgeGrabJumpValue) || (fNode.y &lt; mClose[mClose.Count - 1].y &amp;&amp; fNode.y &lt; fNodeTmp.PY) || ((mMap.IsGround(fNode.x - 1, fNode.y) || mMap.IsGround(fNode.x + 1, fNode.y)) &amp;&amp; fNode.y != mClose[mClose.Count - 1].y &amp;&amp; fNode.x != mClose[mClose.Count - 1].x)) mClose.Add(fNode);</pre><p>That's it—these are all the changes that we needed to make to update the pathfinding algorithm.</p><h2>Bot Changes</h2><p>Now that our path shows the spots at which a character can grab a ledge, let's modify the bot's behaviour so that it makes use of this data.</p><h3>Stop Recalculating reachedX and reachedY</h3><p>First of all, to make  things clearer in the bot, let's update the <code class="inline">GetContext()</code> function. The current problem with it is that <code class="inline">reachedX</code> and <code class="inline">reachedY</code> values are constantly recalculated, which removes some information about the context. These values are used to see whether the bot has already reached the target node on its x- and y-axes, respectively. (If you need a refresher on how this works, <a href="https://gamedevelopment.tutsplus.com/tutorials/a-pathfinding-for-2d-grid-based-platformers-making-a-bot-follow-the-path--cms-24913" target="_self">check out my tutorial about coding the bot</a>.)</p><p>Let's simply change this so that if a character reaches the node on the x- or y-axis, then these values stay true as long as we don't move on to the next node.</p><p>To make this possible, we need to declare <code class="inline">reachedX</code> and <code class="inline">reachedY</code> as class members:</p><pre class="brush: csharp noskimlinks noskimwords">public bool mReachedNodeX; public bool mReachedNodeY;</pre><p>This means we no longer need to pass them to the <code class="inline">GetContext()</code> function:</p><pre class="brush: csharp noskimlinks noskimwords">public void GetContext(out Vector2 prevDest, out Vector2 currentDest, out Vector2 nextDest, out bool destOnGround)</pre><p>With these changes, we also need to reset the variables manually whenever we start moving towards the next node. The first occurrence is when we've just found the path and are going to move towards the first node:</p><pre class="brush: plain noskimlinks noskimwords">if (path != null &amp;&amp; path.Count &gt; 1) { for (var i = path.Count - 1; i &gt;= 0; --i) mPath.Add(path[i]); mCurrentNodeId = 1; mReachedNodeX = false; mReachedNodeY = false;</pre><p>The second is when we've reached the current target node and want to move towards the next:</p><pre class="brush: plain noskimlinks noskimwords">if (mReachedNodeX &amp;&amp; mReachedNodeY) { int prevNodeId = mCurrentNodeId; mCurrentNodeId++; mReachedNodeX = false; mReachedNodeY = false;</pre><p>To stop recalculating the variables, we need to replace the following lines:</p><pre class="brush: csharp noskimlinks noskimwords">reachedX = ReachedNodeOnXAxis(pathPosition, prevDest, currentDest); reachedY = ReachedNodeOnYAxis(pathPosition, prevDest, currentDest);</pre><p>...with these, which will detect whether we've reached a node on an axis only if we haven't already reached it:</p><pre class="brush: csharp noskimlinks noskimwords">if (!mReachedNodeX) mReachedNodeX = ReachedNodeOnXAxis(pathPosition, prevDest, currentDest); if (!mReachedNodeY) mReachedNodeY = ReachedNodeOnYAxis(pathPosition, prevDest, currentDest);</pre><p>Of course, we also need to replace every other occurence of <code class="inline">reachedX</code> and <code class="inline">reachedY</code> with the newly declared versions <code class="inline">mReachedNodeX</code> and <code class="inline">mReachedNodeY</code>.</p><h3>See If the Character Needs to Grab a Ledge</h3><p>Let's declare a couple variables which we will use to determine whether the bot needs to grab a ledge, and, if so, which one:</p><pre class="brush: csharp noskimlinks noskimwords">public bool mGrabsLedges = false; bool mMustGrabLeftLedge; bool mMustGrabRightLedge;</pre><p><code class="inline">mGrabsLedges</code> is a flag that we pass to the algorithm to let it know whether it should find a path including the ledge grabs. <code class="inline">mMustGrabLeftLedge</code> and <code class="inline">mMustGrabRightLedge</code> will be used to determine whether the next node is a grab ledge, and whether the bot should grab the ledge to the left or to the right.</p><p>What we want to do now is create a function that, given a node, will be able to detect whether the character at that node will be able to grab a ledge. </p><p>We'll need two functions for this: one will check if the character can grab a ledge on the left, and the other will check whether the character can grab a ledge on the right. These functions will work the same way as our pathfinding code for detecting ledges:</p><pre class="brush: csharp noskimlinks noskimwords">public bool CanGrabLedgeOnLeft(int nodeId) { return (mMap.IsObstacle(mPath[nodeId].x - 1, mPath[nodeId].y + mHeight - 1) &amp;&amp; !mMap.IsObstacle(mPath[nodeId].x - 1, mPath[nodeId].y + mHeight)); } public bool CanGrabLedgeOnRight(int nodeId) { return (mMap.IsObstacle(mPath[nodeId].x + mWidth, mPath[nodeId].y + mHeight - 1) &amp;&amp; !mMap.IsObstacle(mPath[nodeId].x + mWidth, mPath[nodeId].y + mHeight)); }</pre><p>As you can see, we check whether there's a solid tile next to our character with an empty tile above it.</p><p>Now let's go to the <code class="inline">GetContext()</code> function, and assign the appropriate values to <code class="inline">mMustGrabRightLedge</code> and <code class="inline">mMustGrabLeftLedge</code>. We need to set them to <code class="inline">true</code> if the character is supposed to grab ledges at all (that is, if <code class="inline">mGrabsLedges</code> is <code class="inline">true</code>) and if there is a ledge to grab onto.</p><pre class="brush: csharp noskimlinks noskimwords">mMustGrabLeftLedge = mGrabsLedges &amp;&amp; !destOnGround &amp;&amp; CanGrabLedgeOnLeft(mCurrentNodeId); mMustGrabRightLedge = mGrabsLedges &amp;&amp; !destOnGround &amp;&amp; CanGrabLedgeOnRight(mCurrentNodeId);</pre><p>Note that we also don't want to grab ledges if the destination node is on the ground.</p><h3>Update the Jump Values</h3><p>As you may notice, the character's position when grabbing a ledge is slightly different to its position when standing just below it:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/25456/image/position_offset.png"></figure><p>The ledge grabbing position is a bit higher than the standing position, even though these characters occupy the same node. This means that grabbing a ledge will require a slightly higher jump than just jumping on a platform, and we need to take this into account.</p><p>Let's look at the function which determines how long the jump button should be pressed:</p><pre class="brush: csharp noskimlinks noskimwords">public int GetJumpFramesForNode(int prevNodeId) { int currentNodeId = prevNodeId + 1; if (mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 &amp;&amp; mOnGround) { int jumpHeight = 1; for (int i = currentNodeId; i &lt; mPath.Count; ++i) { if (mPath[i].y - mPath[prevNodeId].y &gt;= jumpHeight) jumpHeight = mPath[i].y - mPath[prevNodeId].y; if (mPath[i].y - mPath[prevNodeId].y &lt; jumpHeight || mMap.IsGround(mPath[i].x, mPath[i].y - 1)) return GetJumpFrameCount(jumpHeight); } } return 0; }</pre><p>First of all, we'll change the initial condition. The bot should be able to jump, not just from the ground, but also when it is grabbing a ledge:</p><pre class="brush: plain noskimlinks noskimwords">if (mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 &amp;&amp; (mOnGround || mCurrentState == CharacterState.GrabLedge))</pre><p>Now we need to add a few more frames if it's jumping to grab a ledge. First of all, we need to know if it can actually do that, so let's create a function which will tell us whether the character can grab a ledge either to the left or right:<br></p><pre class="brush: csharp noskimlinks noskimwords">public bool CanGrabLedge(int nodeId) { return CanGrabLedgeOnLeft(nodeId) || CanGrabLedgeOnRight(nodeId); }</pre><p>Now let's add a couple frames to the jump when the bot needs to grab a ledge:</p><pre class="brush: plain noskimlinks noskimwords">if (mPath[i].y - mPath[prevNodeId].y &gt;= jumpHeight) jumpHeight = mPath[i].y - mPath[prevNodeId].y; if (mPath[i].y - mPath[prevNodeId].y &lt; jumpHeight || mMap.IsGround(mPath[i].x, mPath[i].y - 1)) return (GetJumpFrameCount(jumpHeight)); else if (grabLedges &amp;&amp; CanGrabLedge(i)) return (GetJumpFrameCount(jumpHeight) + 4);</pre><p>As you can see, we prolong the jump by <code class="inline">4</code> frames, which should do the job fine in our case.</p><p>But there's one more thing we need to change here, which doesn't really have much to do with ledge grabbing. It fixes a case when the next node is the same height as the current one, but is not on the ground, and the node after that is in higher up, meaning a jump is necessary:</p><pre class="brush: plain noskimlinks noskimwords">if ((mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 || (mPath[currentNodeId].y - mPath[prevNodeId].y == 0 &amp;&amp; !mMap.IsGround(mPath[currentNodeId].x, mPath[currentNodeId].y - 1) &amp;&amp; mPath[currentNodeId+1].y - mPath[prevNodeId].y &gt; 0)) &amp;&amp; (mOnGround || mCurrentState == CharacterState.GrabLedge))</pre><h3>Implement the Movement Logic for Grabbing Onto and Dropping Off Ledges</h3><p>We'll want to split the ledge grabbing logic into two phases: one for when the bot is still not near enough to the ledge to start grabbing, so we simply want to continue movement as usual, and one for when the boy can safely start moving towards it to grab it.</p><p>Let's start by declaring a Boolean which will indicate whether we have already moved to the second phase. We'll name it <code class="inline">mCanGrabLedge</code>:</p><pre class="brush: csharp noskimlinks noskimwords">public bool mGrabsLedges = false; bool mMustGrabLeftLedge; bool mMustGrabRightLedge; bool mCanGrabLedge = false; </pre><p>Now we need to define conditions that will let the character move to the second phase. These are pretty simple:</p><ul> <li>The bot has already reached the goal node on the X axis.</li> <li>The bot needs to grab either the left or right ledge.</li> <li>If the bot moves towards the ledge, it will bump into a wall instead of going further.</li> </ul><p>All right, the first two conditions are very simple to check now because we've done all the work necessary already:</p><pre class="brush: plain noskimlinks noskimwords">if (!mCanGrabLedge &amp;&amp; mReachedNodeX &amp;&amp; (mMustGrabLeftLedge || mMustGrabRightLedge)) { } else if (mReachedNodeX &amp;&amp; mReachedNodeY)</pre><p>Now, the third condition we can separate into two parts. The first one will take care of the situation where the character moves towards the ledge from the bottom, and the second from the top. The conditions we want to set for the first case are:<br></p><ul> <li>The bot's current position is lower than the target position (it's approaching from the bottom).</li> <li>The top of the character's bounding box is higher than the ledge tile height.</li> </ul><pre class="brush: csharp noskimlinks noskimwords">(pathPosition.y &lt; currentDest.y &amp;&amp; (currentDest.y + Map.cTileSize*mHeight) &lt; pathPosition.y + mAABB.HalfSizeY * 2)</pre><p>If the bot is approaching from the top, the conditions are as follows:</p><ul> <li>The bot's current position is higher than the target position (it's approaching from the top).<br> </li> <li>The difference between the character's position and the target position is less than the character's height.</li> </ul><pre class="brush: csharp noskimlinks noskimwords">(pathPosition.y &gt; currentDest.y &amp;&amp; pathPosition.y - currentDest.y &lt; mHeight * Map.cTileSize)</pre><p>Now let's combine all these and set the flag which indicates that we can safely move towards a ledge:</p><pre class="brush: csharp noskimlinks noskimwords"> else if (!mCanGrabLedge &amp;&amp; mReachedNodeX &amp;&amp; (mMustGrabLeftLedge || mMustGrabRightLedge) &amp;&amp; ((pathPosition.y &lt; currentDest.y &amp;&amp; (currentDest.y + Map.cTileSize*mHeight) &lt; pathPosition.y + mAABB.HalfSizeY * 2) || (pathPosition.y &gt; currentDest.y &amp;&amp; pathPosition.y - currentDest.y &lt; mHeight * Map.cTileSize))) { mCanGrabLedge = true; }</pre><p>There's one more thing we want to do here, and that is to immediately start moving towards the ledge:</p><pre class="brush: plain noskimlinks noskimwords">if (!mCanGrabLedge &amp;&amp; mReachedNodeX &amp;&amp; (mMustGrabLeftLedge || mMustGrabRightLedge) &amp;&amp; ((pathPosition.y &lt; currentDest.y &amp;&amp; (currentDest.y + Map.cTileSize*mHeight) &lt; pathPosition.y + mAABB.HalfSizeY * 2) || (pathPosition.y &gt; currentDest.y &amp;&amp; pathPosition.y - currentDest.y &lt; mHeight * Map.cTileSize))) { mCanGrabLedge = true; if (mMustGrabLeftLedge) mInputs[(int)KeyInput.GoLeft] = true; else if (mMustGrabRightLedge) mInputs[(int)KeyInput.GoRight] = true; }</pre><p>OK, now before this huge condition let's create a smaller one. This will basically be a simplified version for the movement when the bot is about to grab a ledge:</p><pre class="brush: plain noskimlinks noskimwords">if (mCanGrabLedge &amp;&amp; mCurrentState != CharacterState.GrabLedge) { if (mMustGrabLeftLedge) mInputs[(int)KeyInput.GoLeft] = true; else if (mMustGrabRightLedge) mInputs[(int)KeyInput.GoRight] = true; } else if (!mCanGrabLedge &amp;&amp; mReachedNodeX &amp;&amp; (mMustGrabLeftLedge || mMustGrabRightLedge) &amp;&amp;</pre><p>That's the main logic behind the ledge grabbing, but there's still a couple of things to do. </p><p>We need to edit the condition in which we check whether it is OK to move to the next node. Currently, the condition looks like this:</p><pre class="brush: plain noskimlinks noskimwords">else if (mReachedNodeX &amp;&amp; mReachedNodeY)</pre><p>Now we need to also move to the next node if the bot was ready to grab the ledge and then actually did so:</p><pre class="brush: csharp noskimlinks noskimwords">else if ((mReachedNodeX &amp;&amp; mReachedNodeY) || (mCanGrabLedge &amp;&amp; mCurrentState == CharacterState.GrabLedge))</pre><h3>Handle Jumping and Dropping From the Ledge</h3><p>Once the bot is on the ledge, it should be able to jump as normal, so let's add an additional condition to the jumping routine:</p><pre class="brush: csharp noskimlinks noskimwords">if (mFramesOfJumping &gt; 0 &amp;&amp; (mCurrentState == CharacterState.GrabLedge || !mOnGround || (mReachedNodeX &amp;&amp; !destOnGround) || (mOnGround &amp;&amp; destOnGround))) { mInputs[(int)KeyInput.Jump] = true; if (!mOnGround) --mFramesOfJumping; }</pre><p>The next thing the bot needs to be able to do is gracefully drop off the ledge. With the current implementation it is very simple: if we're grabbing a ledge and we are not jumping, then obviously we need to drop from it!</p><pre class="brush: csharp noskimlinks noskimwords">if (mCurrentState == Character.CharacterState.GrabLedge &amp;&amp; mFramesOfJumping &lt;= 0) { mInputs[(int)KeyInput.GoDown] = true; }</pre><p>That's it! Now the character is able to very smoothly leave the ledge grab position, no matter whether it needs to jump up or simply drop down.<br></p><h3>Stop grabbing ledges all the time!</h3><p>At the moment, the bot grabs every ledge it can, regardless of whether it makes sense to do so. </p><p>One solution to this is to assign a large heuristic cost to the ledge grabs, so the algorithm prioritises against using them if it doesn't have to—but this would require our bot to have a bit more information about the nodes. Since all we pass to the bot is a list of points, we don't know whether the algorithm meant a particular node to be ledge grabbed or not; the bot assumes that if a ledge can be grabbed, the it surely should! </p><p>We can implement a quick workaround for this behaviour: we will call the pathfinding function <em>twice</em>. The first time we'll call it with the <code class="inline">useLedges</code> parameter set to <code class="inline">false</code>, and the second time with it set to <code class="inline">true</code>.</p><p>Let's assign the first path as the path found without using any ledge grabs:</p><pre class="brush: csharp noskimlinks noskimwords">List&lt;Vector2i&gt; path1 = null; var path = mMap.mPathFinder.FindPath( startTile, destination, Mathf.CeilToInt(mAABB.HalfSizeX / 8.0f), Mathf.CeilToInt(mAABB.HalfSizeY / 8.0f), (short)mMaxJumpHeight, false);</pre><p>Now, if this <code class="inline">path</code> is not null, we need to copy the results to our <code class="inline">path1</code> list, because when we call the pathfinder the second time, the result in the <code class="inline">path</code> will get overwritten.</p><pre class="brush: csharp noskimlinks noskimwords">if (path != null) { path1 = new List&lt;Vector2i&gt;(); path1.AddRange(path); }</pre><p>Now let's call the pathfinder again, this time enabling the ledge grabs:</p><pre class="brush: plain noskimlinks noskimwords">var path2 = mMap.mPathFinder.FindPath( startTile, destination, Mathf.CeilToInt(mAABB.HalfSizeX / 8.0f), Mathf.CeilToInt(mAABB.HalfSizeY / 8.0f), (short)mMaxJumpHeight, true);</pre><p>We'll assume that our final path is going to be the path with ledge grabs:</p><pre class="brush: csharp noskimlinks noskimwords">path = path2; mGrabsLedges = true;</pre><p>And right after this, let's verify our assumption. If we've found a path <em>without</em> ledge grabs, and that path is not much longer than the path using them, then we'll make the bot disable the ledge grabs.</p><pre class="brush: plain noskimlinks noskimwords">if (path1 != null &amp;&amp; path1.Count &lt;= path2.Count + 6) { path = path1; mGrabsLedges = false; }</pre><p>Note that we measure the "length" of the path in node count, which can be quite inaccurate because of the node filtering process. It'd be much more accurate to calculate, for example, the <a href="https://en.wikipedia.org/wiki/Taxicab_geometry" target="_self">Manhattan length of the path</a> (<code class="inline">|x1 - x2| + |y1 - y2|</code> of each node), but since this whole method is more of a hack than a real solution, it's OK to use this kind of heuristic here.</p><p>The rest of the function follows as it was; the path is copied to the bot instance's buffer and it starts following it.</p><h2>Summary</h2><p>That's all for the tutorial! As you can see, it's not so difficult to extend the algorithm to add additional movement possibilities, but doing so definitely increases complexity and adds a few troublesome issues. </p><p>Again, the lack of accuracy can bite us here more than once, especially when it comes to the falling movement—this is the area that needs the most improvement, but I've tried to make the algorithm match the physics as well as I can with the current set of values. </p><p>All in all, the bot can traverse a level in a manner that would rival a lot of players, and I'm very pleased with that result!</p> 2015-12-29T15:00:59.000Z 2015-12-29T15:00:59.000Z Daniel Branicki tag:gamedevelopment.tutsplus.com,2005:PostPresenter/cms-24913 A* Pathfinding for 2D Grid-Based Platformers: Making a Bot Follow the Path <p>In this tutorial, we'll use <a href="https://gamedevelopment.tutsplus.com/series/how-to-adapt-a-pathfinding-to-a-2d-grid-based-platformer--cms-882" target="_self">the platformer pathfinding algorithm we've been building</a> to power a bot that can follow the path by itself; just click on a location and it'll run and jump to get there. This is very useful for NPCs!<br></p><h2>Demo</h2><p>You can <a href="http://tutsplus.github.io/A-Star-Pathfinding-for-Platformers/Demos/DifferentSizes/WebPlayer/index.html" target="_self">play the Unity demo</a>, or <a href="http://tutsplus.github.io/A-Star-Pathfinding-for-Platformers/Demos/FollowBot/WebGL/index.html" target="_self">the WebGL version</a> (100MB+), to see the final result in action. Use <strong>WASD</strong> to move the character, <strong>left-click</strong> on a spot to find a path you can follow to get there, <strong>right-click</strong> a cell to toggle the ground at that point, <strong>middle-click</strong> to place a one-way platform, and <strong>click-and-drag</strong> the sliders to change their values.</p><iframe src="https://tutsplus.github.io/A-Star-Pathfinding-for-Platformers/Demos/Embedded/bot-following-path.html" width="600" height="246" frameborder="0" scrolling="no"></iframe><h2>Updating the Engine</h2><h3>Handling the Bot State</h3><p>The bot has two states defined: the first is for doing nothing, and the second is for handling the movement. In your game, though, you'll probably need many more to change the bot's behaviour according to the situation.</p><pre class="brush: csharp noskimlinks noskimwords">public enum BotState { None = 0, MoveTo, }</pre><p>The bot's update loop will do different things depending on which state is currently assigned to <code class="inline">mCurrentBotState</code>:</p><pre class="brush: plain noskimlinks noskimwords">void BotUpdate() { switch (mCurrentBotState) { case BotState.None: /* no need to do anything */ break; case BotState.MoveTo: /* bot movement update logic */ break; } CharacterUpdate(); }</pre><p>The <code class="inline">CharacterUpdate</code> function handles all the inputs and updates physics for the bot.</p><p>To change the state, we'll use a <code class="inline">ChangeState</code> function which simply assigns the new value to <code class="inline">mCurrentBotState</code>:<br></p><pre class="brush: csharp noskimlinks noskimwords">public void ChangeState(BotState newState) { mCurrentBotState = newState; }</pre><h3>Controlling the Bot</h3><p>We'll control the bot by simulating inputs, which we'll assign to an array of Booleans:</p><pre class="brush: csharp noskimlinks noskimwords">protected bool[] mInputs;</pre><p>This array is indexed by the <code class="inline">KeyInput</code> <code class="inline">enum</code>:</p><pre class="brush: csharp noskimlinks noskimwords">public enum KeyInput { GoLeft = 0, GoRight, GoDown, Jump, Count }</pre><p>For example, if we want to simulate a press of the <strong>left</strong> button, we'll do it like this:</p><pre class="brush: csharp noskimlinks noskimwords">mInputs[(int)KeyInput.GoLeft] = true;</pre><p>The character logic will then handle this artificial input in the same way that it would handle real input. </p><p>We'll also need an additional helper function or a lookup table to get the number of frames we need to press the <strong>jump</strong> button for in order to jump a given number of blocks:</p><pre class="brush: csharp noskimlinks noskimwords">int GetJumpFrameCount(int deltaY) { if (deltaY &lt;= 0) return 0; else { switch (deltaY) { case 1: return 1; case 2: return 2; case 3: return 5; case 4: return 8; case 5: return 14; case 6: return 21; default: return 30; } } }</pre><p>Note that this will only work consistently if our game updates with a fixed frequency and the character's starting jump speed is the same. Ideally, we'd calculate these values separately for each character depending on that character's jump speed, but the above will work fine in our case.</p><h2>Preparing and Obtaining the Path to Follow</h2><h3>Constraining the Goal Location</h3><p>Before we actually use the pathfinder, it'd be a good idea to force the goal destination to be on the ground. This is because the player is quite likely to click a spot that is slightly above the ground, in which case the bot's path would end with an awkward jump into the air. By lowering the end point to be right on the surface of the ground, we can easily avoid this.</p><p>First, let's look at the <code class="inline">TappedOnTile</code> function. This function gets called when the player clicks anywhere in the game; the parameter <code class="inline">mapPos</code> is the position of the tile that the player clicked on:</p><pre class="brush: csharp noskimlinks noskimwords">public void TappedOnTile(Vector2i mapPos) { }</pre><p>We need to lower the position of the clicked tile until it is on the ground:</p><pre class="brush: csharp noskimlinks noskimwords">public void TappedOnTile(Vector2i mapPos) { while (!(mMap.IsGround(mapPos.x, mapPos.y))) --mapPos.y; }</pre><p>Finally, once we arrive at a ground tile, we know where we want to move the character to:</p><pre class="brush: csharp noskimlinks noskimwords">public void TappedOnTile(Vector2i mapPos) { while (!(mMap.IsGround(mapPos.x, mapPos.y))) --mapPos.y; MoveTo(new Vector2i(mapPos.x, mapPos.y + 1)); }</pre><h3>Determining the Starting Location</h3><p>Before we actually call the <code class="inline">FindPath</code> function, we need to make sure that we pass the correct starting cell. </p><p>First, let's assume that the starting tile is the bottom-left cell of a character:</p><pre class="brush: csharp noskimlinks noskimwords">public void MoveTo(Vector2i destination) { Vector2i startTile = mMap.GetMapTileAtPoint(mAABB.Center - mAABB.HalfSize + Vector2.one * Map.cTileSize * 0.5f); }</pre><p>This tile might not be the one we want to pass to the algorithm as the first node, because if our character is standing on the edge of the platform, the <code class="inline">startTile</code> calculated this way may have no ground, as in the following situation:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/24466/image/edge.png"></figure><p>In this case, we'd like to set the starting node to the tile that's on the <em>left side of the character</em>, not on its center.</p><p>Let's start by creating a function that will tell us if the character will fit a different position, and if it does, whether it's on the ground at that spot:</p><pre class="brush: csharp noskimlinks noskimwords">bool IsOnGroundAndFitsPos(Vector2i pos) { }</pre><p>First, let's see if the character fits the spot. If it doesn't, we can immediately return <code class="inline">false</code>:</p><pre class="brush: csharp noskimlinks noskimwords">bool IsOnGroundAndFitsPos(Vector2i pos) { for (int y = pos.y; y &lt; pos.y + mHeight; ++y) { for (int x = pos.x; x &lt; pos.x + mWidth; ++x) { if (mMap.IsObstacle(x, y)) return false; } } }</pre><p>Now we can see whether any of the tiles below the character are ground tiles:</p><pre class="brush: csharp noskimlinks noskimwords">bool IsOnGroundAndFitsPos(Vector2i pos) { for (int y = pos.y; y &lt; pos.y + mHeight; ++y) { for (int x = pos.x; x &lt; pos.x + mWidth; ++x) { if (mMap.IsObstacle(x, y)) return false; } } for (int x = pos.x; x &lt; pos.x + mWidth; ++x) { if (mMap.IsGround(x, pos.y - 1)) return true; } return false; }</pre><p>Let's go back to the <code class="inline">MoveTo</code> function, and see if we have to change the start tile. We need to do that if the character is on the ground but the start tile isn't:</p><pre class="brush: csharp noskimlinks noskimwords">Vector2i startTile = mMap.GetMapTileAtPoint(mAABB.Center - mAABB.HalfSize + Vector2.one * Map.cTileSize * 0.5f); if (mOnGround &amp;&amp; !IsOnGroundAndFitsPos(startTile)) { }</pre><p>We know that, in this case, the character stands on either the left edge or the right edge of the platform. </p><p>Let's first check the right edge; if the character fits there and the tile is on the ground, then we need to move the start tile one space to the right. If it doesn't, then we need to move it to the left.</p><pre class="brush: csharp noskimlinks noskimwords">if (mOnGround &amp;&amp; !IsOnGroundAndFitsPos(startTile)) { if (IsOnGroundAndFitsPos(new Vector2i(startTile.x + 1, startTile.y))) startTile.x += 1; else startTile.x -= 1; }</pre><p>Now we should have all the data we need to call the pathfinder:<br></p><pre class="brush: csharp noskimlinks noskimwords">var path = mMap.mPathFinder.FindPath( startTile, destination, Mathf.CeilToInt(mAABB.HalfSizeX / 8.0f), Mathf.CeilToInt(mAABB.HalfSizeY / 8.0f), (short)mMaxJumpHeight);</pre><p>The first argument is the start tile. </p><p>The second is the destination; we can pass this as-is. </p><p>The third and fourth arguments are the width and the height which need to be approximated by the tile size. Note that here we want to use the ceiling of the height in tiles—so, for example, if the real height of the character is 2.3 tiles, we want the algorithm to think the character is actually 3 tiles high. (It's better if the real height of the character is actually a bit less than its size in tiles, to allow a bit more room for mistakes from the path following AI.) </p><p>Finally, the fifth argument is the maximum jump height of the character.</p><h3>Backing Up the Node List</h3><p>After running the algorithm we should check whether the result is fine—that is, if any path has been found:</p><pre class="brush: csharp noskimlinks noskimwords">if (path != null &amp;&amp; path.Count &gt; 1) { }</pre><p>If so, we need to copy the nodes to a separate buffer, because if some other object were to call the pathfinder's <code class="inline">FindPath</code> function right now, the old result would be overwritten. Copying the result to a separate list will prevent this.</p><pre class="brush: csharp noskimlinks noskimwords">if (path != null &amp;&amp; path.Count &gt; 1) { for (var i = path.Count - 1; i &gt;= 0; --i) mPath.Add(path[i]); }</pre><p>As you can see, we're copying the result in reverse order; this is because the result itself is reversed. Doing this means the nodes in the <code class="inline">mPath</code> list will be in first-to-last order.</p><p>Now let's set the current goal node. Because the first node in the list is the starting point, we can actually skip it and proceed from the second node onwards:</p><pre class="brush: plain noskimlinks noskimwords">if (path != null &amp;&amp; path.Count &gt; 1) { for (var i = path.Count - 1; i &gt;= 0; --i) mPath.Add(path[i]); mCurrentNodeId = 1; ChangeState(BotState.MoveTo); }</pre><p>After setting the current goal node, we set the bot state to <code class="inline">MoveTo</code>, so an appropriate state will be enabled. </p><h2>Getting the Context</h2><p>Before we start writing the rules for the AI movement, we need to be able to find what situation the character is in at any given point. </p><p>We need to know:</p><ul> <li>the positions of the previous, current and next destinations</li> <li>whether the current destination is on the ground or in the air</li> <li>whether the character has reached the current destination on the x-axis</li> <li>whether the character has reached the current destination on the y-axis</li> </ul><p>Note: the destinations here are not necessarily the final goal destination; they're the nodes in the list from the previous section.</p><p>This information will let us accurately determine what the bot should do in any situation.<br></p><p>Let's start by declaring a function to get this context:</p><pre class="brush: csharp noskimlinks noskimwords">public void GetContext(out Vector2 prevDest, out Vector2 currentDest, out Vector2 nextDest, out bool destOnGround, out bool reachedX, out bool reachedY) { }</pre><h3>Calculating World Positions of Destination Nodes</h3><p>The first thing we should do in the function is calculate the world position of the destination nodes. </p><p>Let's start by calculating this for the previous destination. This operation depends on how your game world is set up; in my case, the map coordinates do not match the world coordinates, so we need to translate them. </p><p>Translating them is really simple: we just need to multiply the position of the node by the size of a tile, and then offset the calculated vector by the map position:</p><pre class="brush: csharp noskimlinks noskimwords">prevDest = new Vector2(mPath[mCurrentNodeId - 1].x * Map.cTileSize + mMap.transform.position.x, mPath[mCurrentNodeId - 1].y * Map.cTileSize + mMap.transform.position.y);</pre><p>Note that we start with <code class="inline">mCurrentNodeId</code> equal to <code class="inline">1</code>, so we don't need to worry about accidentally trying to access a node with an index of <code class="inline">-1</code>. </p><p>We'll calculate the current destination's position in the same way:</p><pre class="brush: csharp noskimlinks noskimwords">currentDest = new Vector2(mPath[mCurrentNodeId].x * Map.cTileSize + mMap.transform.position.x, mPath[mCurrentNodeId].y * Map.cTileSize + mMap.transform.position.y);</pre><p>And now for the next destination's position. Here we need to check if there are any nodes left to follow after we reach our current goal, so first let's assume that the next destination is the same as the current one:</p><pre class="brush: csharp noskimlinks noskimwords">nextDest = currentDest;</pre><p>Now, if there <em>are</em> any nodes left, we'll calculate the next destination in the same way that we did the previous two:</p><pre class="brush: csharp noskimlinks noskimwords">if (mPath.Count &gt; mCurrentNodeId + 1) { nextDest = new Vector2(mPath[mCurrentNodeId + 1].x * Map.cTileSize + mMap.transform.position.x, mPath[mCurrentNodeId + 1].y * Map.cTileSize + mMap.transform.position.y); }</pre><h3>Checking Whether the Node is on the Ground</h3><p>The next step is to determine whether the current destination is on the ground. </p><p>Remember that it is not enough to only check the tile directly underneath the goal; we need to consider the cases where the character is more than one block wide:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/24466/image/custom_size_3.png"></figure><p>Let's start by assuming that the destination's position is <em>not</em> on the ground:</p><pre class="brush: csharp noskimlinks noskimwords">destOnGround = false;</pre><p>Now we'll look through the tiles beneath the destination to see if there are any solid blocks there. If there are, we can set <code class="inline">destOnGround</code> to <code class="inline">true</code>:</p><pre class="brush: csharp noskimlinks noskimwords">for (int x = mPath[mCurrentNodeId].x; x &lt; mPath[mCurrentNodeId].x + mWidth; ++x) { if (mMap.IsGround(x, mPath[mCurrentNodeId].y - 1)) { destOnGround = true; break; } }</pre><h3>Checking Whether the Node Has Been Reached on the X-Axis</h3><p>Before we can see if the character has reached the goal, we need to know its position on the path. This position is basically the center of the bottom-left cell of our character. Since our character is not actually built from cells, we are simply going to use the bottom left position of the character's bounding box plus half a cell:</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 pathPosition = mAABB.Center - mAABB.HalfSize + Vector2.one * Map.cTileSize * 0.5f;</pre><p>This is the position that we need to match to the goal nodes.</p><p>How can we determine whether the character has reached the goal on the x-axis? It'd be safe to assume that, if the character is moving right and has an x-position greater than or equal to that of the destination, then the goal has been reached. </p><p>To see if the character was moving right we'll use the previous destination, which in this case must have been to the left of the current one:</p><pre class="brush: csharp noskimlinks noskimwords">reachedX = (prevDest.x &lt;= currentDest.x &amp;&amp; pathPosition.x &gt;= currentDest.x);</pre><p>The same applies to the opposite side; if the previous destination was to the right of the current one and the character's x-position is less than or equal to that of the goal position, then we can be sure that the character has reached the goal on the x-axis:</p><pre class="brush: csharp noskimlinks noskimwords">reachedX = (prevDest.x &lt;= currentDest.x &amp;&amp; pathPosition.x &gt;= currentDest.x) || (prevDest.x &gt;= currentDest.x &amp;&amp; pathPosition.x &lt;= currentDest.x);</pre><h3>Snap the Character's Position</h3><p>Sometimes, because of the character's speed, it overshoots the destination, which may result in it not landing on the target node. See the following example:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/24466/image/overshot.gif"></figure><p>To fix this, we'll snap the character's position so that it lands on the goal node.</p><p>The conditions for us to snap the character are:</p><ul> <li>The goal has been reached on the x-axis.</li> <li>The distance between the bot's position and current destination is greater than <code class="inline">cBotMaxPositionError</code>.</li> <li>The distance between the bot's position and the current destination is not very far, so we don't snap the character from far away.</li> <li>The character did not move left or right last turn, so we snap the character only if it's falling straight down.</li> </ul><pre class="brush: csharp noskimlinks noskimwords">if (reachedX &amp;&amp; Mathf.Abs(pathPosition.x - currentDest.x) &gt; Constants.cBotMaxPositionError &amp;&amp; Mathf.Abs(pathPosition.x - currentDest.x) &lt; Constants.cBotMaxPositionError*3.0f &amp;&amp; !mPrevInputs[(int)KeyInput.GoRight] &amp;&amp; !mPrevInputs[(int)KeyInput.GoLeft]) { pathPosition.x = currentDest.x; mPosition.x = pathPosition.x - Map.cTileSize * 0.5f + mAABB.HalfSizeX + mAABBOffset.x; }</pre><p><code class="inline">cBotMaxPositionError</code> in this tutorial is equal to 1 pixel; this is how far off we let the character be from the destination while still allowing it to go to the next goal.<br></p><h3>Checking Whether the Node Has Been Reached on the Y-Axis</h3><p>Let's figure out when we can be sure that the character has reached its target's Y position. First of all, if the previous destination is below the current one, and our character jumps to the height of the current goal, then we can assume that the goal has been reached.</p><pre class="brush: csharp noskimlinks noskimwords">reachedY = (prevDest.y &lt;= currentDest.y &amp;&amp; pathPosition.y &gt;= currentDest.y);</pre><p>Similarly, if the current destination is below the previous one and the character has reached the y-position of the current node, we can set <code class="inline">reachedY</code> to <code class="inline">true</code> as well.<br></p><pre class="brush: csharp noskimlinks noskimwords">reachedY = (prevDest.y &lt;= currentDest.y &amp;&amp; pathPosition.y &gt;= currentDest.) || (prevDest.y &gt;= currentDest.y &amp;&amp; pathPosition.y &lt;= currentDest.y);</pre><p>Regardless of whether the character needs to be jumping or falling to reach the destination node's y-position, if it's really close, then we should set <code class="inline">reachedY</code> to <code class="inline">true</code> also:</p><pre class="brush: csharp noskimlinks noskimwords">reachedY = (prevDest.y &lt;= currentDest.y &amp;&amp; pathPosition.y &gt;= currentDest.y) || (prevDest.y &gt;= currentDest.y &amp;&amp; pathPosition.y &lt;= currentDest.y) || (Mathf.Abs(pathPosition.y - currentDest.y) &lt;= Constants.cBotMaxPositionError);</pre><p>If the destination is on the ground but the character isn't, then we can assume that the current goal's Y position has not been reached:<br></p><pre class="brush: csharp noskimlinks noskimwords">if (destOnGround &amp;&amp; !mOnGround) reachedY = false;</pre><p>That's it—that's all the basic data we need to know to consider what kind of movement the AI needs to do.</p><h2>Handling the Bot's Movement</h2><p>The first thing to do in our <code class="inline">update</code> function is get the context that we've just implemented:</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 prevDest, currentDest, nextDest; bool destOnGround, reachedY, reachedX; GetContext(out prevDest, out currentDest, out nextDest, out destOnGround, out reachedX, out reachedY);</pre><p>Now let's get the character's current position along the path. We calculate this in the same way we did in the <code class="inline">GetContext</code> function:</p><pre class="brush: csharp noskimlinks noskimwords">Vector2 pathPosition = mAABB.Center - mAABB.HalfSize + Vector2.one * Map.cTileSize * 0.5f;</pre><p>At the beginning of the frame we need to reset the fake inputs, and assign them only if a condition to do so arises. We'll be using only four inputs: two for movement left and right, one for jumping, and one for dropping off a one way platform.</p><pre class="brush: csharp noskimlinks noskimwords">mInputs[(int)KeyInput.GoRight] = false; mInputs[(int)KeyInput.GoLeft] = false; mInputs[(int)KeyInput.Jump] = false; mInputs[(int)KeyInput.GoDown] = false;</pre><p>The very first condition for movement will be this: if the current destination is lower than the position of the character and the character is standing on a one way platform, then press the <strong>down</strong> button, which should result in the character jumping off the platform downwards:</p><pre class="brush: csharp noskimlinks noskimwords">if (pathPosition.y - currentDest.y &gt; Constants.cBotMaxPositionError &amp;&amp; mOnOneWayPlatform) mInputs[(int)KeyInput.GoDown] = true;</pre><h3>Handling Jumps</h3><p>Let's lay out how our jumps should work. First off, we don't want to keep the jump button pressed if <code class="inline">mFramesOfJumping</code> is <code class="inline">0</code>.</p><pre class="brush: csharp noskimlinks noskimwords">if (mFramesOfJumping &gt; 0) { }</pre><p>The second condition to check is that the character is <em>not</em> on the ground. </p><p>In this implementation of platformer physics, the character is allowed to jump if it just stepped off the edge of a platform and is no longer on the ground. This is a popular method to mitigate an illusion that the player has pressed the jump button but the character didn't jump, which might have appeared due to input lag or the player pressing the jump button right after the character has moved off the platform. </p><pre class="brush: csharp noskimlinks noskimwords">if (mFramesOfJumping &gt; 0 &amp;&amp; !mOnGround) { }</pre><p>This condition will work if the character needs to jump off a ledge, because the frames of jumping will be set to an appropriate amount, the character will naturally walk off the ledge, and at that point it will also start the jump. </p><p>This will not work if the jump needs to be performed from the ground; to handle these we need to check these conditions: </p><ul> <li>The character has reached the destination node's x-position, where it's going to start jumping. </li> <li>The destination node is not on the ground; if we are to jump up, we need to go through a node that's in the air first.</li> </ul><pre class="brush: csharp noskimlinks noskimwords">if (mFramesOfJumping &gt; 0 &amp;&amp; (!mOnGround || (reachedX &amp;&amp; !destOnGround))) { }</pre><p>The character should also jump if it is on ground and the destination is on the ground as well. This will generally happen if the character needs to jump one tile up and to the side to reach a platform that's just one block higher.</p><pre class="brush: csharp noskimlinks noskimwords">if (mFramesOfJumping &gt; 0 &amp;&amp; (!mOnGround || (reachedX &amp;&amp; !destOnGround) || (mOnGround &amp;&amp; destOnGround))) { }</pre><p>Now let's activate the jump and decrement the frames of jumping, so that the character holds the jump for the correct number of frames:<br></p><pre class="brush: csharp noskimlinks noskimwords">if (mFramesOfJumping &gt; 0 &amp;&amp; (!mOnGround || (reachedX &amp;&amp; !destOnGround) || (mOnGround &amp;&amp; destOnGround))) { mInputs[(int)KeyInput.Jump] = true; if (!mOnGround) --mFramesOfJumping; }</pre><p>Note that we decrement the <code class="inline">mFramesOfJumping</code> only if the character is not on the ground. This is to avoid accidentally decreasing the jump length before starting the jump.</p><h3>Proceeding to the Next Destination Node</h3><p>Let's think about what needs to happen when we reach the node—that is, when both <code class="inline">reachedX</code> and <code class="inline">reachedY</code> are <code class="inline">true</code>.</p><pre class="brush: csharp noskimlinks noskimwords">if (reachedX &amp;&amp; reachedY) { }</pre><p>First, we'll increment the current node ID:</p><pre class="brush: csharp noskimlinks noskimwords">mCurrentNodeId++;</pre><p>Now we need to check whether this ID is greater than the number of nodes in our path. If it is, that means the character has reached the goal:</p><pre class="brush: csharp noskimlinks noskimwords">if (mCurrentNodeId &gt;= mPath.Count) { mCurrentNodeId = -1; ChangeState(BotState.None); break; }</pre><p>The next thing we must do is calculate the jump for the next node. Since we'll need to use this in more than one place, let's make a function for it:</p><pre class="brush: csharp noskimlinks noskimwords">public int GetJumpFramesForNode(int prevNodeId) { }</pre><p>We only want to jump if the new node is higher than the previous one <em>and</em> the character is on the ground:</p><pre class="brush: csharp noskimlinks noskimwords">public int GetJumpFramesForNode(int prevNodeId) { if (mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 &amp;&amp; mOnGround) { } }</pre><p>To find out how many tiles we'll need to jump, we're going to iterate through nodes for as long as they go higher and higher. When we get to a node that is at a lower height, or a node that has ground under it, we can stop, since we know that there will be no need to go higher than that.</p><p>First, let's declare and set the variable that will hold the value of the jump:</p><pre class="brush: csharp noskimlinks noskimwords">public int GetJumpFramesForNode(int prevNodeId) { if (mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 &amp;&amp; mOnGround) { int jumpHeight = 1; } }</pre><p>Now let's iterate through the nodes, starting at the current node:</p><pre class="brush: csharp noskimlinks noskimwords">public int GetJumpFramesForNode(int prevNodeId) { if (mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 &amp;&amp; mOnGround) { int jumpHeight = 1; for (int i = currentNodeId; i &lt; mPath.Count; ++i) { } } }</pre><p>If the next node is higher than the <code class="inline">jumpHeight</code>, and it's not on the ground, then let's set the new jump height:</p><pre class="brush: csharp noskimlinks noskimwords">public int GetJumpFramesForNode(int prevNodeId) { if (mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 &amp;&amp; mOnGround) { int jumpHeight = 1; for (int i = currentNodeId; i &lt; mPath.Count; ++i) { if (mPath[i].y - mPath[prevNodeId].y &gt;= jumpHeight &amp;&amp; !mMap.IsGround(mPath[i].x, mPath[i].y - 1)) jumpHeight = mPath[i].y - mPath[prevNodeId].y; } } }</pre><p>If the new node height is lower than the previous, or it's on the ground, then we return the number of frames of jump needed for the found height. (And if there's no need to jump, let's just return <code class="inline">0</code>.)</p><pre class="brush: csharp noskimlinks noskimwords">public int GetJumpFramesForNode(int prevNodeId) { int currentNodeId = prevNodeId + 1; if (mPath[currentNodeId].y - mPath[prevNodeId].y &gt; 0 &amp;&amp; mOnGround) { int jumpHeight = 1; for (int i = currentNodeId; i &lt; mPath.Count; ++i) { if (mPath[i].y - mPath[prevNodeId].y &gt;= jumpHeight) jumpHeight = mPath[i].y - mPath[prevNodeId].y; if (mPath[i].y - mPath[prevNodeId].y &lt; jumpHeight || !mMap.IsGround(mPath[i].x, mPath[i].y - 1)) return GetJumpFrameCount(jumpHeight); } } return 0; }</pre><p>We need to call this function in two places.</p><p>The first one is in the case where the character has reached the node's x- and y-positions:</p><pre class="brush: csharp noskimlinks noskimwords">if (reachedX &amp;&amp; reachedY) { int prevNodeId = mCurrentNodeId; mCurrentNodeId++; if (mCurrentNodeId &gt;= mPath.Count) { mCurrentNodeId = -1; ChangeState(BotState.None); break; } if (mOnGround) mFramesOfJumping = GetJumpFramesForNode(prevNodeId); }</pre><p>Note that we set the jump frames for the whole jump, so when we reach an in-air node we don't want to change the number of jump frames that was determined <em>before</em> the jump took place. </p><p>After we update the goal, we need to process everything again, so the next movement frame gets calculated immediately. For this, we'll use a <code class="inline">goto</code> command:</p><pre class="brush: csharp noskimlinks noskimwords">goto case BotState.MoveTo;</pre><p>The second place we need to calculate the jump for is the <code class="inline">MoveTo</code> function, because it might be the case that the first node of the path is a jump node:</p><pre class="brush: csharp noskimlinks noskimwords">if (path != null &amp;&amp; path.Count &gt; 1) { for (var i = path.Count - 1; i &gt;= 0; --i) mPath.Add(path[i]); mCurrentNodeId = 1; ChangeState(BotState.MoveTo); mFramesOfJumping = GetJumpFramesForNode(0); }</pre><h3>Handling Movement to Reach the Node's X-Position</h3><p>Now let's handle the movement for the case where the character has not yet reached the target node's x-position. </p><p>Nothing complicated here; if the destination is to the right, we need to simulate the <strong>right</strong> button press. If the destination is to the left, then we need to simulate the <strong>left</strong> button press. We only need to move the character if the difference in position is more than the <code class="inline">cBotMaxPositionError</code> constant:</p><pre class="brush: csharp noskimlinks noskimwords">else if (!reachedX) { if (currentDest.x - pathPosition.x &gt; Constants.cBotMaxPositionError) mInputs[(int)KeyInput.GoRight] = true; else if (pathPosition.x - currentDest.x &gt; Constants.cBotMaxPositionError) mInputs[(int)KeyInput.GoLeft] = true; }</pre><h3>Handling Movement to Reach the Node's Y-Position</h3><p>If the character has reached the target x-position but we still it to jump higher, we can still move the character left or right depending on where the next goal is. This will just mean that the character does not stick so rigidly to the found path. Thanks to that, it'll be much easier to get to the next destination, because instead of simply waiting to reach the target y-position, the character will be naturally moving towards the next node's x-position while it's doing so.<br></p><p>We'll only move the character towards the next destination if it exists at all and it's not on the ground. (If it's on the ground, then we can't skip it because it's an important checkpoint—it resets the character's vertical speed and allows it to use the jump again.)</p><pre class="brush: csharp noskimlinks noskimwords">else if (!reachedY &amp;&amp; mPath.Count &gt; mCurrentNodeId + 1 &amp;&amp; !destOnGround) { }</pre><p>But before we actually move towards the next goal, we need to check that we won't break the path by doing so.</p><h3>Avoiding Breaking a Fall Prematurely</h3><p>Consider the following scenario:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/24466/image/wrong_tunnel.gif"></figure><p>Here, as soon as the character walked off the ledge where it started, it reached the x-position of the second node, and was falling to reach the y-position. Since the third node was to the right of the character, it moved right—and ended up in a tunnel <em>above</em> the one we wanted it to go into.</p><p>To fix this, we need to check whether there are any obstacles between the character and the next destination; if there aren't, then we are free to move the character towards it; if there are, then we need to wait.<br></p><p>First, let's see which tiles we'll need to check. If the next goal is to the right of the current one, then we'll need to check the tiles on the right; if it's to the left then we'll need to check the tiles to the left. If they are at the same x-position, there's no reason to make any pre-emptive movements.</p><pre class="brush: csharp noskimlinks noskimwords">int checkedX = 0; int tileX, tileY; mMap.GetMapTileAtPoint(pathPosition, out tileX, out tileY); if (mPath[mCurrentNodeId + 1].x != mPath[mCurrentNodeId].x) { if (mPath[mCurrentNodeId + 1].x &gt; mPath[mCurrentNodeId].x) checkedX = tileX + mWidth; else checkedX = tileX - 1; }</pre><p>As you can see, the x-coordinate of the node to the right depends on the width of the character.</p><p>Now we can check whether there are any tiles between the character and the next node's position on the y-axis:</p><pre class="brush: csharp noskimlinks noskimwords">if (checkedX != 0 &amp;&amp; !mMap.AnySolidBlockInStripe(checkedX, tileY, mPath[mCurrentNodeId + 1].y)) { }</pre><p>The <code class="inline">AnySolidBlockInStripe</code> function checks whether there are any solid tiles between two given points on the map. The points need to have the same x-coordinate. The x-coordinate we are checking is the tile we'd like the character to move into, but we're not sure if we can, as explained above. </p><p>Here's the implementation of the function.</p><pre class="brush: csharp noskimlinks noskimwords">public bool AnySolidBlockInStripe(int x, int y0, int y1) { int startY, endY; if (y0 &lt;= y1) { startY = y0; endY = y1; } else { startY = y1; endY = y0; } for (int y = startY; y &lt;= endY; ++y) { if (GetTile(x, y) == TileType.Block) return true; } return false; }</pre><p>As you can see, the function is really simple; it just iterates through the tiles in a column, starting from the lower one.</p><p>Now that we know we can move towards the next destination, let's do so:</p><pre class="brush: csharp noskimlinks noskimwords">if (checkedX != 0 &amp;&amp; !mMap.AnySolidBlockInStripe(checkedX, tileY, mPath[mCurrentNodeId + 1].y)) { if (nextDest.x - pathPosition.x &gt; Constants.cBotMaxPositionError) mInputs[(int)KeyInput.GoRight] = true; else if (pathPosition.x - nextDest.x &gt; Constants.cBotMaxPositionError) mInputs[(int)KeyInput.GoLeft] = true; }</pre><h3>Allowing the Bot to Skip Nodes</h3><p>That's almost it—but there's still one case to solve. Here's an example:</p><figure class="post_image"><img alt="" data-src="https://cms-assets.tutsplus.com/uploads/users/881/posts/24466/image/reachedGoal.gif"></figure><p>As you can see, before the character reached the second node's y-position, it bumped its head on the floating tile, because we made it move towards the next destination to the right. As a result, the character ends up never reaching the second node's y-position; instead it moved straight on to the third node. Since <code class="inline">reachedY</code> is <code class="inline">false</code> in this case, it cannot proceed with the path. </p><p>To avoid such cases, we'll simply check whether the character reached the next goal before it reached the current one.</p><p>The first move towards this will be separating our previous calculations of <code class="inline">reachedX</code> and <code class="inline">reachedY</code> into their own functions:</p><pre class="brush: csharp noskimlinks noskimwords">public bool ReachedNodeOnXAxis(Vector2 pathPosition, Vector2 prevDest, Vector2 currentDest) { return (prevDest.x &lt;= currentDest.x &amp;&amp; pathPosition.x &gt;= currentDest.x) || (prevDest.x &gt;= currentDest.x &amp;&amp; pathPosition.x &lt;= currentDest.x) || Mathf.Abs(pathPosition.x - currentDest.x) &lt;= Constants.cBotMaxPositionError; } public bool ReachedNodeOnYAxis(Vector2 pathPosition, Vector2 prevDest, Vector2 currentDest) { return (prevDest.y &lt;= currentDest.y &amp;&amp; pathPosition.y &gt;= currentDest.y) || (prevDest.y &gt;= currentDest.y &amp;&amp; pathPosition.y &lt;= currentDest.y) || (Mathf.Abs(pathPosition.y - currentDest.y) &lt;= Constants.cBotMaxPositionError); }</pre><p>Next, replace the calculations with the function call in the <code class="inline">GetContext</code> function:</p><pre class="brush: csharp noskimlinks noskimwords">reachedX = ReachedNodeOnXAxis(pathPosition, prevDest, currentDest); reachedY = ReachedNodeOnYAxis(pathPosition, prevDest, currentDest);</pre><p>Now we can check whether the next destination has been reached. If it has, we can simply increment <code class="inline">mCurrentNode</code> and immediately re-do the state update. This will make the next destination become the current one, and since the character has reached it already, we will be able to move on:</p><pre class="brush: csharp noskimlinks noskimwords">if (checkedX != 0 &amp;&amp; !mMap.AnySolidBlockInStripe(checkedX, tileY, mPath[mCurrentNodeId + 1].y)) { if (nextDest.x - pathPosition.x &gt; Constants.cBotMaxPositionError) mInputs[(int)KeyInput.GoRight] = true; else if (pathPosition.x - nextDest.x &gt; Constants.cBotMaxPositionError) mInputs[(int)KeyInput.GoLeft] = true; if (ReachedNodeOnXAxis(pathPosition, currentDest, nextDest) &amp;&amp; ReachedNodeOnYAxis(pathPosition, currentDest, nextDest)) { mCurrentNodeId += 1; goto case BotState.MoveTo; } }</pre><p>That's all for character movement!</p><h2>Handling Restart Conditions</h2><p>It's good to have a backup plan for a situation in which the bot is not moving through the path like it should. This can happen if, for example, the map gets changed—adding an obstacle to an already calculated path may cause the path to become invalid. What we'll do is reset the path if the character is stuck for longer than a particular number of frames.</p><p>So, let's declare variables that will count how many frames the character has been stuck and how many frames it may be stuck at most:</p><pre class="brush: csharp noskimlinks noskimwords">public int mStuckFrames = 0; public const int cMaxStuckFrames = 20;</pre><p>We need to reset this when we call <code class="inline">MoveTo</code> function:</p><pre class="brush: csharp noskimlinks noskimwords">public void MoveTo(Vector2i destination) { mStuckFrames = 0; /* ... */ }</pre><p>And finally, at the end of the <code class="inline">BotState.MoveTo</code>, let's check whether the character is stuck. Here, we simply need to check if its current position is equal to the old one; if so, then we also need to increment the <code class="inline">mStuckFrames</code> and check whether the character has been stuck for more frames than <code class="inline">cMaxStuckFrames</code>—and if it was, then we need to call the <code class="inline">MoveTo</code> function with the last node of the current path as the parameter. Of course, if the position is different, then we need to reset the <code class="inline">mStuckFrames</code> to 0:</p><pre class="brush: csharp noskimlinks noskimwords">if (mFramesOfJumping &gt; 0 &amp;&amp; (!mOnGround || (reachedX &amp;&amp; !destOnGround) || (mOnGround &amp;&amp; destOnGround))) { mInputs[(int)KeyInput.Jump] = true; if (!mOnGround) --mFramesOfJumping; } if (mPosition == mOldPosition) { ++mStuckFrames; if (mStuckFrames &gt; cMaxStuckFrames) MoveTo(mPath[mPath.Count - 1]); } else mStuckFrames = 0;</pre><p>Now the character should find an alternative path if it wasn't able to finish the initial one.</p><h2>Conclusion</h2><p>That's the whole of the tutorial! It's been a lot of work, but I hope you'll find this method useful. It is by no means a perfect solution for platformer pathfinding; the approximation of the jump curve for the character that the algorithm needs to make is often quite tricky to do and can lead to incorrect behaviour. The algorithm still can be extended—it's not very hard to add ledge-grabs and other kinds of extended movement flexibility—but we've covered the basic platformer mechanics. It is also possible to optimize the code to make it faster as well as use less memory; this iteration of the algorithm isn't perfect at all when it comes to those aspects. It also suffers from quite poor approximation of the curve when falling at large speeds.</p><p>The algorithm can be used in many ways, most notably to enhance the enemy AI or AI companions. It can also be used as a control scheme for touch devices—this would work basically the same way it does in the tutorial demo, with the player tapping wherever they want the character to move. This removes the execution challenge upon which many platformers have been built, so the game would have to be designed differently, to be much more about positioning your character in the right spot rather than learning to control the character accurately.</p><p>Thanks for reading! Be sure to leave some feedback on the method and also let me know if you've made any improvements to it!</p> 2015-09-29T14:30:20.000Z 2015-09-29T14:30:20.000Z Daniel Branicki