Programming Tips #1 - If Statements
Programming Tips #2 - Making Progress
Programming Tips #3 - Design
Programming Tips #4 - Datums
Programming Tips #5 - Organization
Programming Tips #6 - Procs & Organizing Code
Programming Tips #7 - Comments and Whitespace
Programming Tips #8 - Loops and Ifs
Programming Tips #9 - The Map Editor
Programming Tips #10 - Variable Names
Programming Tips #11 - Bringing it Together
When you first start programming, the problem you're trying to solve is "how do I write code that'll do ______?". Initially you're happy just to get something working, who cares what the code looks like. But when you try to develop a complete game this indifference can become a problem. As your code gets messier and messier, the project becomes harder to work on - you're more likely to write code that has bugs and they become harder to track down and fix. Eventually it'll get to the point where you're wasting a lot of time, don't feel productive, and are more likely to stop working on the project.
As a more experienced programmer, instead of asking "how do I do ______?", you should start asking "what's the best way to do ______?". In this post we'll look at how different ways of calling a proc to do the same task can have very different amounts of complexity.
Many BYOND users have the idea that less code is better, that doing something in one line of code is always better than doing it in four lines of code. Here's an example that shows why this is bad.
Suppose you need to add an overlay to a player to represent the armor they've equipped. The overlay needs a vertical pixel offset that's set based on the player's race (ex: gnomes are shorter than humans, so for gnomes the overlay is shifted down). The overlay also needs an RGB color based on the player's faction. It also needs a layer based on the equipment slot (ex: shields are displayed over top of body armor). Oh yeah, you need to define the icon and icon_state too.
So, here's a single proc that does all that:
add_overlay(i, is, py, r, g, b, l)
// here's how you'd call it:
mob.add_overlay('armor.dmi', "helmet", 6, 192, 144, 64, MOB_LAYER + 2)
When you need to add an overlay you just call add_overlay(). That's nice and easy, right? Not exactly...
While the code seems simple (you're just calling one proc) and it kind of looks simple (you're specifying each value once, there's nothing redundant or unnecessary), it's really not simple. To call the add_overlay() proc you don't just have to remember what the seven parameters are, you also have to remember what order they go in. If you close your eyes right now and try to name the parameters in the correct order, you probably can't. If you can, go 48 hours without reading this article (easy, I know) and try again. Now imagine a project where you have 12 procs that are just as complex.
The problem here can be hard to see. It is very subtle. The code works - it functions correctly - so it doesn't look like there's anything wrong. But, as you continue to work on the project and as your short term memory fills up with other details, you'll forget the order of parameters to add_overlay(). You might realize you forgot them and have to find the proc to check. Or, even worse, you may not realize you got the order wrong and later on you'll have to figure out why certain overlays aren't correct.
When you're working on a game there are two important factors, time and motivation. This problem increases development time (you have to dig through the code to find add_overlay() every time you forget its parameters), but it's not that bad. So it takes 6 minutes to write a proc instead of 5, big deal. The problem is that it kills your motivation. This is why projects become hard to work on.
Most BYOND users can work effectively on a project for a little while. You've probably seen this a million times - someone starts working on a game and after three days they have some decent progress made, but then it stops completely. They could only work on the project for as long as their short term memory could manage the details. Once they had a lot of code with lots of proc names and parameters to remember, they couldn't remember everything. When the procs you write complicate the development of each additional proc you write, you'll eventually reach a point where you're losing motivation because you can't be as productive as you first were.
I said it was a subtle problem =)
(Note: The problem isn't just parameters, it's anything that forces the developer to have to remember too many details)
Luckily there are some ways to fix this. The first is to use named arguments. Named arguments let you assign values to parameters by name, not by order. For example:
world << "[first] [second]"
That proc outputs the first parameter you passed it followed by the second parameter. But, if you call it using named arguments, the order doesn't matter:
my_proc(second = "world", first = "hello")
That'll output "hello world" even though in the proc call, "world" came first.
This means that you don't have to remember the order of the parameters, just their names. Here's an example from the HUD Groups library. This is the proc that you call to add a screen object to a HudGroup. It has a lot of arguments because there are many properties you might want to set:
add(x, y, icon_state = null, text = null, width = null, height = null, value = null, layer = null, icon = null)
The names of the arguments are easy to remember. Icon, icon_state, and layer are all names of vars that DM uses. Text, width, and height are obvious. Value isn't as obvious, but the object you're creating has a var called "value" whose name you'd have to remember anyway.
Another thing to note here is that named arguments allow you to make better use of optional arguments. You can call the add() proc by just saying add(0, 0, layer = 5). If you didn't use named arguments, since layer is the second to last argument, you'd have to put null for all the parameters that come before it (ex: add(0, 0, null, null, null, null, null, 3)).
Named arguments aren't the only way to make things easier to use. You can make the proc look at the list of args it has received and figure out what arg value corresponds to what parameter. For example, suppose we're making a simpler version of the add_overlay() proc that takes an icon, icon_state, and separate RGB values, we could do it like this:
add_overlay(icon, icon_state, red, green, blue)
And you could use named parameters, but we can make it even easier than that:
var/icon, icon_state, red, green, blue
for(var/a in args)
icon = a
icon_state = a
red = a
green = a
blue = a
The proc knows that any value it receives that's an icon is for the icon parameter. Any value it receives that's a string is for the icon_state parameter. The numbers it receives are the RGB values (the first number is red, second is green, third is blue). This lets you call the proc almost any way you want (the only restriction being the RGB order, but that's easy to remember).
In both of these examples you're writing more code than the most naive approach. We're trading time for memory. It takes longer to write but is easier on your memory. While that's a worthwhile trade to make, there's still one problem with both of these methods. When you screw something up, you won't know until you run it. If you put the wrong name when using named parameters, you won't get a compile-time error. It'll cause a runtime error, but to get that you have to run the game and test it out. It's easier to catch and fix these problems if the compiler can catch them.
One way to get compiler errors and simplify how the proc is called is to use objects to wrap up all of the parameters. For example:
// to make an overlay:
var/OverlayOptions/o = new()
o.icon = 'armor.dmi'
o.icon_state = "helmet"
o.pixel_y = 6
o.red = 192
o.green = 144
o.blue = 64
o.layer = MOB_LAYER + 2
This is more than our initial one line of code but it's super simple. We don't have to remember the order of the parameters, just the names of things. And, because we get compiler errors when the names are wrong we don't have to worry about them too much - we can easily use trial and error. If you forget whether you called the var for the red value "red" or just "r", just try one. if o.r = 192 doesn't compile, it must be o.red = 192.