2011/01/02 (Villainmad): Thinking Out Loud, Sequences, and Closed File Formats
So, with the new year,1 I'm going to be semi-periodically blogging about this kind of thing, both to let people know I'm not dead and as a "thinking out loud" exercise that helps me focus my thoughts by writing them down and exposing them to people in general.
Since I haven't actually done anything yet from an astronomical standpoint (where 1 A.U. = "completed game") except "getting a feel for C#," I feel like I'm still in a position where I can safely take a step back, look at what I've been planning, and ask myself questions like "Why am I going to do X?" and "How easy will it be to do X?" and "Would it be easier to do Y or Z instead?" and so forth.
I've got a case in point. Two of them, actually.
So, I have "Sequences" and I have "Functions," and the two are separate entities. The difference is that Functions can have return values, and Sequences can have "wait" statements (i.e. here's an example of an entire Sequence: "Spawn an enemy just off the left-hand side of the screen moving right at such and such a speed, wait two seconds,2 have the enemy stop and fire a ring of eight bullets, wait one second, have the enemy fly off to the right at double the previous speed while the bullets fly towards the player and remain in a circular formation, wait four seconds to give the enemy time to go off the right-hand side of the screen, delete enemy."). This led to the following series of questions:
1. How easy will it be to implement mid-Sequence "wait" statements?
I was worried that the answer would be "fairly difficult." See, you can't just "make it stop what it's doing" beacause you need to make sure that 1. it's actually stopping what it's doing (there's a number of ways to get this wrong), and 2. you want to make sure it doesn't stop anything else (the system function "sleep" would pause the entire program). I started to consider other questions:
2. Would it be easier to do away with the concept of Sequences, and be able to have everything as Functions, but with a delay-before-starting for Functions that don't have return values?
Even though it's more complicated to describe, it may or may not be easier to program for "Function A: spawn this enemy moving in direction X at speed Y, set function B to trigger two seconds later. Function B: if the enemy isn't dead, stop its movement, spawn a bunch of bullets, and then set function C to fire one second later. Function C: have bullets fly towards the player in ring formation, and if the enemy isn't dead, have it start flying off at double previous speed, and sets Function D to run four seconds later. Function D: delete enemy." than as one whole Sequence above. But whether it's easier than the other way depends on what method is used for the other way.
3. What's the practical difference between a Sequence with no "wait" statements and a Function with no return value?
First, a bit of background:
I did not invent the term "function"; it is one of the many (often programming-language-specific) names for "subroutines." A subroutine is a smaller portion of a computer program which is capable of operating solo, and which (with a few ifs and buts) can be called forth from the aether ("calling" is the actual term for this) whenever the program wants from any other part of the program, including itself. They generally take the form of NameOfSubroutine(InputParameters, SeparatedByCommas); a given subroutine must usually have a specific set of input parameters (say, three numbers).
Subroutines fall into two different categories: subroutines which do stuff, and subroutines which tell you something. (The technical term for the "something" is the return value.) The two are not mutually exclusive; many subroutines, after doing stuff, then return the results of that stuff.
Example: let's say I wanted to have a given type of bullet which I want every single enemy in the stage to be able to fire — say, a purple circle 10 pixels wide with a hitbox 4 pixels wide which does 5 damage when it hits the player, etc. etc. etc. So, I create a function which takes the location and starting speed etc. as input parameters, fills in all the blanks, and returns the unique identifier of the bullet in question, so I can get at it later.
Q. Why can't you have a function with both "wait" statements/a delay and a return value? A. Two reasons: First of all, when you want a function's return value, you want it right now. Let's say you have a Function called, I dunno, NumberOfBees(), and you wanted to check if it was greater than 6 at a given point in a Sequence. If it takes five seconds to give you the answer ... well, what's the Sequence supposed to do? Pause while it waits for the answer? Second of all, a subroutine can't do anything after you've gotten its return value. It's called "return" because it returns to the bit where the subroutine got called, see?
But, uh, anyway! Ultimately, no, since Sequences and Functions are both essentially subroutines, there is no practical difference, which is yet another reason to scrap the distinction between the two; just create a rule that goes "you can have return values or wait-statements/delays, but not both," the same way you have "you can't have anything after a 'return' statement."
4. Is there any compelling reason to "wait" statements, rather than delays?
I quickly realized that the answer is yes: variable scope.
Variable scope can be summed up with respect to answering the question, "Does this variable exist outside this particular part of the program?" My setup will basically use three levels of variable-scope: Sequence-level (or Function-level; variables which only exist within a particular Sequence, i.e. "the number of bullets we're going to make within this pattern"), stage-level (variables which only exist within a particular stage, i.e. the number of seconds since the stage began), and global (variables which persist between stages, such as the score and number of lives and the difficulty-level).
In the example above, there's nine things I'm keeping track of: an enemy, and eight bullets. If this were a single Sequence with "wait"s, it'd be simple enough to open it by declaring a variable for the enemy and an array (a variable which is actually a list of other variables of the same type, so that the precise number of bullets can vary by difficulty or whatever) for the bullets.
But if I was using four different functions, I'd need to find some way of getting the information between them. You can't do it directly with Sequence/Function-level variables, it's a bad idea to use stage-level or global variables because there'd be conflict if you wanted to have more than one enemy do that simultaneously, and using them as input-parameters for the next one just feels so ... messy.
It would be easy to code for "different functions for delays." But after I'd done that, it'd be tough to code with it.
5. So ... how would one implement wait-statements?
Okay, so if I'm doing my own file-format, I'm going to put each action in a Sequence into a queue, which is a collection of things in which it is relatively easy to add things to the beginning of the list, and also relatively easy to see and/or remove whatever thing is at the end of the list. It uses the "First In, First Out" system of Doing Stuff With Collections Of Things.
So then what I'd do is, I'd set it up so that every Sequence would try to do everything in its queue until it ran into a "wait" statement, which sets a timer, and on the frame after timer ran out (so that "wait(0)" would actually just advance it to the next frame), it would resume the queue where it left out. (Each Sequence would have a specific timer-variable, and every frame in which a Sequence exists, it would do "If the timer is 1 or greater, decrement it by one; if not, run through the queue until you hit the next 'wait' statement." The benefit of doing it that way is that you could still set a delay before it begins, if it doesn't have a return-value.)3
So, the answer to question 1 is, "if I make the game-format from scratch, not very difficult at all."
But one of the reasons the previous issue came up was because I was considering not making the game-format from scratch.
This train of thought came when I was playing Aquaria, and discovered that all the maps, scripts, and constants (recipes, etc) were in plaintext files, just sitting there in the Windows directories. Well ... the map-format was XML and the scripts were Lua. But the point is that the entire game was effectively human-readable.
I brought this up with Chaoseed (who, uh, is going in the comments here by the name John Evans), who said to me: "One theory of game design is...if people want to hack the game, they'll probably find a way, so why not make it easy."
So, I figure ... why not make it easy? Why not get rid of XNA's content pipeline, and load images and sounds and whatnot from ordinary .png files, then load Lua scripts (or whatever) for the levels? For one thing, it'll save having to recompile everything every five minutes, and makes the game easier to mod for those who want to get creative. Hmm ... official support for mods, there's an idea ...
Uh ... well, if I used Lua in particular I'd have to come up with a new method of doing the "wait" function, thus throwing out the entire previous issue.
Psh, complaining about that leads to the sunk cost fallacy, especially since I haven't actually written a single line of code yet. I've put more effort into writing this article, and that was mostly due to writing up all the stuff for non-programmers.
So, yeah, I think that I'm going to be doing things differently than what I originally planned: Villainmad will be made of things which are human-readable, and can even be modified if you wanna. Lord knows I'll find it easier to work with. After all, this is more of an experiment than anything else; I wasn't going to sell it.
1Insofar I started writing this on Wednesday.
2Technically, it will be measuring it in frames (most games have 60 frames per second as the baseline, as will Villainmad), but let's keep this simple.
3Incidentally, fractional timers will be allowed. Why the hell would anyone want to do that? Well, let's say you have a particular bullet-pattern which fires a miniature spiral of bullets over the course of one second, and you want it to launch between 30 and 120 bullets, depending on the difficulty. In the former case, there will be two frames between each bullet, so it will go "Okay, launch the first bullet right now, launch the second one with a delay of 2, the third one with a delay of 4, the fourth with a delay of 6 ..." and so forth. But with 120 bullets per second, it will go "Launch the first bullet right away, the second one with a delay of 0.5, the third with a delay of 1 ..." and it would effectively launch two bullets per frame.