Reusable Animated Clouds For the Pico-8


In the process of creating scenery elements, in this case clouds, for a WIP Pico-8 game. I wanted to build a process that could be called at any point in the game and place a unique element within the scene. It quickly became evident that the concept of a reusable function could back fire on my idea of a programable design. This led me to the question: at what point does the processing of parameters and customization of new objects created in a prototype, out weigh a simple tween?

The original concept of unique clouds was scaled back, the weight and token usage didn’t warrant the features that were available for the potential number of times the function would be called.

In this code walkthrough, I’m illustrating how to implement clouds through a window in a section of the screen. With a few minor adjustments, the script could be modified to work full width or the speed adjusted.

Building the Clouds

Let’s start by creating a few base objects that will house the parameters for the cloud style:

cl01 = {
	current = 0, -- This is the unique timer for this cloud, each cloud object has it's own
	top_offset = -3, -- Off screen starting position for this layer of the cloud
	top_width = 5, -- Layer width
	middle_width = 9,
	middle_offset = -5,
	bottom_width = 4,
	bottom_offset = -2,
	max_size = false, -- Used as a flag to indicate the last animated layer has reached it's max size and should now start animating back.
	delay_start = 0, -- The start of the delay counter
	delay_end = 0 -- End of delay counter for this flag, once the counter reaches this value, the cloud will start it's animation.
}
cl02 = {
	current = 0,
	top_offset = -4,
	top_width = 5,
	middle_width = 9,
	middle_offset = -3,
	bottom_width = 4,
	bottom_offset = -6,
	max_size = false,
	delay_start = 0,
	delay_end = 200
}

Each cloud requires it to be it’s own unique object, along with width and spacing properties, you’ll notice the variable to house counter data: “current” and the delay vars, these need to be unique to each object.

Next, we’ll get into the core of the feature, the cloud_draw function. This function is called at 30fps directly from the special _draw() function or by a proxy function as I’ll be doing in this tutorial. There are two parameters that need to be supplied: cl_next and top. cl_next is the unique object or table we defined in the previous step and top is the vertical position of the cloud. We’re defining the y position from the draw function since technically y never changes.

function cloud_draw(cl_next, top) -- cl_next is the supplied, unique object i.e. cl01 or cl02. Top will dictate the vertical position of the cloud.
	if cl_next.delay_start < cl_next.delay_end then -- This counter will determine the start time of the cloud.
		cl_next.delay_start += 1
	else 
		if cl_next.current < 20 then -- Internal ticker, by adjusting the max value of current or the steps it increments by, you adjust the animation speed. cl_next.current += 2 else cl_next.current = 0 -- "current" has reached it's theshold so reset it for the next frame and then commence this frames animation cl_next.top_offset += 1 -- Move top, middle and bottom 1px cl_next.middle_offset += 1 cl_next.bottom_offset += 1 if cl_next.bottom_width > 3 and cl_next.bottom_width <= 11 and cl_next.max_size == false then cl_next.bottom_width +=1 -- Bottom layer hasn't reached it's max size so grow it's width 1px elseif cl_next.bottom_width >= 8 then -- Bottom is larger than max size so animate back.
				cl_next.bottom_width -= 1
				cl_next.max_size = true
			
			elseif cl_next.bottom_width < 9 and cl_next.max_size == true then -- Third stage in the width of the bottom layer makes this animation into a loop. cl_next.bottom_width += 1 cl_next.max_size = false end end --line1 for i=0,cl_next.top_width do -- We're using a for loop to render the width of each layer if cl_next.top_offset > 56 then -- By adjusting the max value, you can increase or decrease the position the cloud will reach before it returns to the start position.
				cl_next.top_offset = 0
			else
				pset(cl_next.top_offset+i, top, 7) -- draw the layer
			end
		end
		
		--line2
		for i=0,cl_next.middle_width do
			if cl_next.middle_offset > 56 then
				cl_next.middle_offset = 0
			else
				pset(cl_next.middle_offset+i, top+1, 7)
			end
		end
		
		--line3
		for i=0,cl_next.bottom_width do
			if cl_next.bottom_offset > 56 then
				cl_next.bottom_offset = 0
			else
				pset(cl_next.bottom_offset+i, top+2, 7)
			end
		end
	end
end

Before we move into the special Pico-8 functions, let’s set a proxy function to help keep _draw() cleaner. Also by deferring any draw actions, we can add conditionals at a later date to control game levels etc.

function draw_lvl()
	--window
	rectfill(5, 31, 45, 8, 12) -- Draw the window the cloud will travel in
	
	cloud01 = cloud_draw(cl01, 15) -- Call as many instances of clouds as needed, each with it's own unique object and vertical position
	cloud02 = cloud_draw(cl02, 19)
	
	-- walls
	rectfill(0,5,90,8, 15) -- I've cut the walls of the room into four parts to provide a mask for the window, notice the order each element is loaded
	rectfill(0,5,4,40, 15)
	rectfill(0,31,90,40, 15)
	rectfill(46,5,90,40, 15)
	
	-- Alternatively, you could use the clip function, the down side to this is anything rendered after the "clip" will also be cut from the renderer.
	
	rect(4, 31, 46, 8, 4) -- window frame
	
	-- Buildings
	spr(15,apartm.x0+39,apartm.y0+5,1,2,false) -- add in some background detail
	spr(13,apartm.x0+23,apartm.y0+5,2,2,false)
	spr(29,apartm.x0+5,apartm.y0+13,2,1,true)
end

You might notice the 4 rectfill functions that are drawing the walls. Together they leave an opening for the window and clouds essentially creating a mask for the clouds to travel behind. There’s an alternate way to limit the viewable cloud area and that is through the clip() function. The only catch with using clip is that anything that is called after the clip will also be clipped. So depending on the layout of your scene and the order of the layers, it might prove easier to use the method I’ve illustrated.

Finally, we reach the standard Pico init and loop functions, although _draw() is empty, we’ll keep it in place for the sake of completeness.

function _init() 
	cls()
end

function _update()

end

function _draw()
	cls()
	draw_lvl()
end

Feel free to download, modify or tear to pieces the Pico-8 cart from here.

Leave a Reply

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