Andrey Listopadov

Game1 W3/4 (again)

Who knew that writing a post about how I’ve procrastinated for two whole weeks instead of following my own challenge would be so motivating? Immediately after I posted the previous post on Monday, I regained interest and started working on physics integration. Well, making a platformer without experience turned out to be harder than I thought, though I got stuck on things that are not platformer specific.

For the first half of the week, I’ve been trying to implement collision resolution myself. This… didn’t go well, as can be seen here:

Figure 1: The green rectangles depict the area in which I check for possible collisions, and red rectangles mark tiles that collided with the player

Figure 1: The green rectangles depict the area in which I check for possible collisions, and red rectangles mark tiles that collided with the player

I’ve read some tutorials on TIC’s wiki, mainly this one. It mentions how to track collisions by checking nearby squarest cells on the map grid. That was exactly my idea, but it didn’t work well, as was shown in the GIF above.

I suspect the main reason for that is that the player object moves at irregular intervals because the distance is determined by velocity, and not constant as in the tutorial. Thus, the object often overshoots or undershoots the wall and the floor, which led to the player stuck in walls, or going completely through them. It reminded me of problems I had with my previous game, where the ball often got through the platform and bricks, so after failing to do physics again I decided to use a library.

Using a library in a TIC game means that the library has to be included in the game cart because there’s no other way of shipping games for this platform - a cart is a self-contained program without external dependencies. TIC’s carts also have limited capacity so the amount of code I can include is also limited. Fortunately, there’s a small enough library called bump.lua that does AABB collision detection, which is exactly what I need for this type of game. TIC games are tile-based, so axis-aligned rectangles are exactly what I need to handle. With some trickery, it is even possible to implement slopes by using special kinds of boxes that define a slope offset based on the X coordinate.

There was a problem though. bump.lua is a Lua library, and while TIC supports Lua, I’m doing this game in Fennel. Usually, it’s not a problem, because projects written in Fennel can use Lua libraries without any issues, and technically I could have used bump.lua as a library from TIC-80 too. But, as I mentioned, TIC carts must be self-contained, and thus can’t load an external library. So I had to decide - either I port bump.lua to Fennel, or compile my Fennel code to Lua and continue the development this way. I also considered re-implementing a smaller subset of bump.lua specifically for using it in TIC with map tiles, which would probably make the library smaller and easier to handle, but I decided that I want to actually see some results of my work at the end of this month.

There was another compelling way of handling this situation though. I could have used Fennel’s -require-as-include compile flag to, well, include bump.lua as an embedded dependency, and after the development is finished compile the cart to Lua right before shipping. This would be the easiest way of doing things, as it would mean that I can do a completely normal development and use any libraries I want, but the downside is that the cart will be a hot mess of obfuscated Lua code, and I wanted the cart to actually contain something readable. Maybe I’ll try this in the future, who knows?

So, after some consideration of continuing in Lua and just copying bump.fnl code into the cart, I decided to translate bump.lua to Fennel. As much as I like Lua, Fennel is more pleasant to work with. It wasn’t that hard to do, given that there’s an excellent Lua-to-Fennel decompiler, that did most of the job. It does generate weird code here and there, calling set-forcibly! every now and then, and inserting (lua "return ") in places where Lua code had a sole return, or doing things like this:

-- original Lua code
local function sortByTiAndDistance(a,b)
  if a.ti == b.ti then
    local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
    local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
    local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
    return ad < bd
  end
  return a.ti < b.ti
end
;; Lua decompiler result
(fn sort-by-ti-and-distance [a b]
  (when (= a.ti b.ti)
    (local (ir ar br) (values a.itemRect a.otherRect b.otherRect))
    (local ad
           (rect-get-square-distance ir.x ir.y ir.w ir.h ar.x ar.y ar.w ar.h))
    (local bd
           (rect-get-square-distance ir.x ir.y ir.w ir.h br.x br.y br.w br.h))
    (let [___antifnl_rtn_1___ (< ad bd)] (lua "return ___antifnl_rtn_1___")))
  (< a.ti b.ti))


;; cleaned version
(fn sort-by-ti-and-distance [a b]
  (if (= a.ti b.ti)
      (let [ir a.itemRect
            ar a.otherRect
            br b.otherRect
            ad (rect-get-square-distance
                ir.x ir.y ir.w ir.h ar.x ar.y ar.w ar.h)
            bd (rect-get-square-distance
                ir.x ir.y ir.w ir.h br.x br.y br.w br.h)]
        (< ad bd))
      (< a.ti b.ti)))
Code Snippet 1: e.g. here, the decompiler transformed a single-branch if into when and generated an explicit return, but a smarter decompiler could transform this into two branched if without any early returns. Conceptually these two variants are the same, except one is more like the guard-return style and another is more like actual lisp-style.

So I took some extra time to clean all set-forcibly! calls, restored the original comments which were lost during translation, and removed all instances of explicit early returns, by converting the code to use branches instead. After compiling the library back to Lua, I tested it with some demos listed in the library’s readme and it worked fine, so I hope I didn’t introduce any new bugs during the cleanup state. The next thing was to integrate it into the game.

It wasn’t that hard to do, as I just copied the whole library code into the cart, but I was worried if it will be able to handle lots of squares. My idea was to iterate over every map cell, check if it is solid or not, and if so add it to the world. Thus, each non-empty tile on the map exists in the world and acts as a collideable object. As a result, I can use the map editor basically as a level editor, as adding a tile basically is like adding a world object.

The resulting performance seems to be fine on my PC, though I’m unsure about the browser which I’ll target to make the game playable on my itch.io web page. I’m still exploring the idea of using special tile flags to make rooms longer than they seem on the map, as described in the platformer tutorial that I’ve mentioned. But this probably requires special handling when it comes to integrating it with the bump library. For now, the result (with some debug info) looks like this:

Figure 2: bump.lua in TIC-80 with some debug visualisations of collisions

Figure 2: bump.lua in TIC-80 with some debug visualisations of collisions

The next thing to do was the camera. Now, since each object in the world has the same coordinate as the map, the only thing I need to do is implement the translation of world coordinates to screen coordinates based on player position with some slight interpolation. Fortunately enough, there’s a handy tutorial on camera movement, and while I’d like to try and implement this myself, I decided to rely on external source of wisdom.

Unfortunately, for some weird reason, the camera described in this tutorial worked differently for me. For some reason, the player sprite was drawn in the top left corner of the screen, and the camera was also positioned there. I mean, it moves correctly but is unplayable in this state. After I’ve experimented with some random numbers I’ve managed to get it to work:

Figure 3: The camera moves with a slight delay to add a bit more dynamic feel, but I didn&rsquo;t go for a more proper camera implementation that tries to show more in the direction of movement yet. Also, what&rsquo;s up with these GIFs generated by TIC, their size is around 10MB each, so I had to heavily compress them myself, yet they look entirely the same afterwards.

Figure 3: The camera moves with a slight delay to add a bit more dynamic feel, but I didn’t go for a more proper camera implementation that tries to show more in the direction of movement yet. Also, what’s up with these GIFs generated by TIC, their size is around 10MB each, so I had to heavily compress them myself, yet they look entirely the same afterwards.

With that in place, now I need to design some levels and add more graphics, and sounds. As well, as implement actual environment interactions, such as roaming and collideable enemies, stationary hazards like spikes, health, score, and respawn systems, and maybe some other intractable objects, like coins to collect. Not sure what I’ll do first, probably the latter, as it will teach me how to add collideable items to the world that should not be treated as platforms.