Ludum Dare 55 Postmortem

Another Ludum Dare has come and gone! This time I teamed up with Whaies and we created a game about summoning lawyers and other ... things ... to court. You can play it right here, or rate it at the Ludum Dare site.

I had a lot of fun figuring out the code for Cards. I wanted them to lift, rotate, and shuffle like the real thing and I think the solution I came up with was pretty neat!

Anatomy of a Card

Anybody familiar with TIC-80 will know the basic way to draw a sprite is with the spr function. You give it an index in the sprite sheet and [x,y] coordinates and your done. But what if you want to do something crazy like rotate the sprite?! For that we must somehow implement affine transformation.

The way everyone in the TIC-80 community has done this is using the ttri, textured triangle, function. I actually wrote about this a couple years ago! The basic idea is to use 2 textured triangles to draw a rectangle, and by manipulating the 4 coordinates using math you can do all sorts of fun transformations. Here's what that look likes like in Janet.

# adapted from https://cxong.github.io/tic-80-examples/affine-sprites

(defn deg2rad [theta] (* theta PI_OVER_180))
(defn rad2deg [theta] (* theta ONE_EIGHTY_OVER_PI))

(defn aspr/rotate [x y ca sa]
  [(- (* x ca) (* y sa))
   (+ (* x sa) (* y ca))])

(defn aspr [x y &named
	    u1 v1
	    texsrc chromakey
	    sx sy flip rotate w h
	    ox oy shx1 shy1 shx2 shy2]
  (default u1 0)
  (default v1 0)
  (default texsrc TEXSRC_SPR)
  (default chromakey -1)
  (default sx 1)
  (default sy 1)
  (default flip 0)
  (default rotate 0)
  (default w 1)
  (default h 1)
  (default ox (math/floor (/ (* w 8) 2)))
  (default oy (math/floor (/ (* h 8) 2)))
  (default shx1 0)
  (default shy1 0)
  (default shx2 0)
  (default shy2 0)

  (let [sx (if (= 1 (% flip 2)) (* -1 sx) sx)
	sy (if (> flip 2) (* -1 sy) sy)
	ox (* -1 ox sx)
	oy (* -1 oy sy)

	# Shear & rotate
	shx1 (* -1 shx1 sx)
	shy1 (* -1 shy1 sy)
	shx2 (* -1 shx2 sx)
	shy2 (* -1 shy2 sy)
	rr rotate
	ca (math/cos rr)
	sa (math/sin rr)
	[rx1 ry1] (aspr/rotate (+ ox shx1) (+ oy shy1) ca sa)
	[rx2 ry2] (aspr/rotate (+ ox shx1 (* w 8 sx)) (+ oy shy2) ca sa)
	[rx3 ry3] (aspr/rotate (+ ox shx2) (+ oy shy1 (* h 8 sy)) ca sa)
	[rx4 ry4] (aspr/rotate (+ ox shx2 (* w 8 sx)) (+ oy shy2 (* h 8 sy)) ca sa)
	[x1 y1] [(+ x rx1) (+ y ry1)]
	[x2 y2] [(+ x rx2) (+ y ry2)]
	[x3 y3] [(+ x rx3) (+ y ry3)]
	[x4 y4] [(+ x rx4) (+ y ry4)]

	# UV coords
	u2 (+ u1 (* w 8))
	v2 (+ v1 (* h 8))]
    (tic80/ttri x1 y1 x2 y2 x3 y3 u1 v1 u2 v1 u1 v2 texsrc chromakey)
    (tic80/ttri x3 y3 x4 y4 x2 y2 u1 v2 u2 v2 u2 v1 texsrc chromakey)))
Sidenote: that this implementation does not use matrices, which maybe means "affine" is the wrong technical term?

With this function we can draw a sprite and rotate/scale it however we need.

Awesome!

HOWEVER, there's one problem. This rotates sprites, but how do we transform text? I didn't want to waste space in the spritesheet embedding letters, and TIC-80's print function definitely does not support rotating.

The Solution is to abuse use VRAM. The video ram in TIC-80 is "double banked", which means you basically have 2 screens which are drawn over each other to work with. Conveniently, ttri supports pulling the texture from various different places. Those are the spritesheet, tilemap, and the VRAM you're currently not drawing to. With this we can

  1. switch to VRAM 0
  2. draw out the entire card using spr, print and map calls. Don't worry about any sort of rotation or scaling at this point.
  3. switch to VRAM 1
  4. switch to clear the screen to hide anything on VRAM 0
  5. use our custom sspr function with VRAM 0 as our texture source to draw our card with scale and rotation!

Here's the annotated card drawing source code in Upcard Writ

# begin drawing in vbank 0
(tic80/vbank 0)
(tic80/cls 0)

# draw empty base of card, which is in the map
# I palette swap based on its type... maybe more on that in a future blog post
(with-pallete-swap (match (character :integrity)
			 :virtuous {5 14}
			 :pragmatic {5 9}
			 :sleazy {5 4})
      (tic80/map 0 0  6 10  0 0  0))

# only face up cards need to have their details drawn
(when flipped?
      # print out title
      (print-centered (character :name) 24 4 1 false 1 true)
      (print-centered (character :name) 24 3 7 false 1 true)

      # draw the cards picture if it has one
      (when (character :sprite)
	(tic80/rect 0 11 48 32 (character :sprite-bg))
	(tic80/spr (character :sprite) ;(character :sprite-args))
	(tic80/rectb 0 11 48 32 7))

      # draw the cards "resources"
      # ... theres some annoying logic to split it into 2 rows
      (loop  [[i [mod ev]] :pairs (array/slice (character :evidence) 0
					 (min 2 (length (character :evidence))))]
	(evidence-spr ev (+ 8 (* i 22)) 43)
	(tic80/spr (match mod :+ 1 :- 2) (+ 3 (* i 22)) 43 0))
      (when (> (length (character :evidence)) 2)
	(loop  [[i [mod ev]] :pairs (array/slice (character :evidence) 2)]
	  (evidence-spr ev (+ 8 (* i 22)) 60)
	  (tic80/spr (match mod :+ 1 :- 2) (+ 3 (* i 22)) 57 0))))

# some other stuff happens...
# eventually we switch to vbank 1
(tic80/vbank 0)
(tic80/cls 0)

# and Finally draw the card with rotation and scale!
(aspr ;(round (- (self :pos) [0 (self :height)]))
	  :u1 0 :v1 0
	  :w 6 :h 10
	  :texsrc TEXSRC_VRAM
	  :sx (self :scale) :sy (self :scale)
	  :rotate (self :rotation))

Phew! Here's the results (I've removed a screen clear so you can see both vrams).

My gut says I've finally stumbled into a technique that's very common throughout gamedev. Drawing to textures/images that you only ever use internally is foundational for most GPU shaders. I'm excited to explore this more in the future!


Other Musings on the Jam

I want to work with more people

For the longest time I thought of myself as the "I work alone" type. It is still true that I enjoy all parts of creating games: the art, music, coding, and design. However there's only so much you can do in a certain amount of time! Working with others means creating more ambitious projects! Its also just plain fun to bounce ideas back and forth, to create with others.

I want to learn more 52 card games

This game about cards has also sparked a personal exploration of classic 52 Card games. I went and bought myself a deck of bridge size cards for my small hands, and have begun working through the wonderful Isaludo solo games collection by Wil Su. I've also voluntold my wife to learn some 2 player card games. I think the end result of all this will be me designing my own 52 Card game.. Or maybe even my own alternative deck! I already have some idea cooking.