Download the related program
Welcome to the second installment of the new RPG tutorials. Hopefully by now, most of the rust has worn off... and I'll be able to provide you all with a much better tutorial.
If you missed the previous one... It showed you how to write a scripting engine for your RPG. If you don't see it around here somewhere, ask zkman, I'm sure that he still has it. :)
This time around, I'll show you how to write a semi-active battle engine!
Before we start, let me point out that all of the code used in this tutorial is available in a zip file, along with some graphics, which demonstrate the engine in action. You can download it now, or read the tutorial first. If you're new to this, I would suggest reading the tutorial, as the code is not commented.
NOTE: This tutorial is aimed at a novice QB programmer. If you're a beginner, it shouldn't be too tough to figure out, But you may wish to brush up on your coding skills first.
Setting it all up.
DECLARE SUB GetHandLocation ()
Next, we must dimension our arrays, set up our global variables, and our constants as well. The first constants, are true and false. We will be using these for our 'flag' variables, to see check for certain happenings and tell the program to comtinue doing something if they are false, or to do something else if they are true.
CONST True = -1, False = NOT True
Next, we dimension the arrays we will use in the engine. These are needed to store our graphics. For this engine, there a four different enemies, and all are 32x32 in size. The two player characters are also 32x32, there are also three frames of animation for them. Next, our hand pointer is one 16x16 sprite. Finally, we need to allocate two arrays to hold parts of the graphic background which our sprites might be put over. This way, we can get what's behind the sprites, put or sprite on the screen, then, restore the background when we're done. Finally, we will also allocate space for masks in our sprite arrays.
Basically, This is the calculation I used to determine how big each array would have to be:
((SpriteXSize * SpriteYSize / 2) * NumberOfSprites) - 1
If you are not familiar how this all works, I would suggest reading a tutorial about sprites and graphics in SCREEN 13. It would help you understand more of what we're doing here.
DIM SHARED Hand%(258)
Next, we need to declare all the variables and dimension all of the arrays, which we will be using for our data. Basically, we need space to hold the players stats and enemy stats. Below, are the stats I deemed necessary for this engine to work. It's a good idea to give everything a name which will tell you what each variable is for; Ie. PlayerHP% is a much better variable name than, say A1% to hold the player's Hit Points.
DIM SHARED PlayerName$(1 TO 2), PlayerAlive%(1 TO 2), PlayerType%(1 TO 2)
Next, we'll declare arrays to hold the X and Y location of our hand pointer.
This will come in handy (No pun intended... really...) later.
DIM SHARED HandX%, HandY%
Now, we will initialize the random number seed. What is this? Well, we need the ability to generate random numbers, so that player and enemy damage isn't always the same. This line tells QB that we will be doing so later.
Okay... Now we're more or less set up. The next parts of this tutorial will deal with individual parts of the engine. It is recommended that you read all of them carefully, as they are all intertwined closley together.
The rest of the main module.
Well, we have to tell QB to load our initialization routines next. To do this, we just call them by name like so:
Once we're initialized... Let's switch to screen 13 (320x200 resolution video mode, with 256 colours if you don't know) and call our main Battle sub to begin the fight:
SCREEN 13 Battle
Well... The battle's done. Now, the engine reverts to text mode and displays a short message and waits for a key press. After this, it exits. This last bit of code, you should leave out.
SCREEN 0: WIDTH 80
The main battle sub.
Our subroutine starting code... QB will automatically do this for you, when you create a new sub, so you may skip this part if you wish.
First, we must initialize our battle stats, then tell the program to draw the battle screen. It is done by calling the DrawBattleScreen. Note that we also pass a value of 1 to it. This way, the sub will know to draw a starting battle screen, and not an ending one.
Now, we begin our main battle loops.
These next lines, will calculate the agility of player and enemy characters that are still alive. This sub will take the agility of each character, and add it to a total every time it loops. Once a character's total is greater than 99... That character will be allowed a turn. Also, some of the player part of these lines, draws the little red and yellow status bar, which shows how much longer a player has to wait until their next turn.
FOR I = 1 TO 4
Here, we have a little timer delay. This is to slow the battles down to a playable speed. If the delay wasn't here, you would notice that everyone attacks almost at once! The battles wouldn't be much fun then. You can raise this number to slow a battle down, and lower the number to speed it up.
LOOP UNTIL GoThere% OR GoGo%
A character can now take a turn... If GoThere% is true, this means that it's an enemy's turn.
IF GoThere% THEN
Next, we check which enemy is allowed a turn, and if they are still alive or not... Just in case. Once we have found out, we draw the enemy mask on it's location. This will draw the enemy in all black, so the player knows which enemy is about to attack them.
FOR I = 1 TO 4
HitPlayer% = INT(RND * 2) + 1
After a player has been selected, these next lines check if that player is alive. Then a calculation is made based on the enemy's strength, the player's defense, and the help of a few random numbers, to determine how much damage the enemy has done. The random numbers are used just to add some variety to the damage. Instead of an enemy doing, say, 5 damage on a player all the time, they may do 3 sometimes, and 6 some other time.
Finally, we must take the damage done off of the player's HP. If the player's HP is less than one after this, it means they are dead. We then change the player's HP to 0 (So you don't see negative numbers in their stats) and make the PlayerAlive% variable of that player false. This way, the engine will know not to let that player have a turn, or to show their picture on the screen.
IF PlayerAlive%(HitPlayer%) THEN
Next, we show the damage done on the appropriate player, and delay for 3 seconds. The delay is necessary to allow us to see how much damamge the enemy did, and on who.
IF HitPlayer% = 1 THEN
Now that the enemy's turn is done, their agility meter must be set back to 0. We must also make GoThere% false, so the engine knows to return to the pervious loop. Finally, we must make our DrawNeed% variable equal to 1. We will use this variable later when calling the DrawBattleScreen so we know what parts of the battle we will need to redraw.
EnemyGo%(I) = 0: GoThere% = False: DrawNeed% = 1
Next, our routine checks if it was a player's turn right after the enemy's. If so, we must redraw the screen, using our DrawNeed% variable from before.
IF DrawNeed% = 1 AND GoGo% THEN DrawBattleScreen DrawNeed%
Now, we check if it's the player's turn to go. If so, we check which player will be going. Then, we draw a choice box for them which will give the player their options (RUN, ATTACK) and set up our hand pointer X and Y values. We then draw the player's mask over the player, this way, we will know which character's turn it is. We also draw our hand pointer next to the first choice in our choice box.
IF GoGo% THEN
Now, we have to create a loop. Then, we trap the keyboard buffer using INKEY$ to check which keys the player has pressed. This is all used so that the routine knows to move the hand pointer up or down, and which choice the player has selected when they hit enter.
The player has hit enter. Now, we check where the hand pointer was when they pressed the enter key. If it was at Attack (HandY% = 5), then we allow the player to choose which enemy they wish to attack. This is done with the hand pointer, and another loop while checking the keyboard. You will also notice that before the handpointer is moved, the routine checks if which enemy is still alive and which one isn't. This is so that the hand pointer is never pointing to an enemy that is no longer there.
You will also notice that this is where we use our array to hold the back- ground graphics were the hand pointer is put. This is necessary because once the player moves the hand pointer, we need to restore the graphics that were there before the pointer was drawn.
After the player hits enter here... We determine which enemy they have decided to attack. Then, we make a calculation based on the player's strength and the enemy's defense... As usual, we throw in a few random numbers to add variety.
Finally, we show an animation of the player attacking (We use our second array to hold background graphics here) and display the damage done on the enemy they've attacked. Again, we pause after this for 3 seconds, so that we may see who was attacked by whom, and how much damage was done.
IF HandY% = 5 THEN
A pic of the engine
This is the rest of our first choice box. Here, if the player has chosen to
run away (HandY% = 29) then we do a calculation based on the player's agility
and the agility of a random enemy to detrmine if the player can succesfully
run, or not. Why a random enemy? Simple, we want to allow the player to have
a fair chance from running from any group of enemies that isn't based on the
agility of the quickest or slowest enemy in the group. If the player can run,
then RanAway is made true.
ELSEIF HandY% = 29 THEN
Well... A player's turn has occured, so now we must reset their agility meter to 0 and make GoGo% false so that our program knows to return to the previous loop.
PlayerGo%(I) = 0: GoGo% = False
After a player or an enemy has had their turn, this routine checks to see if either the enemies or the party have been wiped out. If the players are dead, then Lost is made true. If the enemies are dead, then Won is made true.
IF EnemyAlive%(1) = False AND EnemyAlive%(2) = False AND EnemyAlive%(3) = False AND EnemyAlive%(4) = False THEN
Next, we check the situation (Players won, lost, ran away, or still fighting) and redraw the battle screen based on what has happened. After this is done, or DrawNeed% variable is reset to 0 again.
IF Lost = False AND Won = False AND RanAway = False THEN
Finally... This whole battle loop continues until something occurs to end the battle. This will be the players winning, losing, or running away. If any of these happen. The engine exits the loop.
LOOP UNTIL Lost OR Won OR RanAway
Once the loop is exited, the engine determines what has happened. If the players lost, a losing message is displayed. If they ran away, a message is displayed saying so. If they won, a message is displayed saying so, then the total gold and experience they won is calculated, displayed, and added to the living characters totals.
IF Lost THEN
The ChoiceBox sub.
Notice that a BoxType% variable is passed to it. This tells the sub which type of box to draw. If a box with the enemy names is needed, it is called like this: ChoiceBox 0 If a box with the player's choices is needed, it is called like this: ChoiceBox 1
SUB ChoiceBox (BoxType%)
LINE (0, 0)-(120, 50), 23, B
IF BoxType% = 0 THEN
The DrawBattleScreen sub.
Notice that the ScreenType% variable is passed to it. This is used to determine what parts of the screen to draw.
SUB DrawBattleScreen (ScreenType%)
DEF SEG = &HA000
IF ScreenType% < 2 THEN
FOR I = 1 TO 4
FOR I = 1 TO 2
The GetHandLocation sub.
IF EnemyAlive%(1) THEN
The InitBattle sub.
FOR I = 1 TO 4
Quick! Spot the stoner rock referance in the next line! :)
EnemyName$(I) = "Orange Goblin"
The InitRandomStats sub.
PlayerName$(1) = "DarkDread": PlayerName$(2) = "Tyranny"
PlayerMaxHP%(1) = 52: PlayerMaxHP%(2) = 37
PlayerHP%(1) = PlayerMaxHP%(1): PlayerHP%(2) = PlayerMaxHP%(2)
PlayerST%(1) = 7: PlayerST%(2) = 5
PlayerType%(1) = 0: PlayerType%(2) = 3
These next lines, you may wish to keep. They tell the engine where to put each enemy and character when drawing them.
EnemyX%(1) = 25: EnemyY%(1) = 75
PlayerX%(1) = 275: PlayerY%(1) = 75
The InitSprites sub.
DEF SEG = VARSEG(Players%(0)): BLOAD "players.spr", VARPTR(Players%(0)): DEF SEG
The StatsBox sub.
LINE (121, 0)-(319, 50), 23, B
The TimerDelay sub.
SUB TimerDelay (Seconds!)
CurrentTime! = TIMER
Putting it all together.
Basically, what you'll want to do, is take the stuff that's in the main part of the battle.bas file, and copy it over to your RPG. Of course, don't forget to change anything you may need to change. Then, copy all of the subs to your RPG as well. Now you're one step away from having semi-active battles in your RPG!
This is the easy part... Below, I've given you an example of how to make the random battles work in your RPG. You will likely have to change some lines, but this is the basic idea. Just put this code in your main loop:
There you go. That's all you need to get started. Should you have any problems with the engine, feel free to e-mail me at firstname.lastname@example.org and I'll be happy to help you out. Good luck with your RPG!
Next time (and there WILL be a next time, mwuahahah!).
Well... These tutorials are written for YOU. So tell me what you want to read about next. Do you want to know how to create an Eye of the Beholder/ Legend of Lith 2 style 3D engine... or something else?
Incidentally, if you wish to check out the Serenity homepage for some free RPGs written in QB, you can go here
>>>Back to Top<<<