Pico-8 Particle Animation With Sprite Font


Pico-8 Old school demoLast months Revision demo party inspired me to take a few evenings to write an “old skool” demo for the Pico-8. It’s a fairly simple demo that is reminiscent of the Amiga and C64 demo scene, it illustrates a randomly generated star field that is animated within the bounds of the Pico’s screen. To add that retro flare, a text marque scrolls across the footer displaying a message. The built in font set didn’t have that sizzle I was looking for, so I opted for a custom sprite based font.

You can watch the full demo over at Pico-8 BBS.

Star Field Generation and Animation

The position and quantity of stars is randomly generated and stored in a 2 dimensional array.


function gen_stars()
	for row = 1, star.num do
	    star_array[row] = {}
	
	    for column = 1, star.num do
		    star.rand = rnd(star.num)
			star.rand = flr(star.rand)
			
	        star_array[row][column] = star.rand
	   end
	end
end

To ensure theses locations are generated only once, we call the randomization routine from the _init() function:


function _init() 
	timer = {}
	timer.t = 0
	timer.c = 0

	star = {}
	star.start = 0
	star.num = 120

	gen_stars()
end

The star animation routine is referenced from the _draw() function. To get the randomized position effect, we utilize the x and y axis of the 2d array by switching the x and y axis references and by passing the coordinate into the pset() function to position a pixel on screen. Inside the for loop, we can get the position of each pixel and by utilizing a combination of if statements that listen to the pixels position, then we can switch direction of each pixel and it’s color.


function star_ani()
	local x_move = {}
	local color_flash = 7
	for y = 1, 10 do
	    for x = 1, 10 do
		  x_move = (star_array[y][x]) + timer.t    
		  local y_pos = {}		  
		  	
			if  (x_move >0) then
				 y_pos = star_array[x][y] + (timer.t / 10)
				x_move = x_move * (x_move / 100)
				
				if  (x_move < 130) then color_flash = 7 end if (x_move > 130) then
					x_move =  200 - (x_move / 2)
					
					color_flash = 5
				end
			end
			
			if  (x_move <0) then
				y_pos = star_array[x][y] - (timer.t / 10)
				x_move = x_move * (x_move / 100)
				if  (x_move < 130) then color_flash = 5 end if (x_move > -200) then
					x_move =  1 + (x_move * 2)
					color_flash = 7
				end
			end

			pset(x_move, y_pos,  color_flash)
	    end
	end
	if timer.t <= 250 then 
		timer.t = timer.t + 1 
	end 
	if timer.t >= 250 then
		timer.t =  -200
	end
end

Message Marque Animation

The custom font and message display is a simple sprite based system that stores the message letters in an array. An algorithm then loops through the array and uses the array index multiplied by 8 (the width of the font) to pull the character and position it.

Animating the text along the x axis involves decrementing each characters calculated position by 1. Bouncing the text on the y axis is achieved by  implementing a simple if, else statement with three conditions.

Here’s the message render that is called from the _draw() function:


function message_print()
	timer.c = timer.c +1
	local alt_length = message.length - (message.length * 10) 
	
	if alt_length > message.x_move then
		message.x_move = 128
	end

	if timer.c > 30 then
		message.x_move = message.x_move - 2
		if message.y_move > 110 and  message.y_move < 120 and y_flag == true then
			message.y_move = message.y_move + 1
			if message.y_move == 119 then
				y_flag = false
			end
		elseif message.y_move < 120 and message.y_move > 110 and y_flag == false then
			message.y_move = message.y_move - 1
			if message.y_move == 111 then
				y_flag = true
			end
		end

		for i=1, message.length do
			local y_ab = flr(message.y_move)
			message.message_pos_x[i] = {}
			message.message_pos_x[i] =  (message.x_start + (i*8)) + message.x_move 
			
			message.message_pos_y[i] = {}
			message.message_pos_y[i] =  message.y_move
			spr(message.message[i],message.message_pos_x[i], message.message_pos_y[i], 1, 1)
		end
	end
end

The actual message itself, that I mentioned, was created from an array and is declared in the _init() function at program load:


function _init() 
	timer = {}
	timer.t = 0
	timer.c = 0
	
	star = {}
	star.start = 0
	star.num = 120

	star_array = {}
	letter = {}	
																																																									
	message = {}
	message.x_start =0 
	message.y_start =0 
	message.y_move = 117
	message.x_move = 128
	l = {
		a = 0,
		b = 1,
		c = 2,
		d = 3,
		e = 4,
		f = 5,
		g = 6,
		h = 7,
		i = 8,
		j = 9,
		k = 10,
		l = 11,
		m = 12,
		n = 13,
		o = 14,
		p = 15,
		q = 16,
		r = 17,
		s = 18,
		t = 19,
		u = 20,
		v = 21,
		w = 22,
		x = 23,
		y = 24,
		z = 25,
		exclaim = 26,
		quest = 27,
		dot = 28,
		dash = 29,
		one = 30,
		two = 31,
		three = 32,
		four = 33,
		five = 34,
		six = 35,
		seven = 36,
		eight = 37,
		nine = 38,
		zero = 39,
		space = 40
	}
	-- The Message!
	message.message = {l.c, l.o, l.s, l.m, l.i, l.c, l.space, 
		l.w, l.a, l.v, l.e, l.space, 
		l.r, l.i, l.d, l.e, l.r, l.space, 
		l.v, l.zero, l.dot, l.one, l.space,
		l.dash, l.space, 
		l.a, l.space, 
		l.six, l.five, l.z, l.e, l.r, l.o, l.two, l.space, 
		l.p, l.r, l.o, l.d, l.u, l.c, l.t, l.i, l.o, l.n, l.dot, l.space, 
		l.s, l.h, l.o, l.u, l.t, l.space, 
		l.o, l.u, l.t, l.space, 
		l.a, l.n, l.d, l.space, 
		l.g, l.r, l.e, l.e, l.t, l.z, l.space, 
		l.t, l.o, l.space, 
		l.dash, l.space, 
		l.a, l.m, l.i, l.g, l.o, l.s, l.space, 
		l.p, l.o, l.d, l.c, l.a, l.s, l.t, l.space, 
		l.dash, l.space,
		l.c, l.o, l.r, l.r, l.o, l.s, l.i, l.v, l.e, l.six, l.eight, l.zero, l.nine, l.space,
		l.a, l.n, l.d, l.space, 
		l.t, l.h, l.e, l.space, 
		l.r, l.e, l.t, l.r, l.o, l.space, 
		l.h, l.o, l.u, l.r, l.space, 
		l.p, l.o, l.d, l.c, l.a, l.s, l.t, l.exclaim, l.space, 
		l.a, l.m, l.i, l.g, l.a, l.space, 
		l.f, l.o, l.r, l.e, l.v, l.e, l.r, l.exclaim, l.space, 
		l.dash, l.dash, l.space, 
		l.a, l.n, l.d, l.space, 
		l.p, l.i, l.c, l.o, l.dash, l.eight, l.space, 
		l.t, l.o, l.o, l.space,
		l.o, l.f, l.space, 
		l.c, l.o, l.u, l.r, l.s, l.e, l.dot, l.space,
		l.v, l.i, l.s, l.i, l.t, l.space,
		l.u, l.s, l.space,
		l.i, l.n, l.space,
		l.t, l.h, l.e, l.space,
		l.w, l.e, l.l, l.l, l.space,
		l.a, l.t,l.space,
		l.w, l.w, l.w, l.dot, l.c, l.o, l.u, l.n, l.t, l.i, l.n, l.g, l.v, l.i, l.r, l.t, l.u, l.a, l.l, l.s, l.h, l.e, l.e, l.p, l.dot, l.c, l.o, l.m, l.space,
		l.a, l.n, l.d, l.space,
		l.w, l.w, l.w, l.dot, l.w, l.h, l.i, l.t, l.e, l.o, l.u, l.t, l.l, l.a, l.b, l.s, l.dot, l.c, l.o, l.m,
	}
	message.length = count(message.message)
	message.message_pos_x ={}
	message.message_pos_y ={}
	music(0) 
	
	y_flag = false
	gen_stars()
end

Notice how I abstracted the sprite positions with letter variables, this makes writing out your message “a little” easier.

You can download the full source code and Pico-8 cart at Github, here.

Thanks for reading, I hope that this demo will help your own endeavors in some way.

Leave a Reply

Your email address will not be published. Required fields are marked *