Log In  


Posted this in the discord, but decided it was worth a repost here. If you're new to binary math, this is a decent introduction of the concepts, and how you can use them in Pico-8.

Introduction to binary numbers

Numbers in pico-8 are stored in binary as 16.16 (32 bits, half above the decimal point, half below). A good way to visualize these numbers is like this:
... 128 64 32 16 8 4 2 1 . 1/2 1/4 1/8 1/16 ...

Thus, 1001.01 is how the number 9.25 actually looks in binary. To convert whole numbers, use your fingers and count powers of two until you hit the last one less than your number, then subtract that much from the number and repeat the process (so 47 is 32+8+4+2+1 == 101111).
pico-8 lets you type numbers as binary (0b101111) or hexadecimal (0x2f), but they're all the same as 47. Use whichever conversion method makes it easiest for you.

Negative numbers are a whole other thing involving two's complement (so -1 is 0b1111111111111111 in Pico-8, because then adding 1 to it makes it zero). Probably not worth mentioning if you're just starting out, but something to keep in mind.
Now, with that out of the way, let's talk binary ops...

Binary operations

The important part about dealing with binary ops is that you have to remember that everything is powers of two. 1 is 0001, 2 is 0010, 4 is 0100, 8 is 1000, so these numbers will become very important for checking if a single bit is on or off. Your basic operations for changing bits are these:

(number & 0b0010)!=0 --is bit 2 on?
number = number | 0b0100 --turn bit 3 on
number = number & 0b1111111111111011.1111111111111111 --turn bit 3 off (but save the rest!)
number = number ^^ 0b1000 --toggle bit 4 on/off (make it the opposite of what it was, 0 or 1)

Shifting operators are great, because they're basically like multiplying or dividing by powers of two, but faster for the computer to use. They let you add bits together in the same number, by moving them to the same "slots". 0001<<3 == 1000, and 1000>>2 == 0010. Remember also that the way these numbers are stored means that <<3 is the same as multiplying by 2^3, or 8. >>2 is the same as dividing by 2^2, or 4.

The difference between a "logical right shift" and an "arithmetic right shift" has to do with what gets shifted into the empty slots. Remember that two's complement weirdness where -1 was stored as 1111111111111111? Well, >> preserves the top bit, so it still divides negative numbers properly by powers of two. >>> just puts 0s into the empty slots.

If you're working with bits and not math, you usually want the >>> operator instead.

Rotation is only really useful in rare cases, in my experience. Some random number generators use it because it doesn't discard anything or replace stuff with zeros, which are obviously less-random ways to adjust your data.

How to use all this

The most common case of bitwise ops is packing multiple numbers into the same number. If you only need numbers between 0 and 15, they can be stored in 4 bits, so there's 28 bits just sitting around doing nothing in each pico-8 number! You can therefore use shifting and bitwise ORs to make a number hold more than one piece of information inside it, which saves space. And that's actually how the screen pixels are stored in memory, since they can only have 16 color values per pixel. If you peek or poke into the screen memory at 0x6000 and above, you'll need to understand how to do some bit shifting to put the colors in the right places.

Just one last thing for bitwise, and then I'll end my TED talk. If you need binary flags (on or off values), you can obviously store that in just one bit. Things like pico-8's sprite flags are stored in this way, so checking bits by &'ing with powers of two will tell you if those individual bits are on or off directly. With the set-on or set-off examples I put above, you can adjust those true/false flags as well.

fget(spr,3) is basically the same as (flags[spr]&(0b00000001<<3))!=0.
That might seem more complicated in expression syntax, but if you go through it this should make sense.

Also note that I wrote a lot of binary syntax with leading zeros for clarity, but you don't have to do that. I much prefer hex myself, and when you get comfortable with it that becomes much easier to use. The conversion between hexadecimal and binary is very easy, because every four 0000's just become a number from 0-f, similar to how the numbers 0-9 are repeated to store bigger numbers like 10 or 100. It's probably outside of the scope of an introduction to these concepts, but once you learn this you have another tool you can use.

If you're interested in experimenting with any of this, I strongly recommend that you check out the Memory section of the Pico-8 manual. There are lots of cool functions to use there!

29


2

@shy

Very interesting post! This got me thinking - since cart data stores 32-bit numbers, you could encode up to four 8-bit numbers in a single storage slot, this would be useful for a high scores table for example, and only require one storage slot to save a player name (up to 4 characters) and a second slot for their score (a 32-bit number up to 32767).

I created two functions to pack/unpack the bits, however only the first two numbers seem to be unpack correctly, the last two always unpack as either 0 or 255. I am not sure what I am missing to solve this problem. Any hints as to how I can fix this would be greatly appreciated.

The full program is listed here:

function bpack(n1,n2,n3,n4)
 return (n4<<24) |
        (n3<<16) |
        (n2<<8)  |
        (n1<<0)
end

function bunpack(packed)
 local n1=(packed>>0) & 0xff
 local n2=(packed>>8) & 0xff
 local n3=(packed>>16) & 0xff
 local n4=(packed>>24) & 0xff
 return n1,n2,n3,n4
end

-- four random 8-bit numbers
i1=flr(255*rnd())
i2=flr(255*rnd())
i3=flr(255*rnd())
i4=flr(255*rnd())

-- pack them
bits=bpack(i1,i2,i3,i4)

-- unpack them
o1,o2,o3,o4=bunpack(bits)

-- print the outcome
color(10)
print("packing four 8-bit no's:")
print(i1..", "..i2..", "..i3..", "..i4)

color(9)
print("packed as: "..bits)

color(11)
print("unpacked numbers:")
print(o1..", "..o2..", "..o3..", "..o4)

2

In the fixed point 16:16 format Pico-8 uses there are 16 bits available for integers, and 16 for the 'mantissa' (fractional part).
You'll need to store two of those numbers in the fractional bits by shifting them to the right instead.


1

@wez: I know I'm very late to this conversation, but also the reason that you are only getting 255 and 0 is because you are using >> instead of >>>. Any whole number shifted >>16 or more is only going to return the top bit of the 16.16 fixed format repeated over and over again (since that's what >> does).
So just make sure to use >>>8 and >>>16 to save values (instead of <<16 and <<24), and <<8 and <<16 to recover values (instead of >>>16 and >>>24), as that's where the other 16 bits of storage space are located.


Another set of functions to use cart data as arbitrary storage space using binary: https://www.lexaloffle.com/bbs/?tid=30711
(maybe not up to date with the latest operators that are faster than functions)


2

@shy that works perfectly, thanks.

For posterity here are the fixed pack/unpack functions:

function bpack(n1,n2,n3,n4)
 return (n4>>>16) |
        (n3>>>8) |
        (n2<<8)  |
        (n1<<0)
end

function bunpack(packed)
 local n1=(packed>>0) & 0xff
 local n2=(packed>>8) & 0xff
 local n3=(packed<<8) & 0xff
 local n4=(packed<<16) & 0xff
 return n1,n2,n3,n4
end

2

This tutorial helped me understand this so much better, and now I can save TONS of data in my 64 values of dset() space. Thanks!

p.s. here's a version of this that can save 32 1-bit switches. I use it to save on/off states for a lot of objects in my game to persistent cartdata. 32 switches in a single dset() value!

-- takes in a table of 32 0|1 values
function b32pack(nums)
	local b=0
	for i =1,32 do
		local s=i-16
		b=b | nums[i]>>s
	end
	return b
	-- returns a 32-bit number
end

-- takes a 32-bit number
function b32unpack(b)
	local nums={}
	for i =1,32 do
		local s=i-16
        local n=(b&1>>s)<<s
		nums[i]=abs(n)
        -- absolute value must be taken since neg values get a -1 in the last bit
	end
	return nums
	-- returns a table of 32 0|1 values
end

1

...and here's a version that can take integers of any bitsize up to 16bit, and pack them down into however many can fit into 32 bits. In case you want to be flexible with your save system.

Here, they take parameters 'bs' for bitsize and 'n' for how many values, where n*bs MUST <=32

-- takes in a table 't' of 'n' values of bitsize 'bs'
function b32pack(t,bs,n)
	local b=0
	for i=1,n do
		local a=t[i] or 0   --adds a 0 if i>#t
		local s=(i*bs)-16
		b=b|a>>s
	end
	return b
	-- returns a 32-bit number
end

-- takes a 32-bit number and the amount 'n' of 'bs'-bit values desired
function b32unpack(b,bs,n)
	local t={}
	for i=1,n do
		local e,s=1,(i*bs)-16
		for l=1,bs do e=e*2 end
		e-=1
		t[i]=(b&e>>(s))<<s
	end
	return t
	--returns the table
end

It won't work for values above 16 bits, or for non-integers, and when bs*n>32 it will fudge the numbers, obviously.


for completeness, we now have a built-in function for big scores: https://www.lexaloffle.com/dl/docs/pico-8_manual.html#TOSTR



[Please log in to post a comment]