Welcome to the New Book. If you're reading this, you probably read everything I write--or, possibly, you want to make your own computer games (or make your own games better). Over the coming chapters, you're going to learn how to do it... even if you've never programmed before.
If you're new to BYOND, you're in the right place. For you, here's a quick rundown on the system: BYOND is "semipro," meaning it's easy for a beginner or amateur to use, but it has enough power to enable you to create high quality games. BYOND can't make 3D games, but it works for everything else: board or card games, strategy, 2D action, RPGs, even text-based games. With its simplicity and power, the system allows an emphasis on what really matters about games: quality gameplay.
If you're not new to BYOND, is this book for you? Possibly. If you've read my beginners' tutorials, then you're already familiar with the teaching methods and speed used here, though the material of the ZBTs is left behind for new territory by about page 18. For the seasoned programmer, the most useful component of the book may be the "Adapt this game" section of each chapter, in which you'll find new ways of looking at some old themes.
This book starts off with step-by-step instructions on how to make your very first game, then launches into deconstructions of some familiar games--checkers, Uno, and Risk. With each game, you'll learn how to:
* Break the game down into actions you need the computer to perform
* Translate these needs into instructions the computer can understand
* Use the language of DM to program the game, and
* Tinker with the game's basic elements to create an original game of your own
...all with an eye for giving you the skills to come up with your own great ideas and turn them into reality. Once the games are deconstructed, you'll find more chapters on how to create a good user interface, how to test your games and fix bugs, and other tips. As soon as I write them.
1. Getting started with BYOND
In this chapter, we'll get you using the BYOND software to write code and make graphics. If you're new to BYOND--if you don't even think of yourself as a computer person--don't worry; everything in this first chapter will be explained step by step. (If you're old to BYOND, you'll find the chapter similar to the "ZBT2" tutorial available on the website.)
By chapter's end, you'll have made your very first game. It's going to be a board game, a bit like a solitaire version of Othello that starts out with stones already randomly placed on the board. (This happens to be one of the strengths of computer games: they make random or complicated setups easy!)
Sections in this chapter:
I. Creating the environment
III. Compiling and running
IV. Creating graphics
VIII. If and else
IX. Move- and win-checking
XII. Elements of play
XIII. Adapt this game!
I. Creating the environment
Go to your computer's program menu and open the Dream Maker program. Under the File menu, choose "New Environment..." and click on the right side of the input box, then enter the name "Testgame" for our first game. Click OK. When the next box comes up, you'll see it wants to create a new Code File with the same name. Click OK again.
Every game... or "world," to the computer... has to have an environment. For one world you may have many code files, or many sound files, and so on, but they're all part of the same environment--in our case, the Testgame environment. A code file (with the .dm extension) is where the actual programming is done. In the panel on the left side of your Dream Maker, you can see icons representing the environment and the Testgame code file.
A folder for this environment was created in your BYOND\bin\ folder, since we didn't specify otherwise. By default, BYOND is installed in C:\Program Files\BYOND (if you don't know where BYOND was installed on your computer, it's probably there). If you look in the bin folder, you'll see a Testgame folder. In that, you'll see Testgame.dme (the environment) and Testgame.dm (the code file).
At the top of Testgame.dm, our code file, put this line:
//This is a test game created by <your name> on <today's date>.
...where you replace what's in the <>'s with the appropriate information.
That's a comment. It begins with the double slash, which makes it turn grey in the Dream Maker for easy identification. You can say anything you want in a comment and it won't affect the code.
It's good programming practice to comment your code. A comment should be made as if you're explaining the code to another programmer, even if you never intend anyone else to see it; besides, forgetting how your own code works is a lot easier than it sounds. It's far better to have good commenting and not need it than to need it and not have it. (When you're still learning the language, comments also help to better remember what you just learned... rather like summarizing in your own words a chapter you're reading for class.)
Hit return a couple times to move down the page, and enter this code (indent lines by pressing the tab key):
We made our first verb. Verbs are the commands the player has access to in a game.
Verb: a command that's available to the player. When this command is typed or clicked, the computer will execute all the code that belongs to it.
We started off with "mob," which means that for the verb to work, a mob must exist in the world. Mob is short for "mobile" and is usually used to represent players and monsters. If we had started off with "turf," then the verb would require a turf to exist before people could use it. Under mob we put "verb." We used a tab before verb, which means it belongs to mob; mob is the parent. If we'd put "verb" all the way on the left, it wouldn't belong to anything.
Next is "say" and some stuff. By tabbing and putting it under verb, we're indicating that say is a verb. What goes on with that line is slightly more complex.
Let's take a closer look at those lines:
say(msg as text) //what the usr says is passed into "msg" as text
Verbs take arguments, as programmers put it. "Argument" is the programming term for "input." In a verb, this input comes from the player (as opposed to from another part of the program). You decide what name to label the incoming information. In this case, we called it "msg," short for "message." We could call it anything we want, but it's good to pick a name that's both descriptive and easy to type.
So, we don't know what input is going to come in here, but whatever it is, we know what we're going to call it. And we know what type of input it's going to be. Text. That is: not a number, not a mob, but text. Which is words, mostly. Text might contain digits in it--"I am 23 years old"--but that doesn't make it a number. A chunk of text is called a text string; that's what our msg will be.
A player who types "say" while running this game in the Dream Seeker will then be able to type any text he wants after it and hit enter. The text gets passed in to the say verb. Picture a player using your verb to say something like "Hello!" and imagine those words dropping right into "msg."
world << "[usr]: [msg]" //the world sees chatroom-like output
We put this line under the verb, indented, since it's the action that belongs to the verb. Those two less-than signs are called an operator. The << operator, in fact. Operators are symbols, usually placed between things, that tell the computer to do something specific. In this case, it's telling the computer to take the right side and output it to whoever's on the left side--the whole world, in our case. Picture the stuff on the right being funneled >> into the world.
"[usr]: [msg]" has quotes around it, which means the whole thing is a text string. When something within a text string has  brackets around it, that's what's called imbedded expression. It's like a nugget buried in our text string. When the code is executed (run by the computer), that text string doesn't get funneled into the world exactly as you see it above. Rather, any imbedded expression gets replaced. You'll see instead whatever the word in brackets refers to. The [usr] is replaced by the name of the user, or whichever player just used the verb. [msg] is, of course, replaced by whatever the user typed in.
III. Compiling and running
Look under the Build menu and choose "Compile." In the bottom panel you should see:
loading Testgame.dme saving Testgame.dmb Testgame.dmb - 0 errors, 0 warnings
If you don't, check your code to make sure it looks like what's above, especially the tabs. The system of using tabs to keep track of which code belongs to which verb or mob is very useful, but if you place a line of code at the wrong level, the compiler won't read it as you intended.
When code is compiled, it's altered into a format the computer can read. If there are no errors during this process, your .dm code file will also be saved. Now go under the Build menu again and choose "Run." BYOND will open the Dream Seeker (the client, or interface to games), perhaps after asking you to log in.
When your Dream Seeker opens, you should see the verb "say" in a panel named "Commands." Click on that or type "say" on the command line (the pink type-in window), then your message. And press enter.
You now have your own chatroom. Congratulations! If you host this program with the button in the lower-right of Dream Seeker, others will be able to join and they'll all have access to the verb.
IV. Creating graphics
Worlds that are wholly text-based can be made with BYOND, but most games will contain a graphical element, so let's explore how to add that. Under the File menu in the Dream Maker, choose "New..." and when the box pops up, choose "Icon File (.dmi)." Name it "square."
Now you're in the icon editor; you can see a new screen with two buttons, a palette and a camera. Click the palette to make a single picture. What you see next is probably self-explanatory; flood the screen with a color you like, then draw a contrasting border around the edges.
Next, make another Icon File (.dmi) and name it "stone." Click the palette and draw a "Fill Oval" in a color that will show up nicely on your square. If you mess up, the color in the very upper-left of the palette can be effectively used as an eraser. It's called the mask, and parts in that color will be transparent in your game.
Once you're done hit the "Back" button in the lower right. You'll see your little stone in the space that was blank before. Click the palette again. Draw another stone in a different color that would look good on your square, and go back once.
We just created an icon with multiple states. It's still one .dmi file, one icon, but it can look two different ways. Think of the icon as a picture book, in which you can flip to whichever picture you want and then show it to people. The whole "book" together is named "stone.dmi."
Double-click beneath the picture of the first stone. In the box, type in the name of the color you used for that stone, and hit OK. Now do the same with the second stone, naming it after its own color. We've named our icon states. Now when we want to display a particular state we can call it by name.
V. Defining prototypes
We made some (sort of) nice graphics, but they're not going to jump into the game all by themselves. Double-click on Testgame.dm, our code file. Beneath the mob code, put this:
obj //new obj prototype,
After that, add:
A prototype is like a blueprint. A blueprint of a house isn't the house itself, but you do use it to build a house. Our code here doesn't create squares and stones in the game. It's just the blueprint. We can make as many squares and stones from that one blueprint as we want. Finally:
world //we set one of our world's characteristics:
We'll take a look at what that did in a moment. Right now, go to the Build menu and compile again, which will save our files. Always remember to save regularly, and make backups of projects you intend to keep. (I had to rewrite half my original tutorial because I forgot to save, and my computer sensed this and obediently crashed.)
In order to place these things in the game, we also need a map. Under the File menu, choose "New..." again and make a new "Map File (.dmp)" named "Testgame." Same name as our environment. You'll be asked to set the map size; just click OK. When the map editor comes up, you'll see it's all squares. That's because we set the world's default turf to square, and we set square's icon, and the map automatically filled in everything with it. This saves us the work of putting in all the squares ourselves.
Compile again, and run. You'll see the squares... but you won't see any stones. We'll have to tell it how exactly to set up all the stones at the beginning.
VI. Working with procs
The word proc is short for procedure. Procs are pieces of code that make all the action happen in the game; you can recognize a proc because parentheses come after its name. Verbs are a type of proc, so you made one earlier without even knowing it. The DM language comes with many built-in procs to make coding easier. Since DM is also about power, we're allowed to override the built-in ones and make them do exactly what we want.
When your program is ordered to go perform a procedure, the relevant proc is "called," as programmers say. An object's New() proc gets called automatically whenever a new instance of that object is created. Since that's built-in, you don't have to code it into your game unless you want to. But we're going to override the world's New() proc so that it does something whenever the game is opened up. Here's our new world code:
world //we set one of our world's characteristics:
The ..() command tells the computer to go call the actual built-in New() proc, which is the "parent" of our version. This is not necessary when overriding every built-in proc, but with some it is very, very important. Many built-in procs perform heavy-duty functions, like allowing other players to connect to the game. The New() we coded is a child, in a sense, and it doesn't know everything the parent does. So we tell it to go ask its parent what else to do. This assures New() will do what we told it plus what it was built to do in the first place.
The other code above is relatively simple. We use var/turf/square/T as a temporary variable to hold any turf we want. When we say "for (var/turf/square/T in world)," we tell the program to "loop" through all the squares in the world, and temporarily drop each one into the T variable so we can do something to it. For each square, we create a new /obj/stone prototype, with (T) in the parentheses to indicate the new stone should be put there. Then the for loop continues and puts a new square in the T variable, till it's done them all.
Now, compile and run. Where are our stones?
They're not there, because the computer got confused. We gave 'stone.dmi' two icon states, and it doesn't know which one to pick. So we're going to tell it. In fact, we're going to make it choose right when stones are created. A great place to do this is in the stones' own New() proc.
obj //new obj prototype,
...where "" and "" are the colors of your stones.
The pick() proc is also built-in, but it doesn't belong to anything. It's called only when we explicitly call it. That's the case with all built-in procs that start with a lowercase letter. (You can find more in the DM Reference at byond.com.) Each is provided to perform a useful function that would be annoying to have to code ourselves. In this case, the proc randomly picks one of the things we put in the parentheses.
You now have a chatroom in which stones get set up in a random pattern each time the game is loaded. Save All or Compile again now, and run the program if you like!
It's time we learned to move these stones around with a mouse. But before we can pick up stones, we'll need a place to "hold" them while they're not on the board.
The characteristics of mobs and turfs we've been fooling around with (such as "icon" and "icon_state") are called built-in variables. A variable is a place where a bit of data is stored. It's called a variable because the info it holds can vary. (The DM Reference lists which variables are built-in.)
We need to make a custom variable for our own special purposes. A variable to hold the stones we'll pick up, so they don't disappear forever once we take them off the board. Make an addition to your mob code, below "mob" and above "verb":
We just defined a new mob variable. It's similar to what we did with var/turf/stone/T, except this one is permanent, so to speak; it's not inside any proc, so it won't disappear when the proc finishes its work. It belongs to mob and it'll be available to us anytime. The way we declared it indicates it's meant to hold a reference to not just anything, but instances of the obj/stone prototype.
Now to be able to pick up stones. Add this before our obj/stone's New() proc:
Click() //overrides its Click() proc
Click() is another built-in overridable proc, like New(). The built-in version doesn't do anything at all, but it will be called automatically whenever an obj/stone is clicked, so we make use of it. The first thing we have the Click() proc do is set the clicked stone's location, or loc, to null, meaning nothing. Our stone has been banished from this mortal plane. But we'll keep track of it by assigning it to the usr's holding variable. The usr is whoever clicked his mouse on the stone; src means the source of the proc, which is the stone.
1. I've put comments on almost every line in our Testgame program, but that's neither expected nor desirable in normal practice. Just put in enough commenting that another coder reading your stuff would be able to figure out basically what it does. Or exactly what it does, perhaps, with very complex code.
2. Note how we give our variables names that relate to their purpose. I strongly recommend this. Make a variable's name as descriptive as you can without making it a pain to type repeatedly.
3. When we declare a variable from within a proc, it exists only in that proc. Once the proc "returns," or finishes running its code, the variable goes poof. If you want another proc to use that variable you must pass it along. You'll learn more about that later.
4. If you recall, we have a line setting the stone's icon_state inside obj/stone's New() proc. If you took that out of the New() proc and put it right below the stone's "icon = 'stone.dmi'" line, you'd get an error:
Testgame.dm:13:error:= :expected a constant expression
That's because if something is unknown when you compile... such as what a random choice will turn out to be... you have to put it inside a proc.
5. I've had you call the parent of both built-in procs we overrode, Click() and New(), while stating that it isn't always necessary. It isn't, but it also doesn't hurt--unless you don't want the parent to perform its usual function, but that's rare. The idea is that even when a particular coding convention isn't always necessary, it's best to make a habit of doing it all the time... like commenting. Because if you don't get in the habit, chances are, the times you'll really need it will be the times you'll neglect to do it. Good coding habits may take more time in the short-term, but in the long-term they can save you hours of frantically searching for what just went wrong.
VIII. If and else
Run your game now and you'll see that you can indeed pick up stones by clicking on them... that is, assuming everything is coded as in the book. The problem is that you can't put any stones down. Let's learn how to do that now. Here's our new obj/stone Click() proc:
Click() //overrides its Click() proc
Now to dissect that.
Thanks to our well-named variable, that's pretty easy to understand: if the usr is holding a stone, execute the code in the "if block." Which is...
usr.holding.loc = loc //puts held stone on clicked stone's square
Here we put the held stone down where the clicked stone sits.
usr.holding = null //makes it so usr is no longer "holding" anything
Last in our if block, we make usr.holding equal nothing, so the program doesn't think we're still holding what we just put on the board. Very important.
And as you might have figured out, this is what the program will do in the chance our if statement fails. That is, if the usr isn't holding a stone. Compile and try it out!
IX. Move- and win-checking
So far, we don't have much of a game. Here's where we take our first real steps beyond learning programming commands to truly making a game. We're going to make it so when you put a stone somewhere, it turns all the other stones in its row and column that same color. The object will be to try to turn the entire board the same color. That's not a very challenging game, so we'll make the player have to do it in a limited number of moves.
The first part of designing a game is figuring out what tasks you'll need the computer to accomplish; next is deciding what you'll need for procs and variables. The next chapter focuses more on this aspect of designing a game, but we'll take our first look now.
What do we need the program to do? Let's see what rules we just set.
Putting down a stone will make stones in its row and column the same color, and
The player has to make all stones the same in seven or fewer moves to win.
As you progress through this book, you'll come to know just which procs are useful for which tasks. For now I'll reveal that we need (1) a proc to change the stones' color; (2) one to check if all stones are the same color; (3) a variable to keep track of the move number; and (4) another proc to see if the maximum move number has been exceeded. Let's make those. First, the easy one:
In keeping with our alphabetical order within blocks of similar code, put that above the var/obj/stone/holding variable. We just need to make sure the move number gets increased when it's supposed to. That's when a player puts a stone down. So under "usr.holding.loc = loc," put:
usr.moves++ //increments usr's move number
Like the comment hints, the ++ means "increment," and it increases the variable by one. Mob/moves is null (it has no value) to start with, and nothing plus one is one. Since the moves variable was declared outside a proc, the computer will even remember whatever we do to it.
Next we'll work on changing and checking stone color. We're going to have to make brand new procs for that. Here's one way to do it:
These are very short procs, but there are some new and complex things going on in them. Let's take a look.
These procs won't belong to anything, so we indent them under the word "proc," which is all the way on the left, indicating it's on its own. Remember mob/verb/say(msg as text)? That was a verb that took an argument, or input. In that case the input was text, and it came from the usr. This time, the input will be a reference to a stone. We're going to call it "thestone" to keep track of it, and when we call this proc, we'll have to remember to "pass it in."
for (var/obj/stone/O in world) //loops through all stones
You've seen a "for" loop before, but the next line may be a mystery. The || means "or." The double-equals operator, ==, is what you use when you're not making something equal something else, only checking if they're equal. What the line is doing is taking a look at, or evaluating, each statement on either side of the || and determining if either one or the other is true. If those conditions are met, the if block gets executed.
O.x and thestone.x refer to their horizontal coordinates, and O.y and thestone.y to their vertical coordinates. The proc looks at each stone in the world and if its x or y is the same as the x or y of the stone that was passed into the proc, its icon_state is made the same as that stone. (Since the icon_state variable effectively holds the stones' color, we won't need to make a separate "color" variable for the stones.)
return ColorCheck(thestone.icon_state) //returns whatever ColorCheck() returns
Right after we've changed the stones' color, we want to check if they're all the same, so we'll know if the player has won. So we call the ColorCheck() proc. All it takes to "call" a proc, or tell the computer to run it, is to use that proc's name and parentheses outside of where it was first defined. In this case, we're inside the ColorChange() proc. The computer is going to call ColorCheck(), take whatever ColorCheck() returns to it, and return that to whatever process called ColorChange(). This is a good example of how the different parts of a computer program can all refer to each other. In very complex programs, one proc might consult another, which will consult another, and so on in a chain of dozens of procs... then the last one in the chain will return its answer to the one before it, who returns an answer to the one before it... and so on until the original proc gets its answer and can proceed.
But ours is a very simple program. We'll just need these two procs. For convenience, we pass into ColorCheck() the color that stones were just being changed to. That way we won't have to guess what color the player is trying to turn all the stones.
ColorCheck(color) //takes a color as an argument
Here's the other proc. Now, technically, we could have put all this information in one proc, but it's good to separate different tasks into different procs. Nice, compartmentalized code is easier to work with. You'll see more examples of this as you read the book.
We perform another for loop here, this time checking to see if each stone is a different color than the color that got passed in (!= means "does not equal"). If O.icon_state checks out to be different from the color we're looking for, that means there's at least one stone the player hasn't converted; and if there's at least one stone left, then he hasn't won, so we return 0 to ColorChange(), which promptly ends, having nothing left to do.
But if the ColorCheck() proc manages to loop through every stone in the world without getting to one that's a different color, that command in the if block will be skipped, since the conditions haven't been met. So the execution of the program will make it all the way down to the line that returns 1.
If you're getting an idea of how our new procs work, great! If not, you're in good company. Many programmers find it difficult to wrap their minds around code as it's written on the page. If that's you, don't become too frustrated as you encounter the boxes of code throughout this book; it's perfectly fine not to understand big blocks of code when you read them. What will help your understanding is putting in hours of getting your hands dirty with Dream Seeker--and trying to visualize the code, especially with tricks like flow charts.
I mentioned that returning 0 when a non-matching stone is found is one way to check for a win. An alternative is to loop through every stone, tally the differently colored, and after that check and see if any odd stones were recorded. That'd require us to define variables to keep track of the tally. Such a method would be required for some kinds of games, but it's not for ours. Coding it as simply as we have is an example of what programmers call "elegant" code. Just by seeing the word chosen for such simple code, you can tell it's considered desirable. If there are two ways of doing something, it's best to choose the simpler... if nothing else, simple (or elegant) code is less likely to have something go wrong with it.
Now to make our game a game, we need to set a maximum move number. How to choose? If you were a game designer by profession, you might get some testers to run through the game a few times and see how many moves they took to win after, say, five practice games to build up some skill. But we'll be cheap and just settle on the number seven for now.
We need a proc to check if the maximum move number has been exceeded. You can put this below the ColorCheck() proc:
I didn't comment this one since by now you can probably tell what's going on just by looking at it. Here we have math right in the imbedded expression. You'll also see the \s text macro, which is provided in the DM language. It looks at the last imbedded expression and, if it was plural, adds an s to the word.
But we've just made three brand new procs... and only called one of them, ColorCheck(), in our code. If we want to use the others we've got to put in code to call them too. How about MoveCheck()--when would be a good time to check whether the player's used up all his moves? After a move, of course; to be more specific, after an unsuccessful move, since if he wins on the last move it doesn't matter if he's used them all up. So how do we know if a move was successful? We call the ColorChange() proc, which calls the ColorCheck() proc. All of this will take place when the player clicks on the board. So our new Click() proc is:
Click() //overrides its Click() proc
See our new if/else? In that if statement, we check to see what ColorChange() returns, which--since ColorChange() returns the result of ColorCheck()--is 1 if the game is won, and 0 if it's not. We use a little shorthand there; note we didn't say "if (ColorChange(usr.holding) == 1). When there's no ==, or != or anything like that inside an if statement's parentheses, the program just checks to see if what's in there exists. And since what's in there is a proc, it's not really checking to see if the proc exists, but whether what it returns exists. That is important. If the ColorChange() proc returns 1, well, 1 exists. If it returns 0... 0 is nothing, so 0 doesn't exist. A statement of "if (1)" will evaluate as true, and "if (0)" as false.
The other change is the new else statement, which calls Movecheck() if the game hasn't been won. See how the new if and else reside within the original if block? You can call that "recursive" or "nesting" code, and it allows you to make very complex structures, whole trees of decisions like a flowchart for the computer to follow. But since it's not laid out graphically like an actual flowchart, such complex code can also be confusing to work with. When nesting, make absolutely sure you have things indented where they're supposed to be. An else must always be at the same indentation level as its partner if.
With the appropriate things in the various <>'s, now all our code together should look like:
//This is a test game created by <your name> on <date>.
At this point, Save All or Compile again.
Here's a little-known fact about programming: most of the time spent writing a program isn't in actually writing it, but in debugging and polishing it. Once you're used to BYOND, you can write playable games in just a few hours. Making them fit for consumption, however, usually takes days or weeks. Why is that?
One reason is that people don't think like computers. Even an expert programmer doesn't see the world the same way a computer does, so we're always going to miss things the computer needs in order to run our game the way we want it to.
The other reason is that designers don't think like players. Programming teaches some excellent lessons in psychology, one of which is that we really do see what we expect to see, even when our expectations are unconscious. Knowing how our game is supposed to work tends to make us blind to all the things that could be wrong with it. Imagine playing our game. Can you spot the problem?
Now try actually playing the game. Wait until you've won or lost... then keep playing, just like a user might. Oops! We need to make the game stop when it's over!
What we have is a runtime bug. Our code all checks out, looks great... but it doesn't do what we want (or what we unconsciously expected). This can be very frustrating. The good news is that fixing bugs is like any other kind of problem-solving: it's very satisfying when you succeed. One of the benefits of BYOND is that there's already a community in place to help test your games and point you in the right direction when you have a coding problem.
But before calling in the cavalry, let's see what we can tackle on our own. It's important to take the time to think through what's going on and give your own brain the benefit of the doubt by trying to think up some solutions. Unfortunately, it takes some experience to be able to think up programming solutions--so for now, forget about code and just think real-world logic. It might sound silly, but: How do you keep people from playing games they've already won?
Suppose you've just finished up a rousing game of Othello with your little brother, George. You've won (of course), but George insists on continuing to move pieces around on the board. How do you stop him?
I can think of a couple ways to do that: physically not letting him move anymore, and taking away the game (or the pieces). And both of these solutions would happen to work for a computer game too. Which one you would choose for our game depends on your own preferences. Do you want people to be able to show off the board once they've finished? Maybe take a screenshot? Or do you want cheap and easy?
Since this game isn't going to be on the top of the popularity charts, let's go cheap and easy. We'll get rid of all the stones on the board once the game's over (take that, George). So make a new proc:
That comes before ColorChange() in the alphabet, so put it above. Now we need to call it when the player has won or lost. The code for wins and losses happens to be in two places, so we'll need to call CleanUp() in two places:
usr << "You've won the game in [usr.moves] moves!"
usr << "You've used up all your moves. Game over!"
Now try our new game.
Whoops! If you typed in the code as shown, you should have seen something like this when you went to compile:
loading Testgame.dme Testgame.dm:21:error:Cleanup:undefined proc Testgame.dm:70:error:Cleanup:undefined proc Testgame.dmb - 2 errors, 0 warnings (double-click on an error to jump to it)
What happened? Even if you know what that error means, you might still miss it: we misspelled CleanUp both times we called the proc, and DM is a case-sensitive language. The uppercase U counts.
This sort of error is called a compile-time error. The compiler's (that is, the Dream Maker's) idea of an error is sometimes different from our own--for instance, it sees this as a case of our trying to call a "Cleanup" proc we never made, but we see it as misspelling a "CleanUp" proc we did make--but compiler errors are generally easier than runtime bugs to fix. You can double-click on an error message in the Dream Maker to go to that line.
Compile and test. And we've made a game.
You can do these exercises for practice, or simply think about them. If you feel able to complete each, you're on your way to a good understanding of basic programming concepts. Can you...
1. Output instructions when a player clicks on a stone?
2. Re-set the stones on the board when the game is over?
3. Allow stones to be placed only on different-colored stones?
4. Look up mouse_opacity in the DM Reference online, and use it to make your game smoother?
5. Figure out why it can sometimes look like no stone was picked up, and think of more than one way to fix it?
XII. Elements of play
You've done your hard work; now it's time to take a breather. In the "Elements of play" chapter sections, we'll be looking at what makes a game what it is. What makes chess different from checkers? What makes Risk different from chess? What parts of Tetris make it so addictive? This kind of analysis can make you a better programmer and game designer in three ways:
1. Being able to see the concepts behind a game will help you break it down into something programmable.
2. If a game you design turns out to be not as much fun as you thought it would be, you'll be better able to see what to change.
3. As you learn the concepts, you'll see more ways to put them together, and come up with more original game ideas yourself.
So let's start with our test game. If you had to describe what makes the test game what it is, what could you say? Not the rules of the game, exactly, but the things that make it similar to or different from other games? Here are some options:
It's a computer game.
It's a board game.
It has a board that's:
--Ten by ten
It has pieces that:
--Are set up at the start of the game
--Are randomly set up
--Are placed on every square of the board
--Come in two colors
--Can be moved wherever the player wants
It has turns.
To win, you have to:
--Turn all the pieces one color
--Do it before 7 turns are up
The game is moderately challenging.
Each session lasts a minute or less.
One of the greatest reasons behind the mediocrity of so many movies, books and even computer games is elements' being put in without the creators' thinking about how (or whether) they added to the work. Nobody stopped to ask, "How does this romance drive the plot forward?" or "How does this battle make the game feel more real?"
To keep from falling into the trap of accidental elements that don't add anything to your game, it's good to get in the habit of noticing each thing you put in and asking yourself why you chose to do it that way. For this game, obviously, the answer is "because the book said to," but when working with future games you may find yourself naming reasons of everything from "I wanted to make a game with shotguns because they're cool" to "I was thinking of Diablo when I did it, even though I decided later not to make it anything like Diablo" to "I have no idea." Each motivation you identify gives you a chance to escape from making a game like everyone else's and move toward making a game that's truly unique or groundbreaking. Okay, so shotguns are cool. What do they add to this particular game?
And now that we're more conscious of our own game's characteristics, how can we adapt them to make our game all it can be?
XIII. Adapt this game!
A brilliant way to create your own game is to take an existing game and ask, "What if this element were changed?" So let's look at one of our choices, the size of the board. The ten by ten formation was chosen for the heck of it; it might not, however, give us the best gameplay. Thankfully, when we coded the Testgame, we unwittingly made it very easy to change the size of the board, all because of how we set up stones. Rather than telling the computer to set one stone at the coordinates 1,1 and the next at 1,2 and so on, we just tell it to loop through every square in the world, setting a stone on each. This means that changing the size of the map won't "break" the game.
Double-click on the Testgame.dmp file on the left side of the Dream Maker. Under Options, select "Set Map Size..." and experiment with making the map larger or smaller, even rectangular, then playing again. You can also easily adjust the maximum move number to make the game challenging at the new size.
Another element is that our pieces are randomly set up. We can change that too; one simple way to set pieces up ahead of time is to put instances of our prototypes right on the map, in the .dmp file. In the map editor, navigate the left-hand tree to find and select "stone." Then under the Object menu, select "Generate Instances from Icon-states." You'll see stones of both colors pop up in the Object box. You can select them and add them to the map in any configuration you like.
Then return to your code and "comment out" the "new /obj/stone(T)" line in world/New() by putting a // in front of it; also comment out the line in the obj/New() proc that picks a random icon state for new stones. Now the computer will ignore those lines. Compile and test your stone setup. Experiment with playing it to see the fewest number of moves you can change all the stones in; then alter the 7s in the MoveCheck() proc to that new number. You now have a puzzle that will challenge your players.
And you can make multiple levels to the puzzle--conveniently, maps can have multiple levels, accessible through the z coordinate variable. Visit "Set Map Size..." again and change the z to 2, and hit the right-arrow in the Coordinate box to edit level 2.
We'll need new code for our budding multi-level game. Where it says:
usr << "You've won the game in [usr.moves] moves!"
Change it to...
usr << "You beat the level in [usr.moves] moves!"
We'll need a new variable for the level. At the beginning of your code, make an unattached, or "global," variable:
var/level = 1
We'll also have to make alterations to ColorCheck() and ColorChange(), which check or change the color of all the stones in the world. We want it to only change those stones on the same level as the player. In the ColorCheck() and ColorChange() procs, replace the word "world" with "view(10)." (If your map has an x or y larger than ten, put that number in instead.) View() is a built-in proc that returns a list of objects in range that are visible to the user, and it's very handy.
You can even set a different number of maximum moves for each level. There are several ways to do this. One way is to alter MoveCheck().
Using switch is a bit like using a bunch of if statements, but more efficient. What's in the parentheses after switch (in our case, the level) is looked at only once by the computer, and then it's compared to what's in the parentheses after the ifs. It allows you to make long lists of ifs that are clean and easy to read. You can think of it as saying, "See what the 'level switch' is set to. If it's 1, do this... If it's 2, do this..."
Now you have a real puzzle game. If you give it the right amount of challenge, in fact, this game could become a popular diversion for players. It requires only one more thing: polishing.
Polishing has little to do with gameplay but everything to do with appeal. Players often make snap judgments about whether to try a game (or stick with it for more than a minute) based on things like whether the graphics are clean, whether the messages are spellchecked and make sense, whether adequate instructions are available... in short, how professional the game looks.
It may be surprising in this age of graphic development, but the most important aspect to getting BYOND players to stick with your small game is actually the third: whether adequate instructions are available. If a player can't figure out what the object of the game is supposed to be or how to start playing, the quality of the graphics hardly matters.
Put the game objectives right up front, where players will be able to see when they log in. "Welcome! The object of the game is to turn all the stones the same color. Click on a stone to pick it up, and drop it on another stone to change the color of that row and column. Try to beat all 10 levels!" (In the next chapter, you'll learn about the Login() proc, which will provide a very useful tool; you can also check out the DM Reference online and learn about it on your own.)
To make a truly polished game that's a joy and not a pain to play, you'd need a little more than that. Can you also figure out how to:
1. Display a message telling the player the maximum number of moves for the level?
2. Create a password system in which players receive a new password after completing a level, and can use a "password" verb to jump to that level?
2. Look up the world proc Repop() and use it to reset the board so the player doesn't have to reboot the game in order to start the level over?
You'll be learning more ways to polish as you progress, such as adding sounds, buttons, and other interfaces. In the following chapter, we're going to take a look at how we'd program a game you're already familiar with.