PICO-8 Devlog 4: Refactoring

I'm falling into a rhythm in which I alternate between the two strands of my project, which basically can be summed up as 1) function and 2) form. My last stretch was on the latter, figuring out a graphical style and adding some effects, so it was back to nuts and bolts for this block. Technical debt, basically.

Objective

My movement system is basically the tile-based approach from Lazy Devs Academy's Porklike tutorial. I had a working prototype but the code was very duplicative and ad hoc, so I knew I'd need to sort that out. The other complication was was that instead of just moving one space at a time, my player sprite needs to know in advance how many empty tiles there are in any given direction before it meets an obstruction. So I have two separate mechanisms for which I need general solutions, and they need to work together.

Process

I watched and rewatched Lazy Devs Academy videos, trying to extract the relevant information. It was tough because the order in which he deals with things is not the same as mine, and his use-case is a bit different.

I eventually reached a point where I roughly knew what I wanted to do, but there were too many moving parts to make it happen all in one go. So I made two new carts to spike out the movement and range-finding functions separately, and when I had both working I set about integrating them.

Even using the PICO-8 tabs and organising the code in sensible ways, I'm still struggling to track down why things aren't working. And then, when I do track them down, they're almost always some variant on tables being 1-indexed 🙄

I was going to say "I'll spare you the details", but then I remembered that I'm probably the only person who'll ever read this back, and if I do it'll be because I want to remember the details, so I'll try and get them down here in a relatively light-touch way.

Range-finding

Previously I was assigning empty tile counts to key:value pairs in a table —

empty = {
    n: [num],
    s: [num],
    e: [num],
    w: [num],
}

— and doing a whole if btnp(0) then px+=empty.w thing. That's been abstracted away:

for i=0,3 do -- poll for inputs
	if btnp(i) then
		dx=dirx[i+1] -- -1|0|1
		dy=diry[i+1] -- -1|0|1
		local distx=dx*empty[i+1] -- 'empty' is an array of how far can be moved in any direction
		local disty=dy*empty[i+1] -- either dx or dy will always be zero, so 1-axis movement only
		if not (distx+disty==0) then -- a 0 total means there are no free tiles in that direction
			px+=distx -- again, player position is set on both axes but one of them is alway zero
			py+=disty
			get_moves(px,py)
			pox,poy=distx*8,disty*8
			sfx(sound.launch)
			_upd=upd_pturn
		else
			shake=1 -- do: extract this!
			sfx(sound.blocked)
		end 
	end
end

And I managed to get this whole business:

function update_pturn()
	if pox>0 then
		pox-=8
	end
	if pox<0 then
		pox+=8
	end
	if poy>0 then
		poy-=8
	end
	if poy<0 then
		poy+=8
	end

Down to this:

function upd_pturn()
	local speed=8 -- *|/ 8 only
	pox-=speed*dx
	poy-=speed*dy

Cool. This is what I set out to do!

I'm not going to say that I think this implementation is perfect or optimal or extensible to whatever use cases I might have down the line, but it's appropriately elegant for my current purposes; there's nothing I'd consider technical debt nagging away at my conscience.

Next steps

Back to the FX. I want to get those block particle effects working, shockwaves, crunchy interactions, all that biz.

END