{"id":1628,"date":"2021-12-15T16:05:51","date_gmt":"2021-12-16T00:05:51","guid":{"rendered":"http:\/\/www.adamantyr.com\/blog\/?p=1628"},"modified":"2021-12-15T16:05:51","modified_gmt":"2021-12-16T00:05:51","slug":"making-a-crpg-part-2-maps-scrolling-and-line-of-sight","status":"publish","type":"post","link":"http:\/\/www.adamantyr.com\/index.php\/2021\/12\/15\/making-a-crpg-part-2-maps-scrolling-and-line-of-sight\/","title":{"rendered":"Making a CRPG Part 2 &#8211; Maps, Scrolling and Line of Sight"},"content":{"rendered":"<p>In this, part 2, we will be looking at scrolling maps, a pivotal element of top-down 2D computer role play games of yore.<\/p>\n<p>I think something that is lost on many modern gamers, who didn&#8217;t grow up in the 80&#8217;s. The majority of games on the early 8-bit systems were limited to a single screen of play. Really good games may have multiple screens but when you moved off the edge it would load a new screen.<\/p>\n<div id=\"attachment_610\" style=\"width: 310px\" class=\"wp-caption alignright\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-610\" class=\"wp-image-610 size-medium\" src=\"http:\/\/test.adamantyr.com\/wp-content\/uploads\/2017\/09\/hqdefault-300x225.jpg\" alt=\"\" width=\"300\" height=\"225\" srcset=\"http:\/\/www.adamantyr.com\/wp-content\/uploads\/2017\/09\/hqdefault-300x225.jpg 300w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2017\/09\/hqdefault.jpg 480w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><p id=\"caption-attachment-610\" class=\"wp-caption-text\">It&#8217;s bigger on the inside!<\/p><\/div>\n<p>So it was utterly FANTASTIC to see a game screen that was a view-port on a much larger world. When I first saw Ultima II, I was in total shock. There was (to my viewpoint) no limit to what could be beyond the borders of the screen! It both was thrilling and increased my curiosity and expectations of what the game could have.<\/p>\n<p>A scrolling map was also well beyond what most BASIC languages could do in those days. Some were better than others at high-speed video display, but anything close to full screen was a challenge no matter the platform. So knowledge of assembly language was needed to implement one.<\/p>\n<p>So how to render a map that is bigger than the screen? Let&#8217;s dive in&#8230;<\/p>\n<h2>Storing Maps<\/h2>\n<p>The first thing to figure out how you are encoding your maps internally. How much memory are you devoting internally to a map? Is the data compressed on disk and must be uncompressed when loaded? How many unique tiles can be present on a map? Are tiles global or specified for the map?<\/p>\n<p>Early Ultima&#8217;s like II and III had 64&#215;64 size maps. Both had less than a byte&#8217;s worth of tiles (64 and 128) so they would use up 4K of RAM to store the map, uncompressed and using a full byte per tile. Ultima IV uses 32&#215;32 size maps and some clever coding to load a continuous world map. As you walk around, new 32&#215;32 chunks are loaded. This creates some challenging edge cases (literally) when you approach corners and the game will need to load up to 3 new chunks to get the data needed. Ultima V goes to 16&#215;16 chunks for the world map.<\/p>\n<p>The issue with doing continuous map loads on early 8-bit systems is most of them aren&#8217;t that efficient with loading data continuously. Until hard drives came along, loading even a few kilobytes from floppy disk could take a second or so. And it got worse on systems like the Commodore 64 where the continuous music would suddenly get stuck on a single note as it loaded fresh data. My own experiments with a &#8220;big map&#8221; showed the problem, as every time I approached an edge it would take seconds to load fresh data.<\/p>\n<p>So I decided for my own maps to just have singleton maps that are a maximum of 4K in size, and if you left them you&#8217;d just load a new one as a self-contained area. For tiles, I have 128, and multiple character sets. So for a world map, there is a &#8220;world&#8221; tile set that includes mountains and other features only found on world maps. The leftover bit is used as a lighting mechanic; it indicates if this tile is &#8220;lit&#8221; or not naturally on the map.<\/p>\n<p>The nice thing about a map buffer is it can take any shape you want. Just because I have 4096 tiles doesn&#8217;t mean it&#8217;s automatically 64&#215;64. By specifying a height and width parameter for each map, I can have them in many different sizes. In practice I found that 32&#215;32 was a decent size for most towns and dungeons. 48&#215;48 was nearly perfect, just big enough to have a lot of interesting areas. 64&#215;64 was almost TOO big, there was a few cases where I split maps into multiple maps because a super large map wasn&#8217;t actually ideal, especially if they had a lot of mobs (mobile objects) on them.<\/p>\n<p>Some game maps are compressed on disk so they take less room. With a lot of older CRPG&#8217;s this makes good sense; you don&#8217;t have a lot of unique tiles and you tend to have long horizontal stripes of them, which screams &#8220;compress me!&#8221; The most common form of compression is RLE, or run-length-encoding. RLE defines the data as either a singleton (one tile) or a count of whatever comes next of values to repeat. There&#8217;s usually also a terminator value as well to indicate map processing should end.<\/p>\n<p>For example, if you have 64 unique tiles, the top two bits could be used to indicate a few different ideas:<\/p>\n<ol>\n<li>The top two bits are control bits. If both are 0, it&#8217;s a terminator for the map data. If 01, this is a singleton tile. If 10, it&#8217;s two consecutive tiles (or whatever the most common count of consecutive tiles is in your maps.) If 11, use the tile value as a count and that&#8217;s how many of the next tile to produce.<\/li>\n<li>The top two bits are count bits. Value 0 means it&#8217;s a terminator for the map data. Otherwise produce 1-3 of the given tile.<\/li>\n<\/ol>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignright size-full wp-image-1629\" src=\"http:\/\/test.adamantyr.com\/wp-content\/uploads\/2021\/12\/rle-example.png\" alt=\"\" width=\"245\" height=\"229\" \/>Let&#8217;s look at an example, here is a 7&#215;7 map, showing an island. Uncompressed, if you used a full byte per tile, it would take up 49 bytes.<\/p>\n<p>With method #1 above, it would take two bytes for any length of greater than 2 to store. Crunching the numbers, it would take 25 bytes (including a terminator byte) to store. Almost 50% compression!<\/p>\n<p>With method #2 above, while we are limited to a maximum of 3 repeated tiles, it actually comes in at 21 bytes, over 50% compression. Nice! And the algorithm to decompress is a little simpler as well.<\/p>\n<p>Of course, this example only has two tile types, and the map is rather straight-forward. Any CRPG map is likely to have a lot richer of a data set. And there is a point where RLE won&#8217;t be as effective. My own maps utilize a lot of &#8220;two pair&#8221; tiles where they are the same type (grass, for instance) but are in fact different tiles. This breaks RLE, which expects a lot of the same tiles to be repeated in a horizontal line. For such maps, using a more complex pattern-oriented compression technique like LZW and Huffman makes more sense.<\/p>\n<p>The main value of compression is to reduce disk size, though. Is disk size really a problem to solve? In the old days, the answer was unhesitatingly yes. Most 8-bit systems had 160-180K disks, and every disk was an expense to replicate and put in a box. Data compression saved money. In the modern era, though, with digital distribution and USB sticks that hold more memory than the entire production run of a computer system&#8217;s RAM added together, it&#8217;s not as big of a deal. Even retro enthusiasts these days tend towards modern storage solutions like emulated disk systems or even cartridges with megabytes of space available. So I figured, why bother?<\/p>\n<p>Towards the end of my development work, I did consider that it would be better to have dynamic tiles. In other words, store a key list of the unique tiles used on the map, assign them unique values, then the map data itself using those values. It&#8217;s a nice idea, as each map then basically has it&#8217;s own unique tile set, and you wouldn&#8217;t have a lot of needless replication. But it adds a lot of overhead towards map loading, and would require a complicated editor for maps. Something to consider for the future&#8230;<\/p>\n<h2>Viewing Maps<\/h2>\n<p>Okay, so you have your map loaded in memory. How to get it on screen?<\/p>\n<p>Well, the first thing is to determine the number of tiles you want to appear on the screen. Depending on how many pixels per tile, how large the screen, etc. And is your &#8220;avatar&#8221; character always at the center? If so, an odd number of tiles per side makes sense for balance.<\/p>\n<p>Using a &#8216;view-port buffer&#8217; is a best approach. Don&#8217;t try and pull each tile individually from your map data, use an in-between buffer to store it. Using map height and width and a position offset, it&#8217;s not hard to create a double loop to copy out map data to your view-port buffer. But what do you do when your view-port goes over the edge of the map?<\/p>\n<p>Different games handle it different ways. If you want your map to just stop scrolling at edges, Gauntlet-style, that&#8217;s easy to do; you just make sure offsets never go past a certain value. This does create the sense of reaching a &#8220;map edge&#8221; though, and may remove the player a little from immersion.<\/p>\n<p>Other games just have a default &#8220;overflow&#8221; tile that is used, and interacting with it will pop the character off the map. This also works but still clearly illustrates that you&#8217;ve reached a map edge.<\/p>\n<p>For my own game, I had two versions. One is &#8220;repeat the edge tile&#8221; which takes whatever tile was along that edge and repeats it indefinitely. This creates a less obvious map edge. The other version is &#8220;wrap-around&#8221;, which creates a continuous map by wrapping to the other side. I use this one sparingly, usually for maps that are either &#8220;warped magic&#8221; in nature or in a more clever fashion to create non-square style caverns.<\/p>\n<div id=\"attachment_1022\" style=\"width: 310px\" class=\"wp-caption alignleft\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-1022\" class=\"size-medium wp-image-1022\" src=\"http:\/\/test.adamantyr.com\/wp-content\/uploads\/2019\/01\/02-Craggy-Fjords-300x175.png\" alt=\"\" width=\"300\" height=\"175\" srcset=\"http:\/\/www.adamantyr.com\/wp-content\/uploads\/2019\/01\/02-Craggy-Fjords-300x175.png 300w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2019\/01\/02-Craggy-Fjords-768x449.png 768w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2019\/01\/02-Craggy-Fjords.png 904w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><p id=\"caption-attachment-1022\" class=\"wp-caption-text\">No wasted space!<\/p><\/div>\n<p>One other feature I introduced with my own map system is slanted maps. Since map projection is up for interpretation, I can slant map data by row to make a more natural style map for coast lines that aren&#8217;t going in cardinal directions. Very useful, and very difficult to debug!<\/p>\n<p>One additional task is to place mobs (mobile objects) on your map. This would be your monsters, points of interest, and other items that aren&#8217;t part of the static map. I store these in a separate data set, so they must be placed in the view-port area if they are visible. This puts a practical limit of how many mobs per map, since every extra calculation is costing you processing time.<\/p>\n<p>So now we have our view-port, with our set of tiles. Now (finally) it&#8217;s time for line of sight!<\/p>\n<h2>Line Of Sight<\/h2>\n<p>Back in the early 90&#8217;s I tried to create the LOS algorithm in BASIC, using sine and cosine functions. I thought of it as I was flinging light out from the center and traversing a circle, and that if I struck a barrier that blocked line of sight, everything after it on the path would be dark.<\/p>\n<p>So&#8230; it kind of worked. Except that even on a 11&#215;11 size screen and going 1 degree at a time, it still didn&#8217;t cover all the squares. Plus given it was in BASIC it ran VERY slowly, taking ten minutes to complete.<\/p>\n<p>Years later, I got a hold of the Ultima III LOS algorithm, and was able to see my error. I was thinking in reverse! What you do is trace a path from the tile you want to check LOS on and traverse back to the center.<\/p>\n<div id=\"attachment_1634\" style=\"width: 310px\" class=\"wp-caption alignright\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-1634\" class=\"size-medium wp-image-1634\" src=\"http:\/\/test.adamantyr.com\/wp-content\/uploads\/2021\/12\/los1-300x300.png\" alt=\"\" width=\"300\" height=\"300\" srcset=\"http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los1-300x300.png 300w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los1-150x150.png 150w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los1.png 450w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><p id=\"caption-attachment-1634\" class=\"wp-caption-text\">Diagonal then Cardinal<\/p><\/div>\n<p>The algorithm is pretty simple:<\/p>\n<ul>\n<li>Hide all tiles in the view-port, except the center tile, where your avatar is.<\/li>\n<li>For each tile in the view-port, plot a path back to center. The original algorithm uses two arrays to achieve an offset. It moves diagonally towards the center until it hits a cardinal direction, then continues the rest of the way on the cardinal.<\/li>\n<li>If at any point you encounter a &#8220;blocking&#8221; tile, move on to the next tile.<\/li>\n<li>Otherwise, if you reach center, uncover the target tile and move to the next tile.<\/li>\n<\/ul>\n<p>Simple indeed, but processor-intensive. Besides processing the view port in start-to-end order without taking the 2D nature of the data into account, it also ends up reprocessing a LOT of tiles unnecessarily. If a tile is blocked, then wouldn&#8217;t any tiles between it and the blocking tile also be blocked? And if so, why calculate those at all?<\/p>\n<p>I introduced some optimizations to reduce unnecessary calculations and it worked pretty well. However, the LOS algorithm in Ultima III was rather heavy; a single tile creates a huge swath of diagonal shadow behind it. I noticed that Ultima IV on the PC seemed to have a better algorithm so I took a copy of the source from XU4 (sadly now an extinct project) to analyze.<\/p>\n<div id=\"attachment_1635\" style=\"width: 310px\" class=\"wp-caption alignleft\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-1635\" class=\"size-medium wp-image-1635\" src=\"http:\/\/test.adamantyr.com\/wp-content\/uploads\/2021\/12\/los2-300x300.png\" alt=\"\" width=\"300\" height=\"300\" srcset=\"http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los2-300x300.png 300w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los2-150x150.png 150w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los2.png 450w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><p id=\"caption-attachment-1635\" class=\"wp-caption-text\">Cardinal then Diagonal<\/p><\/div>\n<p>The algorithm is VERY different. It starts by tracing in the cardinal directions from the center outward. If it encounters a blocking tile, it blocks only the cardinal tiles behind it.<\/p>\n<p>After the four cardinal plots, it then does four quadrant calculations, which start on the edge and go towards the center on a horizontal or vertical direction first then diagonal. This has the effect of not blocking so pervasively. It&#8217;s a much more efficient algorithm because it&#8217;s actually taking the structure of the view-port into consideration.<\/p>\n<h2>Light<\/h2>\n<div id=\"attachment_1636\" style=\"width: 310px\" class=\"wp-caption alignright\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-1636\" class=\"size-medium wp-image-1636\" src=\"http:\/\/test.adamantyr.com\/wp-content\/uploads\/2021\/12\/los-light-300x300.png\" alt=\"\" width=\"300\" height=\"300\" srcset=\"http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los-light-300x300.png 300w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los-light-150x150.png 150w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2021\/12\/los-light.png 450w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><p id=\"caption-attachment-1636\" class=\"wp-caption-text\">Light Map<\/p><\/div>\n<p>So, remember that light bit on the tiles? That&#8217;s used to determine dark and light areas. But, if the player has a light source going, how to determine if a square is lit or not?<\/p>\n<p>I use a light map, which has concentric circles of numbers, matching the size of the view port. The center area is value 0, and slowly increases as it goes outward. A light source has a strength (or radius) value, which the light map is subtracted from. If the value is less than zero, it&#8217;s not lit. If it&#8217;s equal or greater, it&#8217;s lit.<\/p>\n<p>The light map is always applied after LOS has been calculated. A square that&#8217;s already blocked remains blocked.<\/p>\n<h2>Elevation<\/h2>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignleft size-medium wp-image-600\" src=\"http:\/\/test.adamantyr.com\/wp-content\/uploads\/2017\/09\/WP_20130330_002-300x169.jpg\" alt=\"\" width=\"300\" height=\"169\" srcset=\"http:\/\/www.adamantyr.com\/wp-content\/uploads\/2017\/09\/WP_20130330_002-300x169.jpg 300w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2017\/09\/WP_20130330_002-768x432.jpg 768w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2017\/09\/WP_20130330_002-1024x576.jpg 1024w, http:\/\/www.adamantyr.com\/wp-content\/uploads\/2017\/09\/WP_20130330_002.jpg 1632w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/>This was a late-development feature, which came about when I was out on a ridge one day looking over a magnificent set of waterfalls in the distance. I realized that despite being in a forest and having a deep valley of forest between us, I could still see the falls clearly. And that got me thinking, what about elevation and LOS?<\/p>\n<p>The elevation map is a separate data set from the map&#8217;s data, as not every map uses elevation. The data is also stored in a compressed format (run-length encoding!) so it doesn&#8217;t take up much disk space. There are four levels of elevation, from 0 to 3.<\/p>\n<p>The math is easy. Whatever elevation your avatar is at, everything below it is not a blocking tile. Everything at same level respects the LOS calculations. And everything above your level is considered a blocking tile automatically.<\/p>\n<p>I mostly use elevation on world maps, but a few special maps use it. I think my favorite in this regard is a sewer dungeon, with both upper and lower levels. The biggest problem to solve with elevation was coming up with clear boundary delineation.<\/p>\n<h2>The Code<\/h2>\n<p>Below is ROA&#8217;s map view algorithm, in TMS9900 assembly. It uses multiple buffer maps for each stage and then combines them at the end for the finished mapview.<\/p>\n<pre>* Map Building Routine\r\n* Extract map from map buffer into VMAP\r\nBLDMAP MOV  R11,*R10+\r\n       LI   R0,MOBVIS\r\n       LI   R1,32\r\nBLDMP0 CLR  *R0+                       * Clear mob visibility array\r\n       DEC  R1\r\n       JNE  BLDMP0\r\n       LI   R3,2\r\n       BLWP @PAGE1                     * Set &gt;2000 to page 2 (map buffer)\r\n       LI   R3,1\r\n       BLWP @PAGE2                     * Set &gt;3000 to page 1 (elevation)\r\n       MOVB @SLANT,R1                  * Get orientation into R1\r\n       SRL  R1,8\r\n       MOV  @DIRY(R1),R9               * Set R9 to -1 (left), 0 (none), or 1 (right)\r\n       MOV  @DSLANT(R1),R8             * Set R8 to 0 (left), -6 (none), -12 (right)\r\n       MOV  @Y,R1                      * Get Y value into R1\r\n       MOV  @X,R2                      * Get X value into R2\r\n       AI   R1,-6                      * Set starting y position\r\n       A    R8,R2                      * Add slant start to x position       \r\n       LI   R3,13                      * Row count\r\n       LI   R4,13                      * Column count\r\n       CLR  R5                         * buffer index\r\n       MOVB @EDGES,@EDGES              * Check if edge or repeating map\r\n       JNE  BLDMPW\r\n* Build map with edge\r\nBLDMPE MOV  R1,R6                      * Copy R1 to R6\r\n       MOV  R2,R7                      * Copy R2 to R7\r\n       BL   @EDGCHK                    * Check if over edge\r\n       MOV  R0,R0\r\n       JEQ  BLDME1\r\n* Edge correction\r\n       COC  @W1,R0                     * Vertical?\r\n       JNE  EDGEM2\r\n       MOV  R6,R6\r\n       JLT  EDGEM1\r\n       MOV  @VWIDTH,R6\r\n       DEC  R6\r\n       JMP  EDGEM2\r\nEDGEM1 CLR  R6\r\nEDGEM2 COC  @W2,R0                     * Horizontal?\r\n       JNE  BLDME1\r\n       MOV  R7,R7\r\n       JLT  EDGEM3\r\n       MOV  @HWIDTH,R7\r\n       DEC  R7\r\n       JMP  BLDME1\r\nEDGEM3 CLR  R7\r\n* Get tile\r\nBLDME1 MOV  R7,R0                      * Copy R7 to R0\r\n       MPY  @HWIDTH,R6                 * Multiply Y by hortz width\r\n       A    R0,R7                      * Calculate map index into R7\r\n       MOVB @MAPBUF(R7),@VMAP(R5)      * Copy tile\r\n       MOVB @MAPENV(R7),@EMAP(R5)      * Copy elevation level\r\n       MOVB @VMAP(R5),@LMAP(R5)        * Copy light level\r\n       SOCB @B128,@VMAP(R5)            * Set high bit on active tile\r\n       SZCB @B127,@LMAP(R5)            * Filter light array to top bit only\r\n       MOVB @B1,@LOSMAP(R5)            * Set LOS map to blocked\r\n       INC  R5                         * Increment the buffer pointer\r\n       INC  R2                         * Increment the column index\r\n       DEC  R4                         * Decrement the window width count\r\n       JNE  BLDMPE\r\n       INC  R1                         * Increment the row index\r\n       A    R9,R8                      * Add slant change to R8\r\n       MOV  @X,R2                      * Move X back into R2\r\n       A    R8,R2                      * Add slant to x position\r\n       LI   R4,13                      * Reset the window width to 13\r\n       DEC  R3                         * Decrement the window height count\r\n       JNE  BLDMPE\r\n       JMP  BLDMP2                     * Jump to next routine\r\n* Build map with wrapping\r\nBLDMPW MOV  R1,R6                      * Copy R1 to R6\r\n       MOV  R2,R7                      * Copy R2 to R7\r\n       BL   @EDGCHK                    * Check if over edge\r\n       MOV  R0,R0\r\n       JEQ  BLDMW1\r\n* Wrap correction\r\n       COC  @W1,R0                     * Vertical?\r\n       JNE  WRAPM2\r\n       MOV  R6,R6\r\n       JLT  WRAPM1\r\n       S    @VWIDTH,R6\r\n       JMP  WRAPM2\r\nWRAPM1 A    @VWIDTH,R6\r\nWRAPM2 COC  @W2,R0                     * Horizontal?\r\n       JNE  BLDMW1\r\n       MOV  R7,R7\r\n       JLT  WRAPM3\r\n       S    @HWIDTH,R7\r\n       JMP  BLDMW1\r\nWRAPM3 A    @HWIDTH,R7\r\n* Get tile\r\nBLDMW1 MOV  R7,R0                      * Copy R7 to R0\r\n       MPY  @HWIDTH,R6                 * Multiply Y by hortz width\r\n       A    R0,R7                      * Calculate map index into R7\r\n       MOVB @MAPBUF(R7),@VMAP(R5)      * Copy tile\r\n       MOVB @MAPENV(R7),@EMAP(R5)      * Copy elevation level\r\n       MOVB @VMAP(R5),@LMAP(R5)        * Copy light level\r\n       SOCB @B128,@VMAP(R5)            * Set high bit on active tile\r\n       SZCB @B127,@LMAP(R5)            * Filter light array to top bit only\r\n       MOVB @B2,@LOSMAP(R5)            * Set LOS map to blocked\r\n       INC  R5                         * Increment the buffer pointer\r\n       INC  R2                         * Increment the column index\r\n       DEC  R4                         * Decrement the window width count\r\n       JNE  BLDMPW\r\n       INC  R1                         * Increment the row index\r\n       A    R9,R8                      * Add slant change to R8\r\n       MOV  @X,R2                      * Move X back into R2\r\n       A    R8,R2                      * Add slant to x position\r\n       LI   R4,13                      * Reset the window width to 13\r\n       DEC  R3                         * Decrement the window height count\r\n       JNE  BLDMPW\r\n* Retrieve data into state and sensing arrays\r\nBLDMP2 MOVB @VMAP+84,@CTILE            * Set current tile value\r\n       MOVB @EMAP+84,@CELEV            * Set current elevation level\r\n       MOVB @B247,@VMAP+84             * Set player graphic for permissible space\r\n       MOVB @B1,@LOSMAP+84             * Set center of LOS map to visible\r\n       SETO @SURRND                    * Clear the surrounding tile contents\r\n       SETO @SURRND+2\r\n       SETO @SURRND+4\r\n       SETO @SURRND+6\r\n       MOVB @VMAP+97,@SURRND           * Copy the down tile\r\n       MOVB @VMAP+83,@SURRND+2         * Copy the left tile\r\n       MOVB @VMAP+71,@SURRND+4         * Copy the up tile\r\n       MOVB @VMAP+85,@SURRND+6         * Copy the right tile\r\n* Mob processing\r\n       LI   R3,4\r\n       BLWP @PAGE2\r\n       CLR  @WORK2                     * Clear WORK2 (in map mob count)\r\n       MOV  @MOBCNT,R0                 * Copy total mob count to R0\r\n       JEQ  BLDMP3                     * If 0, skip to next phase\r\n       MOV  @MOBADR,R1\r\n       CLR  @WORK                      * Clear @WORK (Mob #)\r\n       LI   R6,WORK2+2                 * Set R6 to WORK2+2\r\nBM2    MOVB *R1,R2                     * Copy mob type into R2\r\n       JEQ  BM2B                       * If zero, skip, no counter decrease\r\n       CB   @B23,R2                    * Check if inert\r\n       JEQ  BM2B                       * If so, skip but decrease counter\r\n       JMP  BM2C\r\nBM2A   AB   @B1,@WORK\r\n       DEC  R0                         * Decrement mobs processed\r\n       JEQ  BLDMP3                     * If finished, move on\r\n       JMP  BM2\r\nBM2B   AB   @B1,@WORK\r\n       AI   R1,8                       * Go to next mob\r\n       DEC  R0                         * Decrement mobs processed\r\n       JEQ  BLDMP3                     * If finished, move on\r\n       JMP  BM2\r\nBM2C   MOV  *R1+,@MAPMOB               * Get mob data\r\n       MOV  *R1+,@MAPMOB+2\r\n       MOV  *R1+,@MAPMOB+4\r\n       MOV  *R1+,@MAPMOB+6\r\n       BLWP @MOBWIN                    * Calculate window positon\r\n       MOV  R3,R3\r\n       JLT  BM2A                       * Not visible, skip placement\r\n       INC  @WORK2                     * Increase mob count\r\n       MOV  R3,*R6+                    * Copy index to WORK2 array\r\n       MOVB @MAPMOB+1,*R6+             * Copy pattern to WORK2 array\r\n       MOVB @WORK,*R6+                 * Copy mob index\r\n       MOVB @MAPMOB+1,@VMAP(R3)        * Copy pattern to VMAP for LOS calculations\r\n       LI   R4,4\r\n       LI   R2,MOBSEN                  * Load mob sense data\r\nBM2D   C    R3,*R2+                    * Check if position is next to player\r\n       JNE  BM2E\r\n       MOV  *R2+,R5                    * Get address into R5\r\n       MOVB @WORK,*R5                  * Copy mob to state array\r\nBM2E   INCT R2\r\nBM2F   DEC  R4                         * Loop all four locations\r\n       JNE  BM2D\r\n       JMP  BM2A\r\n* Update sense counter for traps\/secrets\r\nBLDMP3 CLR  R1                         * Clear R1 for sense counter\r\n       LI   R0,SURRND+1                * Set R0 to SURRND array, mob area\r\n       LI   R2,4                       * Set R2 to 4 (4 directions)\r\nBM3A   CLR  R3\r\n       MOVB *R0+,R3                    * Copy mob # to R3\r\n       JLT  BM3B                       * if negative, skip\r\n       SRL  R3,5                       * Make 8-step index\r\n       A    @MOBADR,R3\r\n       MOVB *R3,R4                     * Copy mob type to R4\r\n       SB   @B16,R4                    * Subtract 16 from mob ID\r\n       JLT  BM3B                       * If less than zero, not a hidden mob\r\n       SRL  R4,8                       * Shift value to make index\r\n       MOVB @SENSEV(R4),R5             * Copy from character array\r\n       JEQ  BM3B                       * If 0, skip\r\n       SOCB R5,R1                      * Set bit\r\nBM3B   INC  R0                         * Increase to next tile position\r\n       DEC  R2                         * Decrement counter\r\n       JNE  BM3A\r\n       MOVB R1,@SENSEC+1               * Copy R1 to SENSEC (Counter)\r\n* Check party light level, map onto tiles\r\nLGTMAP MOV  @MAGEYE,R0                 * Check if magic eye is active\r\n       JEQ  LGT1\r\n       BL   @MEVIEW                    * Fully open map\r\n       B    @PCME4A\r\nLGT1   MOVB @LIGHT,R0                  * Check if map is fully lit\r\n       JEQ  LGT1A                      * If so, skip to LOS algorithm\r\n       MOVB @PARTY+30,R0\r\n       ANDI R0,&gt;2000                   * Check for Radiant Pharos\r\n       JEQ  LGT1B\r\nLGT1A  BL   @MEVIEW                    * Fully open map\r\n       JMP  LOS\r\nLGT1B  CLR  R1                         * Set buffer index\r\n       LI   R5,169                     * Set buffer counter\r\nLGT2   MOVB @ALIGHT(R1),R2             * Copy window position light value into R2\r\n       SB   @LGTLV+1,R2                * Subtract the current light strength from window value\r\n       JGT  LGT3                       * If greater, unlit\r\n       MOVB @B128,@LMAP(R1)            * Otherwise, mark the tile lit\r\nLGT3   INC  R1\r\n       DEC  R5\r\n       JNE  LGT2\r\n* Map line-of-sight on the map\r\nLOS    LI   R8,4                       * Number of directions to process\r\n       CLR  R9                         * Direction index\r\nCLOS1  LI   R7,6                       * Count of tiles\r\n       LI   R0,6                       * Column position\r\n       LI   R1,6                       * Row position\r\nCLOS2  CLR  R12                        * Set tile to copy to closed tile\r\n       BL   @POSCLC                    * Calculate position\r\n       MOVB @LOSMAP(R3),R2\r\n       JEQ  CLOS3\r\n       CI   R3,84\r\n       JEQ  CLOS2A\r\n       BL   @CHKTLE                    * Fetch tile's opacity level, also check elevation\r\n       ANDI R5,&gt;8000                   * Check if a blocking tile\r\n       JNE  CLOS3\r\nCLOS2A LI   R12,&gt;0100                  * Set tile to open\r\n* Set tile\r\nCLOS3  MOV  @MAPLOS(R9),R2             * Copy direction vector index\r\n       A    @DIRX(R2),R0               * Add direction vector to column\r\n       A    @DIRY(R2),R1               * Add direction vector to row\r\n       BL   @POSCLC                    * Get buffer index\r\n       MOVB R12,@LOSMAP(R3)            * Copy visible\/blocked value to buffer\r\n       DEC  R7\r\n       JNE  CLOS2                      * Loop through path\r\n       INCT R9                         * Change direction\r\n       DEC  R8\r\n       JNE  CLOS1                      * Loop through rows\r\n* Diagonal LOS\r\nDLOS   LI   R8,4                       * Number of directions to process\r\n       CLR  R9                         * Direction index\r\nDLOS1  LI   R6,6\r\n       LI   R0,6\r\nDLOS2  LI   R7,6\r\n       LI   R1,6\r\nDLOS3  BL   @POSCLC\r\n* Collect four tile indices in FRAM array\r\n       MOV  R3,@FRAM\r\n       MOV  R0,@FRAM+8\r\n       MOV  R1,@FRAM+10\r\n       MOV  @MAPLOS+8(R9),R2\r\n       A    @DIRX(R2),R0\r\n       A    @DIRY(R2),R1\r\n       BL   @POSCLC\r\n       MOV  R3,@FRAM+2\r\n       MOV  @FRAM+8,R0\r\n       MOV  @FRAM+10,R1\r\n       MOV  @MAPLOS+10(R9),R2\r\n       A    @DIRX(R2),R0\r\n       A    @DIRY(R2),R1\r\n       BL   @POSCLC\r\n       MOV  R3,@FRAM+4\r\n       MOV  @MAPLOS+8(R9),R2\r\n       A    @DIRX(R2),R0\r\n       A    @DIRY(R2),R1\r\n       BL   @POSCLC\r\n       MOV  R3,@FRAM+6\r\n       MOV  @FRAM+8,R0\r\n       MOV  @FRAM+10,R1\r\n* Check three surrounding tiles\r\n       CLR  R12\r\n       MOV  @W3,@FRAM+8\r\n       LI   R2,FRAM\r\nDLOS4  MOV  *R2+,R3\r\n       MOVB @LOSMAP(R3),R4\r\n       JEQ  DLOS5\r\n       BL   @CHKTLE\r\n       ANDI R5,&gt;8000\r\n       JEQ  DLOS6\r\nDLOS5  DEC  @FRAM+8\r\n       JNE  DLOS4\r\n       JMP  DLOS7\r\nDLOS6  LI   R12,&gt;0100\r\n* Set tile\r\nDLOS7  MOV  @FRAM+6,R3\r\n       MOVB R12,@LOSMAP(R3)\r\n       MOV  @MAPLOS+8(R9),R2\r\n       A    @DIRY(R2),R1\r\n       DEC  R7\r\n       JNE  DLOS3\r\n       MOV  @MAPLOS+10(R9),R2\r\n       A    @DIRX(R2),R0\r\n       DEC  R6\r\n       JNE  DLOS2\r\n       AI   R9,4\r\n       DEC  R8\r\n       JNE  DLOS1\r\n* Final opening of permitted space\r\nPCMEND CLR  R1                         * Set buffer index\r\n       LI   R0,169                     * Set buffer counter\r\n       MOVB @CELEV,R2                  * Copy elevation to R2\r\nPCME1  MOVB @LOSMAP(R1),R3             * Check LOS\r\n       JEQ  PCME3\r\n       MOVB @VMAP(R1),@LMAP(R1)        * Set the tile onto the map\r\n       JMP  PCME4\r\nPCME3  MOVB @SPACE,@LMAP(R1)           * Make the tile black (invisible)\r\nPCME4  INC  R1\r\n       DEC  R0\r\n       JNE  PCME1\r\n* Place visible mobs\r\nPCME4A MOV  @WORK2,R0                  * Check mob count\r\n       JEQ  PCME7                      * If zero, skip\r\n       LI   R1,WORK2+2\r\nPCME5  MOV  *R1+,R2                    * Get mob index\r\n       MOV  *R1+,R3                    * Get pattern\r\n       CB   @LMAP(R2),@SPACE           * Is the space blacked out?\r\n       JEQ  PCME6\r\n       MOVB R3,@LMAP(R2)               * Set mob on map\r\n       ANDI R3,&gt;00FF                   * Get mob #\r\n       MOVB @B1,@MOBVIS(R3)            * Set mob visibility to 1\r\nPCME6  DEC  R0                         * Decrement count\r\n       JNE  PCME5\r\nPCME7  LI   R0,&gt;F700\r\n       SB   @BOAT,R0\r\n       MOVB R0,@LMAP+84                * Copy the party icon to the center\r\n       B    @SUBRET\r\n\r\n* Magic eye\/Pharos view\r\nMEVIEW LI   R0,169                     * Set all tiles to lit\/visible\r\n       LI   R1,VMAP\r\n       LI   R2,LMAP\r\nMEVW1  MOVB *R1+,*R2+\r\n       DEC  R0\r\n       JNE  MEVW1\r\n       RT\r\n\r\n* Calculate mob position in viewing window\r\n* Returns window index (0-169) in R3, -1 = not in window\r\nMOBWIN DATA VWS,MOBWN0\r\nMOBWN0 MOVB @SLANT,R2                  * Get orientation into R2\r\n       SRL  R2,8\r\n       MOV  @DIRY(R2),R0               * Set R7 to 1 (left), 0 (none), or -1 (right)\r\n       NEG  R0                         * Negate R0\r\n       MOV  @W7,@WMOB                  * Store 7 in WMOB\r\n       MOV  @WN7,@WMOB+2               * Store -7 in WMOB+2\r\n       MOV  @MAPMOB+2,R2\r\n       MOV  R2,R4\r\n       SRL  R2,8                       * Set R2 to mob Y\r\n       ANDI R4,&gt;00FF                   * Set R4 to mob X\r\n       S    @Y,R2                      * Subtract player y from mob y\r\n       CI   R2,7                       * Check right vector\r\n       JLT  MOBWN1\r\n       MOVB @EDGES,@EDGES\r\n       JEQ  MOBWN5\r\n       S    @VWIDTH,R2                 * Subtract wrap offset\r\nMOBWN1 CI   R2,-7                      * Check left vector\r\n       JGT  MOBWN2\r\n       MOVB @EDGES,@EDGES\r\n       JEQ  MOBWN5\r\n       A    @VWIDTH,R2                 * Add wrap offset\r\n       CI   R2,6                       * Check left vector again\r\n       JGT  MOBWN5\r\nMOBWN2 MPY  R2,R0                      * Multiply Y offset by R0\r\n       S    R1,@WMOB                   * Adjust positive boudnary for X\r\n       S    R1,@WMOB+2                 * Adjust negative boundary for X\r\n       MOV  @WMOB,@WMOB+4\r\n       DEC  @WMOB+4\r\n       S    @X,R4                      * Subtract player x from mob x\r\n       C    R4,@WMOB                   * Check down vector\r\n       JLT  MOBWN3\r\n       MOVB @EDGES,@EDGES\r\n       JEQ  MOBWN5\r\n       S    @HWIDTH,R4                 * Subtract wrap offset\r\nMOBWN3 C    R4,@WMOB+2                 * Check vector\r\n       JGT  MOBWN4\r\n       MOVB @EDGES,@EDGES\r\n       JEQ  MOBWN5\r\n       A    @HWIDTH,R4                 * Add wrap offset\r\n       C    R4,@WMOB+4\r\n       JGT  MOBWN5\r\nMOBWN4 MPY  @W13,R2                    * Multiply by 13 (window width)\r\n       AI   R3,84                      * Add center offset\r\n       A    R4,R3                      * Add X delta\r\n       A    R1,R3                      * Add shift delta\r\n       MOV  R3,R3\r\n       JLT  MOBWN5\r\n       CI   R3,168\r\n       JGT  MOBWN5\r\n       MOV  R3,@&gt;0006(R13)             * Copy back to calling routine\r\n       RTWP\r\nMOBWN5 SETO @&gt;0006(R13)\r\n       RTWP\r\n\r\n* Check tile at index in R3\r\nCHKTLE MOVB @LMAP(R3),R4               * Check light level\r\n       JEQ  CHKTL2                     * If not lit, is automatically blocking\r\n       CB   @CELEV,@EMAP(R3)           * Check current elevation against target tile\r\n       JEQ  CHKTL3                     * If equal, continue to opacity test\r\n       JLT  CHKTL2                     * If less, go to block\r\nCHKTL1 CLR  R5                         * Clear R5 (open)\r\n       JMP  CHKTL4\r\nCHKTL2 SETO R5                         * Set R5 (closed)\r\n       JMP  CHKTL4\r\nCHKTL3 MOVB @VMAP(R3),R4               * Copy tile to R4 low byte\r\n       SRL  R4,8\r\n       ANDI R4,&gt;007F\r\n       MOVB @TILES(R4),R5              * Copy tile code into R5 high byte\r\nCHKTL4 RT\r\n\r\n* Check for map edges\r\n* R1 = Y, R2 = X\r\n* R0 returns 0 if no violation, 1 if vertical, 2 if horizontal, 3 if both\r\nEDGCHK CLR  R0                         * Set to 0\r\n       C    R2,@HWIDTH                 * Check X against horizontal width\r\n       JL   EDGCH1\r\n       INCT R0\r\nEDGCH1 C    R1,@VWIDTH\r\n       JL   EDGCH2\r\n       INC  R0\r\nEDGCH2 RT\r\n\r\n* Determine index on map\r\nPOSCLC MOV  R1,R2\r\n       MPY  @W13,R2\r\n       A    R0,R3\r\n       RT\r\n<\/pre>\n<h2>Conclusion<\/h2>\n<p>Despite the numerous calculations going on, the map view creation is pretty fast, enough that I had to introduce some artificial delays. I did notice that the more mobs on the map the more impact it had on performance. For that reason, there is a limit of mobs per map, and I actually broke up some maps into more than one map to remove performance problems.<\/p>\n<p>So that&#8217;s it with maps and part 2! I&#8217;m not sure what part 3 will cover yet, I&#8217;m open to feedback.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this, part 2, we will be looking at scrolling maps, a pivotal element of top-down 2D computer role play games of yore. I think something that is lost on many modern gamers, who didn&#8217;t grow up in the 80&#8217;s. &hellip; <a href=\"http:\/\/www.adamantyr.com\/index.php\/2021\/12\/15\/making-a-crpg-part-2-maps-scrolling-and-line-of-sight\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[3,4,5],"tags":[],"class_list":["post-1628","post","type-post","status-publish","format-standard","hentry","category-coding","category-crpg","category-design"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/pgaeMJ-qg","_links":{"self":[{"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/posts\/1628","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/comments?post=1628"}],"version-history":[{"count":9,"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/posts\/1628\/revisions"}],"predecessor-version":[{"id":1641,"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/posts\/1628\/revisions\/1641"}],"wp:attachment":[{"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/media?parent=1628"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/categories?post=1628"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.adamantyr.com\/index.php\/wp-json\/wp\/v2\/tags?post=1628"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}