Anyone who's hung around any game design forum for any length of time has inevitably seen someone new drift in with a request that looks like this:
"I want to make custom combat engine that is totally awesome and also brews your coffee for you! How do I do it?"
This is awkward. Usually, no one will answer for a day or so, then some kindly soul will step in and gently suggest that the requester start with a smaller, less ambitious project.
But maybe you're past that hump, and you still have no idea how to move mountains with the power of scripting. In this article, I will give you the answer to that age-old newbie question. Are you ready for this?
The answer: Start small.
No, wait. Don't go. It's more than that. But that's the first step. Take a step back and look at the big picture. What do you need your game to do?
Let's go with the example of a turn-based strategy game. Could be tic-tac-toe, chess, or Fire Emblem. What does that engine do at a high level?
- The player takes a turn.
- If the game isn't over, the computer (or second player, if this is a two-player game) takes a turn.
- If the game isn't over, go back to step 1.
- Show some appropriate win/lose message.
plotscript, my game, begin while(game is not over) do ( player takes a turn if (game is not over) then (computer takes a turn) ) show win or lose message end
Hooray! You have created the skeleton of your fancy engine's script. Does it look familiar? It should – after all, it's almost a word-for-word transcript of the outline above.
But it won't compile, will it? We need to add a few things.
include, plotscr.hsd script, game is not over, begin end script, player takes a turn, begin end script, computer takes a turn, begin end script, show win or lose message, begin end
First, you have to include plotscr.hsd. This is necessary in pretty much every plotscript ever. Don't let it distract you; it has no special meaning here.
The rest of the code is empty scripts. In programming parlance, we call these stubs. Stubs are the key to simplifying your scripts. It may seem paradoxical, given that this method will eventually give you a much larger script file, but take a look back at the my game script above, and you'll see that it's much easier to understand what's going on when you break it down like this.
That's all well and good, but now we need to fill in those stubs. Stubs don't do anything by themselves, obviously. Let's start with player takes a turn. In order to write this script, we'll use the same outline method we used earlier. What happens when a player takes a turn?
Well, we need to get more specific at this point. After all, a turn of tic-tac-toe doesn't resemble a turn of chess or Fire Emblem. The process is the same no matter which one we pick. Let's go with chess, for the sake of familiarity and a game that you might actually play. Don't worry if you don't know all the rules of chess; that's not important for this example.
- The player selects a piece.
- The player selects a destination for that piece and moves it to another space on the board.
So let's put that in script form again.
script, player takes a turn, begin variable(piece) piece := select a piece select destination for piece(piece) end script, select a piece, begin end script, select destination for piece, which piece, begin end
Now, this is a little more complicated than my game. But not by much! Bear with me.
First, we have a variable. That's a way for the script to remember something; in this case, which piece the player picked. That variable is later fed into the destination script as an argument. An argument gives a script some context by allowing you to feed it some information.
Now, perhaps you knew all that, and maybe you're thinking, "More stubs! This really isn't going anywhere. It's just stubs all the way down." Calm down; we're actually getting somewhere here.
We have two new scripts. First, we need to figure out a way to allow the player to select a piece. We'll ignore it for now, but remember this for later.
Instead, let's concentrate on the second script. We described it in our outline like this: "The player selects a destination for that piece and moves it to another space on the board." How does that actually work in outline form?
- The player selects a square on the board.
- If the player's selection is not a legal move for the selected piece, then go back to step 1.
- If there is already a piece in the destination, then remove it from the game.
- Finally, move the piece to the destination square.
- The player moves a piece.
- The computer moves a piece.
- Go back to step 1 until the game is over.
The smaller issue is that it's actually incorrect. If the player puts the computer into checkmate, then the computer shouldn't get another turn. Of course, you'll easily realize the mistake after any amount of testing, but by that point, you've already lost one of the principal advantages of using this method: once you write a high-level script, you're done with it. You should never need to touch my game or player takes a turn at this point. If you do, it's because you forgot something in the outline phase.
The larger issue is that a less granular outline heavily encourages you to make bulkier scripts, which are harder to read and are more likely to need changes later on (which is a problem, since they're harder to read). Consider the following (mostly unmodified) script from Super Penguin Chef (minor spoilers):
plotscript, goodnight, begin variable(i) for(i, 0, 3) do ( # Restore HP/MP set hero stat(i, 0, get hero stat(i, 0, maximum stat), current stat) set hero stat(i, 1, get hero stat(i, 1, maximum stat), current stat) ) # Clear adventuring tags set tag(5, off) # adventured set tag(6, off) # beat up set tag(11, off) # restauranted # Special evening events switch(game day) do ( case(1) do ( show text box(227) set tag(7, on) # day two ) case(4) do (payment due := 500) case(9) do (payment due := 2000) case(14) do (payment due := 7500) case(19) do (payment due := 12500) case(24) do (payment due := 25000) case(29) do (payment due := 0) ) wait for text box fade screen out wait(20) game day += 1 total days += 1 switch(game day, mod, 5) do ( case(1) do ($string:temp = "Wednesday") case(2) do ($string:temp = "Thursday") case(3) do ($string:temp = "Friday") case(4) do ($string:temp = "Monday") case(0) do ($string:temp = "Tuesday") ) clear string(1) clear string(2) if (payment due) then ($1 = "${V9} Moneys due next Monday!") if (game day, mod, 5 == 3) then ( $1 = "There's a contest today!" if (payment due) then ($2 = "${V9} Moneys due tomorrow!") ) if (game day, mod, 5 == 4 && payment due) then ( $1 = "${V9} Moneys due tonight!" set tag(17, on) ) expand string(1) expand string(2) # Figure out a starting map - usually home switch(game day) do ( case(2) do ( teleport to map(2, 11, 9) ) case(3) do ( teleport to map(2, 11, 9) create npc(2, 10, 4, right) set npc direction(0, left) ) else ( teleport to map(1, 8, 6) ) ) suspend player show text box(222) wait(5) fade screen in wait for text box # Special morning events switch(game day) do ( case(2) do ( talk to bruschetta(157) ) case(3) do ( talk to bruschetta(279) set npc direction(0, down) walk npc(2, down, 4) wait for npc(2) walk npc(2, right) wait for npc(2) walk npc(2, down, 2) wait for npc(2) destroy npc(2) show text box(302) ) case(4) do ( show text box(276) ) ) wait for text box resume player end
Did your eyes glaze over? Maybe that's because there's way too much going on in that script. If I find out that there's a bug that's causing the player to wake up in the wrong spot, then I have a lot of script to go through before I can find it. Here's a version of that script that's much easier to read:
plotscript, goodnight, begin heal party clear adventuring tags special evening events fade screen out wait(20) game day += 1 total days += 1 reposition party for start of day suspend player show new day text box wait(5) fade screen in wait for text box special morning events end
That's much shorter. Now, if I find out there's a bug that's causing the player to wake up in the wrong spot, then I know to look in the reposition party for start of day script. The actual functionality hasn't changed, and everything I removed exists somewhere else, but now it's very easy to see what happens when the player goes to bed for the night.
With that in mind, let's go back to the select destination for piece script. Here's our outline, in case you've forgotten:
- The player selects a square on the board.
- If the player's selection is not a legal move for the selected piece, then go back to step 1.
- If there is already a piece in the destination, then remove it from the game.
- Finally, move the piece to the destination square.
script, select destination for piece, which piece, begin variable(destination) destination := select a square while (piece can move to square(which piece, destination) == false) do ( destination := select a square ) remove piece from square(destination) move piece to square(which piece, destination) end script, select a square, begin end script, piece can move to square, piece, square, begin end script, remove piece from square, square, begin end script, move piece to square, piece, square, begin end
And now we've stumbled on another advantage of this scripting method: you can reuse your scripts. Watch how we can create some scripts to fill in three of our existing stubs.
script, select a piece, begin variable(piece, square) while(piece == false) do ( square := select a square piece := piece on square(square) ) return(piece) # TODO: Make sure this piece belongs to the player end script, remove piece from square, square, begin variable(npc) npc := piece on square(square) if (npc <> false) then (destroy npc(npc)) end script, move piece to square, piece, square, begin walk npc to x(piece, square x(square)) walk npc to y(piece, square y(square)) wait for npc(piece) end script, piece on square, square, begin # Returns the NPC ID of the piece at the given location return(npc at spot(square x(square), square y(square))) end script, square x, square, begin # Given a square, returns its X coordinate end script, square y, square, begin # Given a square, returns its Y coordinate end
We've managed to reuse select a square, as well as the new scripts piece on square and square x/y. Even better, we have scripts that actually do something now. Once you get down to a low enough level, you're using the built-in plotscripting commands to actually interact with the game, but in small, manageable chunks. There's nothing confusing about the move piece to square script, and we'd be able to reuse it when it came time to do the computer takes a turn script.
Which we won't, at least not in this article. If you're going to make a chess game, make it two-player. (After all, you should start with a smaller, less ambitious project.)
And this is where we'll stop. Now, you don't magically know how to make your super combat engine that brews your coffee for you. But what you should know after reading this article is how to turn this:
"How do I make a custom battle engine?"
...into this:
"I'm making a custom battle engine and I'm trying to figure out how to determine if the hero is touching the enemy. How do I do that?"
And I can guarantee you'll get a better response with the second question. Ask enough of those, and you will know how to write that script. And when you do, you owe me a coffee.
No comments:
Post a Comment