Tutorial: Custom Functions

August 25, 2020 tutorial 17 minutes, 24 seconds

Tutorial

Custom Functions

Overview

The youth of today are not like the drug-addled, yacht-rock-loving youth of the past: With their avocado toasters and their passion for Tic Tacs, today's youth are in a Fast Lane to the Future (tm). Here's one technique that will help get them there: Custom Functions. A feature of most contemporary programming languages, the custom function can be used in many ways. So let's learn more.

The Container Store

Know this: Custom functions are a lot like The Container Store, but for code.

The Container Store sells, well, containers: Stackable translucent bins that can hold all of that junk that is currently stacked up in your closet, your armoire, under your bed, and in your ex-boyfriend's basement. Container Store boxes are expensive and those flimsy-lidded bins will probably outlast us and our stuff by orders of magnitude -- but they stack! And the fit neatly on shelves! They afford us the illusion of a regulated, well-ordered, rational world, and at this point, you can't put a price on falsehoods like that.

Custom Functions: "Get Your Shit Together"

As you get a feel for them, remember that custom functions effectively do the same work as Container Store bins:

  1. Organize code into more manageable sections;
  2. Get the clutter of code out of your way;
  3. Make it easier to find things when you are looking for them;
  4. Make you less likely to write new code that does something your old code already did;
  5. Make a roomful of weirdly-shaped stuff actually stack up neatly.

As your code gets longer and more complicated, and your games become more compelling to play, Custom Functions will make your work a lot easier; they'll also make it easier when you share your code with others.

There are lots of different ways to use them: Don't worry about how other people use them for now, though: Instead, experiment with them and find how they make sense to you. Start with the Basics, below, and write some simple code to make use of those ideas. Starting this month, most of the code you write will make use of at least one or two custom functions.

Basic Implementation: Flow

We have talked at length about "program flow:" The way that conditionals, loops, and the _draw() function are part of a digital infrastructure that you build into the code you write. Remember that a computer depends on working out problems and carrying out processes as a series of instructions. Some things happen simultaneously, obviously, but usually programs "unfold" over time, like a game of chess, or a good conversation. One event leads to another; one decision means confronting a new set of choices; and so on.

So "program flow" describes how the computer follows the routes you create for it: Which variable should it use to store a new value; how long until the player's on-screen position is updated; when to stop playing the "Power Up" sound effect.

On a basic level, custom functions don't really change how your program works, but they do allow us to better organize how your program flows: Instead of just laying your program out in one big, undifferentiated lump of code, these functions help organize your program into bins and boxes.

Example: Geometric Pattern

Here's a bit of code that draws a whole bunch of circles on a grid, resembling (to my mind, any way) a pattern in a woven carpet. In order to make this happen today-ish, and not next week, I've taken some shortcuts with my code, so don't look too closely at what I've done. For example: Instead of showing my work and calculating all the important values from the start, I just use "magic numbers" -- I plug in numbers that make sense to me; that's not a great approach, but it can make my early attempts to "just get this to work" a lot more streamlined, and I can work out the math later, if it works. What's more, the code is literally at least 3X as long as it needs to be. But that's not a big deal for now: I just want something to show up on my screen. So let's make that happen.

Here's the first iteration of the program.

function _init()
end

function _update()
end

function _draw()
 cls(1)
 -- background circles, big
 for col = 1, 7 do
  for row = 1, 7 do
   x=col*12
   y=row*12
   circ(x, y, 12,4)
   end
  end

 -- now a second layer, 
 -- smaller circles
 -- also less frequently drawn
 -- (note that it counts by 2s)
 for col = 1, 7, 2 do
  for row = 1, 7, 2 do
   x=col*12
   y=row*12
   circ(x, y, 4,8)
  end
 end
end

It probably takes me a few tries to get it to work, some tweaking here and there, but after some experimenting, it works: I can run the code and there on the screen is part of a pattern that I want to keep working towards.

Since I'm tired of coding by now, maybe this is a good point to build out a little custom function, and spend some less-focused time reorganizing things.

Moving Code to a New Function

So I think to myself, "Look, this carpet thing is working... So let's put that code in a bin and get it out of my way."

First things first: We've got to give it a name. Naming matters, of course: Later, it would be hard to find this code if I were to store it in a custom function called function myKoolFunction(). And I will admit that at 3AM, some of my code features functions like function OMG_thissucks() and function whyAmIBothering(). But try to avoid bad choices like that: Every decision that SaturdayNightYou makes is a decision that SundayMorningYou has to live with: Programming means learning to care about Future You.

So let's give SundayMorningYou a break and create a bin call function carpet_pattern() and store our code in there.

But where exactly is "in there"? Almost anywhere in your program will work: Since we're naming the function, the computer won't just execute it once it accidentally bumps into it -- we'll have to demand that PICO8 run the code. So anywhere will do.

You can put the function before the _draw() function if you want, but for the sake of clarity, it is usually best to put it after the closing end of _draw(). Or, even better, put your new function in a shiny new tab all its own (press the "+" near the top of the screen to create a new tab).

In truth, it doesn't really matter: All of the code is in the same single text document. So do what makes sense, and be flexible when you discover a better approach in the future.

So here's the custom function, first. It is almost exactly the same as it was in _draw(), it just has a new name.

function carpet_pattern()
 -- background circles, big
 for col = 1, 7 do
  for row = 1, 7 do
   x=col*12
   y=row*12
   circ(x, y, 12,4)
   end
  end
 -- second layer, smaller
 -- note these are less frequently drawn
 for col = 1, 7, 2 do
  for row = 1, 7, 2 do
   x=col*12
   y=row*12
   circ(x, y, 4,8)
   end
  end
end

The one change I made: I left behind the first line, cls(1). I'll explain in a moment.

But first, since I've taken all that code out of _draw(), I have one last job to do: Tell _draw() how to get to the work I still need it to do. Here's what that looks like:

function _draw()
 cls(1)
 carpet_pattern()
end

Note: I don't write out the word function in the third line of code: I only use that when I'm "declaring" a function (which is how we let the computer know what to call our function in the first place). Note also that I still need those parentheses after the name, and that I cannot have spaces (and a few other characters) as part of the name.

Even so, isn't that better? Especially if the new function is safely stowed on a totally different tab, everything is so much neater. And it still works exactly as before.

One Last Thing

So why'd I leave cls(1) behind, in _draw()? Good question! Recall the earlier parallel with The Container Store. Dumping random stuff in $1000 worth of overpriced plastic bins certainly does get that junk off of your floor, but it makes everything more complicated later. Imagine that Jared drops by, all casual-like, and now he wants his dumb Nickleback hoodie back, which he told you could keep, but he's a jerk, and we've been over this. While you're wondering where you left it so he can be on his backwards-baseball-hat-wearing-way, he's admiring how neat your apartment has become lately. And he asks about the contents of all those opaque plastic bins in the closet. Dammit! The Nickleback hoodie is in one of those f--ing bins. It'll take forever to find it.

Again: TuesdayYou can make things a lot easier for FridayYou by imagining the trouble you're likely to run into in the time between.

Some Function-Building Rules of Thumb

  1. A custom function should probably only do one thing;
  2. That function should be built so that it will always be as useful as possible.

Yes, at the moment, you'll want to clear the screen every time you draw that carpet pattern, but what if you start drawing other things, too? If we decide to leave cls(1) at the top of the carpet_pattern() function, we're locked in to a weird habit: No matter what else our code is doing, and no matter the order in which we draw our carpet pattern in relation to other patterns on screen, we're committed to always clearing the screen first. That doesn't really make sense. So keep your functions organized in terms of the work they do -- and don't put two different kinds of action in the same function. They should be "discrete" -- that is, functions should stay in their lanes.

Boxes Inside Bigger Boxes

But that brings me to a second point: Since my little bit of code draws two different layers, I could have broken the carpet_function() down even further, creating two custom functions. In this code, I wouldn't really bother, but I could, and everything would work just fine. In the same way that breaking a long expression down into several shorter lines of code often helps me understand more clearly what I'm doing, compartmentalizing your code (boxes inside boxes) might be useful, too.

Here, I've broken that single function into two smaller functions. Notice that each has its own name, each function still begins by having its name "declared" as a function, and each is still closed-up with a final end terminator.

function carpet_base()
 for col = 1, 7 do
  for row = 1, 7 do
   x=col*12
   y=row*12
   circ(x, y, 12,4)
  end
 end
end

function carpet_top()
 for col = 1, 7, 2 do
  for row = 1, 7, 2 do
   x=col*12
   y=row*12
   circ(x, y, 4,8)
  end
 end
end

Again, that's 100% street legal. We just have to add another little bit of business to our _draw() loop.

function _draw()
 cls(1)
 carpet_base()
 carpet_top()
end

So now we call each function, first carpet_base, then carpet_top. _draw() dispatches program flow to the first, and then the second. Your player cannot see any difference, but it is a bit easier to understand.

But why stop there? _draw() isn't the only function that can call others: Functions can call each other, too. Later, this fact will actually make the world seem like a much cooler place than you ever knew it to be ("recursive functions"), but for now, it is an option that may or may not be appealing to you.

function _draw()
 cls(1)
 carpet_maker()
end

function carpet_maker()
 carpet_base()
 carpet_top()
end

function carpet_base()
 -- code
end

function carpet_top()
 -- more code
end

Everything we've reviewed here is the tip of an important iceberg called "abstraction." Abstraction is the process of taking a bunch of complicated code, bundling it up, and tucking it somewhere out of the way, so that the actual workings of the code become hidden or invisible.

So... How much abstraction is too much abstraction? That's an important question -- and there is, to my small way of thinking, no more important question that game designers, developers, and players can ask. But we'll get to that later. For now: Do what works, and try to get a sense for how you prefer to shape your code.