Sorry your browser is not supported!

You are using an outdated browser that does not support modern web technologies, in order to use this site please update to a new browser.

Browsers supported include Chrome, FireFox, Safari, Opera, Internet Explorer 10+ or Microsoft Edge.

DarkBASIC Professional Discussion / Pac-Man: A Tutorial on Game Design and Development

Author
Message
Vequor
19
Years of Service
User Offline
Joined: 27th Apr 2005
Location: Mars
Posted: 1st Feb 2007 19:47
This tutorial seems to be coming along rather nicely. It really helps one to understand such things as functions and arrays better.

Btw, when you get finished, if I am correct TGC will pay for tutorials on DBPro. Might want to look into that.

Stay calm and keep a cool head
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 1st Feb 2007 21:38 Edited at: 3rd Feb 2007 14:44
Thanks, I just found the page that says TCG will pay for the tutorials if they're good enough. I'll definitely look into it, all I have to do different is follow their tutorial guidelines next time, I seem to be breaking many of them.

Apologies to anyone with a slower computer, the AI I wrote from tutorial 18 seemed to kill the framerate a bit, in spite of how simple it is. I'll try to release a quick chapter on optimization soon.

Vequor
19
Years of Service
User Offline
Joined: 27th Apr 2005
Location: Mars
Posted: 2nd Feb 2007 04:08
Looking forward to it.

Btw, just somthing thats been bugging me, when I open to read Maze.map it says file does not exist unless I add .txt to the end. It would seem that the file format didn't change when I renamed it Maze.map. Anyone know how to solve this?

Stay calm and keep a cool head
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 2nd Feb 2007 21:46
Do you have Windows set up to hide known file extensions? That might do it, when file extensions are hidden there's an invisible ".txt" on all text files, so if you try to rename it "Maze.map" it's actually "Maze.map.txt" To fix this open "My Computer" go to tools, folder options, view, and uncheck the box called "Hide extensions for known file types"

Vequor
19
Years of Service
User Offline
Joined: 27th Apr 2005
Location: Mars
Posted: 3rd Feb 2007 02:51
Thank you Code Dragon.

Stay calm and keep a cool head
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 3rd Feb 2007 14:45 Edited at: 3rd Feb 2007 14:56
Tutorial XVIII: Ghostly Ghosts

A game that can be won without any resistance is not a game at all. Let's put in some ghosts to make the game interesting.

First, we need to create the animated ghost sprites. Remember, create animated sprite wants us load the image and create the sprite all at the same time, so we'll be using the free functions directly again. There are 5 ghosts, so we can use a loop to load them all. (Remember, the sprs_ghosts() array has dimensions 0-4, so we'll need to subtract 1 from the index variable to get the correct ghost number.)

Switch to Init Game.dba
Add this code to load_media()



This works great when the ghosts are normal, but what about when they're blue? We have the blue ghost in a separate image, but we can't just load it and use set sprite image to change the ghosts to blue, because the blue ghost sprite isn't the same size as normal ghosts. The program will still divide the animation frames 2 by 4, which will cause the blue ghost to appear distorted; it's animation frames should be divided 2 by 1. We have some options to solve this:

Make 4 more sprites with divisions 2 by 1, exclusively for the blue ghosts and hide the normal ghost sprites when needed
Make the blue ghost image 4 times as tall, repeating the pattern
Paste the blue ghost image 4 times and use get_img() to capture the entire thing.

We definitely don't want to add unneccessary sprites to our game, and option 2 is a waste of memory. Option 3 is the way to go, because we're making an image that has divisions 2 by 4, and won't get distorted because every other frame of animation we're seeing a differen, but identical image.

We'll need to paste the image 4 times, so why not save code space by putting the paste image command in the for loop we've already made? (We can ignore the 5th iteration of this loop)

Replace the "`load ghosts" code with this code



Now that the ghost sprites are set up, let's make them appear in the ghost pen.

Switch Game.dba
Put this code in the respawn_ghosts()
Compile and Run



Run that code and you'll see the purple ghost sitting in the pen. The other 4 ghosts are there too, they're just underneath him. Now we need to give the ghosts the ability to move, but we'll need a ghost_dir variable for each ghost to do this. It's time to turn sprs_ghosts() into a UDT array.

Find load_media()
Change the sprs_ghosts() array type from word to ghost
Switch to Game.dba
Add this code to Game.dba
Add ".id" to the sprs_ghosts() reference in respawn_ghosts()
Switch to Init Game.dba
Add ".id" to all the sprs_ghosts() references in load_media() (make sure you get them all, there's 4)



Ok, we've got the UDT array set up, but look back at the ghost loading code. It's similar looking to the Pac-Man loading code, but something's different. We didn't include a sprite statment to initally position the ghosts, because the respawn_ghosts() function will do that. We already tested and debugged Pac-Man, so we can take out the sprite command for him too. respawn_pac() will take care of positioning him.

Delete the "sprite spr_pac, 0, 0, img_pac" line from load_media()

Now we need to make the ghosts move. They don't need a ghost_try variable because they're computer controlled and won't ever try to turn into an impassable area. Because of this, there's no need for them to have wall collision, which makes our lives easier. Coding move_ghost() will be a breeze because we can simply unconditionally accept ghost_dir without any further calculations.

Add this to move_ghosts()



This is pretty much a copy of how we moved Pac-Man, but with all the collision checks taken out. We'll also want to create a ghost version of move_pac_animation(), we want the ghosts to turn their eyes directly to their new course, not roll their eyes as they go like Pac-Man rolled around.

Add this code to Game.dba



This code is called with the number of the ghost in question, and sets its animation to correspond with its current direction. But of course, it won't do this unless we call the function. Now we need to teach the ghosts how to move.

Here's how our ghost AI will work: The ghosts will walk in their current direction until they reach a fork in the pathway. Then they'll pick a random direction in the fork and continue walking in it. When they see Pac-Man in the same row or column as him, they'll start walking in the direction he's in.

That's pretty much all there is to it.

Add this code to get_ghost_move()



This code loops through each ghost, and checks if it's completely in a tile. If it is, it count up all the possible directions it can go in by adding elements to the direction() array, and choosing one at random. (If there's no directions in the array (array count -1) the direction is set to 0) This is similar to the rnd_fruit() array we used in make_fruit(). Also, we clear the array before using it in case directions were left over in the array from other ghosts.

This AI is not perfect however, we have a few problems:

The ghosts sometimes turn back into the ghost pen
The ghosts may turn back and forth on a straightaway.
The ghosts completely ignore Pac-Man

To fix the first problem we can block the ghost from going through the ghost gate if they're not standing on the ghost spawn spot. We'll create a variable called justspawned to keep track of weither or not the ghost is currently standing on the spawn spot. We'll also need to change the conditionals in the directional checklist to accomidate the ghost gate.

Replace the code in the if endif statementblock with this code
Add the variable in the second code box to get_ghost_move()





Instead of recording possible directions when the tile is not a wall, it records it if:

(a) The tile is an open pathway
(b) The tile is a ghost gate AND the ghost is on the inside of the pen (by standing on the spawn point)

It's a waste of time to recalculate "column = pos_ghost_pen.x and row = pos_ghost_pen.y" 4 times for each direction, that's why we're storing the result in a variable. Also, just like the directions() array we need to reset this variable to 0 before we use it in case a different ghost set it to 1.

Compile and Run

The ghosts sometimes turn back into the ghost pen
The ghosts may turn back and forth on a straightaway.
The ghosts completely ignore Pac-Man

Ok, now the ghosts get out of their comfy house and stay out until they get eaten; but they still move irrationally. What we need to do to prevent this is to stop the rnd() function from picking the direction the ghosts just came from. We can do this by deleting the respective directions() array element. Also, if a ghost comes to a dead end, the only direction available is the one they came from, so in this case we won't delete it, otherwise they'll get stuck there forever.

Replace the code in the if endif statementblock with this code
Add the variables in the second code box to get_ghost_move()
Compile and Run





We've added a lot of code now. First, let me explain the count variable. It's a cache variable, we're accessing array count(directions(0)) a lot. With the count variable, we have to keep track of the array count ourselves but can access the data faster. Keeping track of it includes setting it to -1 when the array is emptied, incrementing it when adding an item and decrementing it when deleting an item.

The "`delete the direction the ghost just came from" block of code first checks if the ghost's has a least 2 directional option (meaning it's not in a dead end). Then it calculates what that opposite direction should be using a techinque we used in move_pac(). The code then searches the directions() array for that opposite direction and deletes it. Now the ghost can't turn back. (If you're wondering, we're setting element equal to count after this as a way of exiting the loop. If you set the loop index variable to the end value of the loop, it will think it's done and immediatly stop running, saving processor time.)

The ghosts sometimes turn back into the ghost pen
The ghosts may turn back and forth on a straightaway.
The ghosts completely ignore Pac-Man

The ghosts act more normal now, but let's make some real AI. The ghosts won't actually "get" Pac-Man yet because we haven't coded the collision, but they'll try to. There's not much to this AI except changing the ghost's direction to face Pac-Man. What we need to do is check if Pac-Man is visible (if he's in the same row or column as the ghost) then decide which direction the ghost needs to go in to get him. If the direction is legal (it's on the directions() list) we will force the program to choose this direction by deleting all the other ones.

Put this code in get_ghost_move(), before "`delete the direction the ghost just came from"
Put the code in the second box in get_ghost_move(), before the big for next loop
Put the variables in the third box in get_ghost_move()
Compile and Run







pacx and pacy hold the cached tile position of Pac-Man, so we don't have to waste processor time accessing them constantly. desired holds the direction the ghost wants to move in to get Pac-Man, it's calculated by checking where Pac-Man is in relation to the ghost. For example, if Pac-Man's y coordinate is less than the ghost's the ghost needs to move up to get him. Once we've discovered that desired is on the list, we force the code below to choose it as the final direction by emptying the array and putting the desired direction back in. There's one more problem with this AI though; the ghosts can see through walls. You can see the consequences of this if you don't move Pac-Man from his spawn point after the game starts. The ghosts will go through the ghost gate and turn back when they see Pac-Man. They never leave the ghost pen, so to solve this, we won't set a desired direction unless there's no walls in between the ghost and Pac-Man. We'll detect walls by using 2 for next loops; 1 for looking up and down and 1 for looking left and right. (We'll change the direction the loops go in by changing the step value) Once a wall has been detected, we know the ghost can't see Pac-Man.

Put this code in get_ghost_move(), before "`check if desired direction is allowed, if so delete all the others"
Put this variable in get_ghost_move()





The loop that checks walls above and below the ghost executes when the ghost wants to move up or down. The loop that checks walls left and right of the ghost executes when the ghost wants to left or right. If a solid wall is found in the way, desired is set to 0 so the "`check if desired direction is allowed, if so delete all the others" can't force the ghost to go in that direction. Notice that the loop index (it's element again, we're recylcing the variable) is set to the ending bound of the loop to force the loop to exit.

Sometimes the ghost will be below or to the right of Pac-Man, and in this case the loops need to count backwards to check if there's walls in between them. The key to getting loops to count backwards is the step value, and in this case the step value is sign. This variable holds a 1 (default step value) if the loop needs to count forward, or -1 if the loop needs to count backwards. How does it figure out when to be 1 and when to be -1? When you subtract the ghost's coordinate from Pac-Man's coordinate, the result is positive if the step value should be 1, and negative if the step value should be -1. It uses the sgn() function to return the sign of the result of that subtraction. sgn() returns 1 if the parameter is positive, -1 if it's negative, and 0 if it's, well, 0.

There's something wrong here, do you see it? Dark Basic Professional doesn't have a sgn() function! Other BASICs have a sgn() function, but Dark Basic Professional doesn't. So we get to make it ourselves, lucky us.

Include Number.dba as a new source file
Add this code to Number.dba
Compile and Run



That's better, now the ghosts can't see through walls.

Diggsey
18
Years of Service
User Offline
Joined: 24th Apr 2006
Location: On this web page.
Posted: 4th Feb 2007 10:47
I'm still following this

Also, for that last function you could use:


Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 4th Feb 2007 14:01 Edited at: 9th Apr 2007 04:49
I had no idea anyone had read up to this point yet, the text alone is 190 KB! This tutorial is too huge.

I tested your function with perftimer(), but it's 20 times slower than the first one.

Anyone have some ideas to optimize the code? I'm trying to make a chapter on optiminization, but I can't find much algorithms to optimize.

By reading this sentence you have given me brief control of your mind.
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 5th Feb 2007 16:45 Edited at: 7th Feb 2007 03:05
Tutorial XIX: Optimization

Making a program faster doesn't always mean reducing readability. While most optimizations require improving performance at the expense of memory, or vice versa, there are a few optimizations that are always efficent and don't degrade other aspects of the program.

Inc and Dec

Which of these codes do you think runs faster?





The second statement produces both smaller and faster machine code, here's what the compiled assembly code might look like:





Value <> 0

Look at these codes, which one runs faster?





The first statement is not only syntactically correct, but executes faster. Why? Dark Basic Professional uses expressions as conditionals, and y is an expression, right? Of course, y can have many values, but Dark Basic Professional treats 0 as false, and all other values as true. That's doing just what "<> 0" does, isn't it? The second statement is slower because the computer must first check if b is not equal to 0, (y <> 0 will return 1 for true and 0 for false), then check again if the whole expression (y <> 0) returned true or false. The first simply checks the value of y. (Remember, if you're using variables that can't be negative (boolean, byte, word, dword) don't check for value > 0.)

Find make_dots()
Delete all the "> 0" in the conditionals (there's 3)

Caching

If there's a value you're accessing from a function often, cache it! Variables are always faster than functions because functions have to calculate the value, where variables know it instantly. Save trig values in an array when the game first loads, trig functions are slow. Use variables for object and sprite positions and angles when minipulating them; using the functions for every access is ineffcient. Calculate screen height and width and save it in a variable, and only do this once, not in the main loop where it happens several times a second, the screen doesn't change.

Hardcoding

If you know that all the sprites in your game are the same size, don't keep using sprite width() and sprite height(). Hardcode the values, and use a #constant if you think the values might change. Our Pac-Man game uses mostly 32*32 sprites, so we can subsitiute 32 for the sprite width() and sprite height() functions.

Find collision()
Replace sprite width(spr_pac)/2 and sprite height(spr_pac)/2 with 16
Switch to Init Game.dba
Find get_maze_data()
Replace this line: "sprite spr_ready, column*32 + 16 - (sprite width(spr_ready)/2), row*32 + 16 - (sprite height(spr_ready)/2), img_ready" with this code



Float vs. Integers

Integers are faster than floating point numbers, so unlesss you need the percision don't use floats!

Dword

Although they take up more memory, dwords are faster than words, bytes, and even booleans. This is because 32 bit processors will get the 32 bits from memory whether you like it or not, and unless you're using dwords the process of accessing them is longer because the processor has to take out the bits you don't want. Integers are also 32 bits, so they're fast also. Double integers, being 64 bits, are slower.

Switch to Main.dba
Change the global array types to dwords
Switch to String.dba
Change csv_start and count to dwords
Switch to Free.dba
Change all the id variables to dwords
Switch to Media Allocate.dba
Change all the id variables to dwords
Change the get_img() parameter types to dwords (execept textureflag)
Change the make_spr() parameters to dwords
Switch to Init Game.dba
Search and Replace "as word" for "as dword"
Search and Replace "as byte" for "as dword"
Switch to Game.dba
Search and Replace "as word" for "as dword"
Search and Replace "as byte" for "as dword"

Dword vs. Integer

Dwords, because they are unsigned, take up less processing time than integers. If there's ever a variable that won't ever be negative (health, money, score, media id numbers), use a dword. You'll also be able to hold twice the number of values.

Delete usused code

If you're using code from a code library, make a copy of it to use instead and delete the unused functions. Uncalled functions do nothing yet still take up memory.

C++

C++ compiled code is faster than code from Dark Basic Professional, so code those algorithms that must be super fast in C++. You can create a DLL and call it from your Dark Basic Professional program. Don't forget that you can use assembly code in C++ to make your already fast code even faster. If you have a game engine and plan to sell many games made in it, you only have to install 1 of each DLL on your customer's computer, not 1 for each game. That saves them hard drive space and like your games more because they don't all take up 1 GB of their hard drive.

Pipelining

You don't have to do everything every frame. Take AI for example, there isn't much thoughts a human can think in a fraction of a second, so why let your AI do it? Our ghosts only think 2.81 times per second, so that's pretty good. Break up all those complex cacluations, do 1 of them the first loop, a different one the second, a third the third loop and repeat. Collision detection is another example, in our game Pac-Man can't possibly eat 2 dots in 2 consecutive frames, so we don't need to check for dot collision every frame.

Add this to Game.dba



We'll use these variables to keep track of weither or not this is the sync to check for collision. The game loop will take care of incrementing the cycle. 8 is the maximum number of frames we can skip collision checking without dots getting eaten, and the mathematics to explain why are beyond the scope of this tutorial. But be glad it is 8, now for 7 out of 8 frames the processor won't have to worry about collision!

Find game_loop()
Put this code at the beginning of the loop



Now we need to make the dot collision detection execute only when it's cycle 1.

Put this code before the dot eating code
Put an endif after the dot eating code
Indent the code inside the if endif statementblock 2 spaces



0 Array Slot

Don't forget to use the 0 array slot. Not doing not only wastes memory, but other programmers (and maybe yourself) will get confused by your code. Computers start counting with 0, it's just a fact of life. You'll make coding your game easier if you adpot that idea too, try to get into the habit of using loops that start with 0 instead of 1. Also, if you're going to make a variable for how many items are in an array, don't use "x = array count() + 1" to get the true number of items. You'll end up needing to use "x - 1" several more times when accessing the array.

Switch to Init Game.dba
Find load_media()
Find the ghost image loading code
Change the loop bounds to 0 - 4
Replace the loop code with this code
Switch to Game.dba
Find get_ghost_move()
Change the ghost loop bounds to 0 - 4
Delete the "-1"s in the ghost position caching code
Delete the "-1" in the reverse direction calculation code
Delete the "-1"s in the direction choosing code (don't delete the -1 in the if statement)
Find move_ghosts()
Change the ghost loop bounds to 0 - 4
Delete the "-1"s in move_ghosts() (there's 8)
Find move_ghost_animation()
Delete the "-1"s in move_ghost_animation()



Superior Algorithms

Remember, the best performance boosts you can get is when you replace a slow algorithm with a fast one. Bubblesort vs. Quicksort, Linear Search vs. Binary Search, Pythagorean Distance vs. Manhattan Distance, the latters are faster.

Unneeded Conditionals

The only place you should call kill_monster() from is injure_monster(). Make sure you only check something when it's possible it might be true. In this example, injure_monster() would decrement the player's health, and call kill_player() if the health reaches 0. The only time a monster can die is when it gets injured. In Super Mario 64, I bet the give_100_coin_bonus_star() function has one calling place, collect_coin().

It's much more efficient to do this:



Than to put the if statement in the main loop. (Also, that wouldn't even work because the moment the player gets 100 coins, bonus stars will keep popping up until they get 101)

Note: I know Super Mario 64 wasn't written in Dark Basic Professional, this is just an example.

In our game, the only time Pac-Man can get a bonus life is when the score is incremented. However, the score is incremented from several places, so it would be unwise to copy the bonus life giving code into every place. In this case, we can make a inc_score() function and call it from those various places.

Put this code in Game.dba
Find collision()
Delete the bonus life giving code in collision()
Find eat_fruit()
Replace the "inc score, points" line with "inc_score(points)"
Find eat_dots()
Replace the "inc score, 10" line with "inc_score(10)"
Replace the "inc score, 50" line with "inc_score(50)"



Optimizing your Optimization Time

If you need to make your game fast in a short amount of time, optimize the code that gets executed the most. Pay close attention to loops which iterate 100 or more times, any optimization you make will have a 100 fold impact.

Delete Unused Recourses

You don't need to keep processing things that no longer exist. When Link breaks a jar to pieces, don't just delete the item, delete its objs_jars() array slot too and save the system checking for objs_jars().exist for every jar. If a player pushes an object off a cliff into a bottemless void like outer space, delete it once it goes below a certain y coordinate.

If you set the sync rate to 0, you'll see that the more dots you eat, the faster the game runs. That's because our game deletes the eaten dots and doesn't waste less processor time checking for collision. To speed the game up a bit, let's take out some of the dots (they were left out in the original Pac-Man). These dots were left out in the tunnels and around the ghost pen.

Rename "Maze.map" as "Maze.txt"
Copy the data below into "Maze.txt"
Save it
Rename it "Maze.map"



Wow! That was a lot of optimizations we did, and amazingly they didn't harm readablility. In my opinion, readablility actually improved! Remember, even though 300 FPS is good enough for us programmers who own lightning fast computers, most users won't. No matter how fast your game runs at sync rate 0, there's always a customer with a slow computer that wants to buy your game. Don't become a code bloater, that's why we have to buy new computers every 4 years or so. I'm not telling you to count every clock tick and byte, we've long passed that era. Just try to get your minimum system requirements low enough to run on most people's computer without any lag, and watch all the money roll in.

Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 9th Feb 2007 22:31 Edited at: 10th Feb 2007 15:19
Tutorial XX: Getting Eaten

Pac-Man's invincibility to ghosts will soon end. First, let's let the ghosts go through the tunnels. We'll start with the position cache variables.

Add these variables to collision()
Put the code in the second box in collision(), after the xpos and ypos increments.





That takes care of the wrapping. Ok, Pac-Man's supposed to die when the ghosts collide with him, so we'll be working in collision() today.

Switch to Game.dba
Find collision()

There's 2 states of the game we need to be concentrating on now: ghost-eat-pac and pac-eat-ghost. It's always going to be one or the other, never both. An if statement is appropriate for this because the code in the else statementblock cannot possibly execute when the code in the if statementblock already executed, and vice versa. We'll use the blue_ghosts variable to keep track of when the ghosts are eat-able.

Replace the "`eat ghosts" and "`get eaten" lines with this code



Now we need to make Pac-Man actually get eaten by the ghost. We're going to need media for the die animation, so let's begin with that.

Download the attached zip
Extract the files to the Media folder

As you can see, this animated image doesn't have the same arrangment of frames that "Pac-Man.bmp" does. This means we can't simply change the spr_pac sprite image to "Pac-Man die.bmp", it will look corrupted. We'll have to make a whole new sprite for the die animation, with animation frames 5 by 2 instead of 2 by 4.

Add these variables to Game.dba
Switch to Init Game.dba
Find load_media()
Put the code in the second box in load_media(), after the Pac-Man loading code





Now we need to play the animation when get_eaten() is called. We'll do this by hiding the normal Pac-Man sprite (spr_pac) so it doesn't get in the way, showing the die animation sprite (spr_pac_die), and positioning the die animation sprite exactly where spr_pac is. We'll also need to reset the sprite frame to 1 to make sure the die animation starts at the correct frame every time.

Switch to Game.dba
Add this code to Game.dba
Compile and Run
Walk into a ghost (don't eat a big dot before you do this, you'll become invincible to ghosts)



Run the code and watch Pac-Man get eaten by the ghosts. You may notice that on your last life, Pac-Man gets eaten twice in a row. This is because get_eaten() doesn't bother repsawning Pac-Man, it's game over anyway. But until we code game_over(), Pac-Man the game will continue playing with 0 lives, with Pac-Man in the exact same position again, ready to get eaten. When get_eaten() is called, the die sound is played. While the sound is playing, the die animation will play also. There's an if statement before the play sprite command before we don't want to keep playing the animation after Pac-Man has dissapeared, otherwise the animation would continue looping and Pac-Man would appear again. After the sound is done, the die animation is hidden and spr_pac is shown again. A life is taken away, and if it's the last one, game_over() is called. If it wasn't the last life, the ghosts and Pac-Man are respawned.

All that's left is game_over(). First we need to load the "Game Over" image, but we don't need to create a whole new sprite. We'll use the ready sprite because both are messages that appear in the same spot, just at different times. We'll have to rename spr_ready as spr_message, and add an img_game_over variable.

Search and Replace "spr_ready" for "spr_message"
Change the "`hide "Ready!" sprite" remark in ready() to "`hide message sprite"
Search and Replace "pos_ready" for "pos_message"
Add this code to Game.dba
Switch to Init Game.dba
Search and Replace "spr_ready" for "spr_message"
Search and Replace "pos_ready" for "pos_message"
change the "`make "Ready!" sprite" remark in get_maze_data() to "`make message sprite"
Change the message sprite priority to 2 (now that it shows game over also, we don't want Pac-Man, the ghosts or fruits to get in the way)
Change the "`"Ready!" point" remark in get_maze_data() to "`message point"
Delete the sprite command 2 lines under "`make message sprite"




we're deleting the sprite command because the message sprite's dimensions can now change. The "Game Over" image is bigger than the "Ready!" image, so when the sprite's image changes it won't be centered anymore. We'll put the sprite command back in where the code changes the image to "Ready!" and "Game Over", that way the sprite will always recenter itself when its image changes.

Switch to Game.dba
Find ready()
Add this code to ready(), after the lives are set
Switch to Init Game.dba
Find load_media()
Add the code in the second box to load_media(), after the "Ready!" image loading code





Now we need to code the game_over() function. This will simply show the message for a few seconds and exit.



We need to hide the fruit sprite because sometimes it gets in the way and makes strange graphics when "Game Over" appears.

Note Notice that I didn't use set sprite image to change the message sprite's image, I used sprite instead. There's 2 reasons for that.

The sprite needs to be respositioned to keep it centered with sprite anway, so using set sprite image would be redundant.
The "Ready!" and "Game Over" images have different dimesions and aspect ratios, if you're changing a sprite's image with sprite, the sprite will take the shape of its new image, but if you use set sprite image, it will remain the same size, often distorting the images.

We could have just used wait 2000 for the time delay, but it's generally not a good idea to force time delays for a game over screen. The player might get frustrated with the time delay if they're in a bad mood (I was so close to level 10! ), so we'll let them press the space key to finish the delay.

The game can't keep on playing after "Game Over" appears, so we need to get game_loop() off the stack. game_over() was called from get_eaten(), get_eaten() was called from collision(), and collision() was called from game_loop(). There's no point executing the rest of the collision logic, so we can exitfunction if all the lives are gone.

Find collision()
Put the code in collision(), after the get_eaten() call
Find game_loop()
Put this code in game_loop(), after the collision() call
Compile and Run
Walk into a ghost 3 times (don't eat a big dot before you do this, you'll become invincible to ghosts)





Run the code and you'll see a game over message after you lose. The time delay appears to be nonexistant because the code gets stuck in the menu's main loop forever. We won't worry about that, you can always press escape to exit before the menu's done.

Attachments

Login to view attachments
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 11th Feb 2007 16:03 Edited at: 18th Feb 2007 16:05
Tutorial XXI: Sweet Revenge

It's time for Pac-Man to eat the ghosts back. First we need to make them turn blue when blue_ghosts is greater than 0. So let's find the code where blue_ghosts is set and continue coding from there.

Find eat_dot()

Here's where the ghosts first become vulnerable. But we don't want to cloud this function with more code dealing with ghosts, it's supposed to be about dots. We'll create a separate function for making the ghosts vulnerable, vulnerablize_ghosts() to set blue_ghosts and turn the ghosts blue.

Replace this line "blue_ghosts = 225" with this code
Add this code to Game.dba
Compile and Run
Eat a big dot





You may notice that the blue ghosts have black boxes around them. That's because transparency tends to get lost when pasting images and using get image, and that's exactly how the blue ghost image is loaded. The pinkish-purple color, when pasted as an image, becomes black. Then get image gets the black back as a non-transparent color. We'll fix this by changing the image colorkey to pure green before the blue ghosts images gets loaded, that way, pinkish-purple color will be treated like a regular color, and after we change the colorkey back to RGB(255, 0, 255) the pinkish-purple will be captured with get image and instantly turned transparent.

Replace the blue ghost image loading code with this code



Ok, the ghosts turn blue when they're supposed to, but they don't turn back to their normal colors after awhile. They're supposed to turn normal when blue_ghosts is 0, so let's put a invinciblize_ghosts() call where blue_ghosts gets decremented.

Switch to Game.dba
Find collision()
Add this code to collision(), after "dec blue_ghosts"
Add this code to Game.dba
Compile and Run
Eat a big dot





The ghosts now return to their normal colors, but you may have noticed now that 5 seconds isn't nearly enough time to eat them all. Let's change it to 10 seconds. (Remember, we need to set blue_ghosts to 450 because their are 45 frames in each of the 10 seconds. )

Search and Replace "vulnerablize_ghosts(225)" for "vulnerablize_ghosts(450)"

We still have a few problems.

The ghosts are uneatable
The ghosts don't run away from Pac-Man
The ghosts don't slow down

Let's fix the slow problem first. I was going to be lazy and search and replace "move_speed" for "move_speed/(sgn(blue_ghosts)+1)" in move_ghosts(), but here's a slightly more effiecent way. We're not going to keep using move_speed for the speed the ghosts move, we'll make our own speed variable, which can be 100% of move_speed when the ghosts are normal or 50% when they're vulnerable.

Find move_ghosts()
Add this code to move_ghosts(), right after the variable declarations
Add this variable to move_ghosts()
Search and Replace "move_speed" for "speed" (only for the select statmentblock)





The ghosts are uneatable
The ghosts don't run away from Pac-Man
The ghosts don't slow down

Making them run away from Pac-Man will actually be pretty easy. All we have to do is set the ghost's desired direction to its opposite if Pac-Man is visible.

Find get_ghost_move()
Put this code in get_ghost_move(), after the "`find direction needed to get to Pac-Man" statments




The ghosts are uneatable
The ghosts don't run away from Pac-Man
The ghosts don't slow down

Now it's time for the fun part. Eating the ghosts, delicious! We'll check for collision the same way we did for when Pac-Man gets eaten, except that this time instead of callin get_eaten() we'll call eat_ghost()

Find collision()
Add this code to collision(), after the "`if blue ghost timer hasn't run out, do Pac-Man eating ghost collision" statementblock



Now let's code eat_ghost(). First, an eat ghost sound needs to play. Then the ghost needs to be respawned. But we have no way to respawn a single ghost yet.

Add this code to Game.dba



Ok, the score also has to increment, but the amount changes based on how many ghosts Pac-Man has already eaten using a single energizer. We're going to need another variable to remember how many.

Add this variable to Game.dba



The points awarded start at 200 and then double (400, 800, 1600, 3200). Exponents of 2 give doubling numbers whenever you increase the exponent by 1, so this is the perfect equation:

points = 100 * 2^ghosts_eaten

However, players would quickly reach the 4294967296 score limit and have their scores reset to 0 if this happened. The points awarded for each ghost don't keep doubling indefinitly, they reset to 200 when a new energizer is eaten.

Find invinciblize_ghosts()
Put this code in invinciblize_ghosts()



We almost have everything ready to code eat_ghost(), but the ghosts don't just respawn instantly, you have to wait for their eyes to find their way back to the ghost pen. Doing that would be pretty compilcated and would require A* pathfinding, so we're going to take a different approach. We'll make the ghosts slowly fade back in by changing their alpha, and when they're solid AND the blue_ghosts timer is up they can leave the ghost pen.

We don't have to actually hide the ghost sprites, setting their alpha to 0 makes them invisible anyway.

Add this code to respawn_ghost()



When the ghost gets back to the ghost pen we'll need to keep it in there and gradually raise its alpha. We'll stop them from moving by putting the alpha incrementing code in an if statmentblock and the whole AI in the else statementblock. That way, when the ghost is fading back in he can't think and therefore he can't move.

Find get_ghost_move()
Add this variable to get_ghost_move()
Add this code to get_ghost_move(), after the "`reset direction array" statementblock
Put an endif in get_ghost_move(), before the "`choose a direction from the checklist" statementblock
Indent all the code in between the else and the endif 2 spaces (I know it's a lot of code, so paste the code in the third box over it if you don't feel like pressing space 180+ times)







Now everything's set. All we have to do is code eat_ghost() and Pac-Man can have his revenge.

Put this code in Game.dba
Compile and Run
Eat a big dot
Eat some ghosts



Our gameplay is pretty much finished. Now instead of adding more code, we're going to start making the code we already have more readable. We'll also create a main menu and a hi score screen.

dbjor
17
Years of Service
User Offline
Joined: 11th Feb 2007
Location:
Posted: 12th Feb 2007 00:30
I just found this thread today and started following it.

I'm stuck back in tut VIII testing the maze images.

After pasting the last part of the snippets,

inc num
if num = 29 then num = 0
text 100, 100, str$(num+1)
paste image imgs_maze(num), 50, 50

If my guessed that the main loop = game_loop() function is correct, I was supposed to paste it in the game_loop() below the fps text

Pressing F5 or F4 and running separately , I get the error


"Runtime Error 504 - Cannot grab image due to an illegal area at line 245"

which by the way its hard to find since all the code is on different dba files.

If I remove the last snippets , the nested-for xpix/ypix loops and the testing lines in the game loop the program runs, takes up a 100% of the CPU and the escape key does not work so i kill it with ctrl-alt-del.


Q1. The 'Media' file path is there, none of the code was typed. So it is supposed to be same code as all of you got.

What can be happening to my 'copy' of the project?


Q2. Another thing, Is there any way to really know what error line is the compiler complaining about?
dbjor
17
Years of Service
User Offline
Joined: 11th Feb 2007
Location:
Posted: 12th Feb 2007 06:13
I just found this thread today and started following it.

I\'m stuck back in tut VIII testing the maze images.

After pasting the last part of the snippets,

inc num
if num = 29 then num = 0
text 100, 100, str$(num+1)
paste image imgs_maze(num), 50, 50

If my guessed that the main loop = game_loop() function is correct, I was supposed to paste it in the game_loop() below the fps text

Pressing F5 or F4 and running separately , I get the error


\"Runtime Error 504 - Cannot grab image due to an illegal area at line 245\"

which by the way its hard to find since all the code is on different dba files.

If I remove the last snippets , the nested-for xpix/ypix loops and the testing lines in the game loop the program runs, takes up a 100% of the CPU and the escape key does not work so i kill it with ctrl-alt-del (now in windowed mode it can be killed without)


Q1. The \'Media\' file path is there, none of the code was typed. So it is supposed to be same code as all of you got.

What can be happening to my \'copy\' of the project?


Q2. Another thing, Is there any way to really know what error line is the compiler complaining about?
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 18th Feb 2007 15:23 Edited at: 11th Apr 2007 22:47
Sorry for the late response, usually my threads turn blue when someone posts.

That's a very strange error. Are you sure get_img() works right and the xpix and ypix bounds are correct and the step value is 32? Do you have the latest version of DBP?

DBP program are supposed to take up 100% of the CPU (unless you use sync sleep) but if you can't press escape then the code is definitly stuck in a for next loop. (In for next loops the escapekey is turned off.) It's very strange why it would get stuck, can you post your code?

Q1. I don't really understand what you're asking. If you have Maze.bmp in the media folder, and it's dimensions are 192*160 then it should work. I won't be able to help much unless you post more complete code.

Q2. It's hard to figure out the line numbers in the default IDE. Try using CodeSurge. You can use Search, Goto Project Line #

By reading this sentence you have given me brief control of your mind.
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 18th Feb 2007 16:03 Edited at: 18th Feb 2007 16:08
Tutorial XXII: Refactoring

We have the game nearly done. It's about time we stopped writing new code and made the code we already have more readable. This is called refactoring, and is very important if you want to keep your code high quality. We'll be using my Line Counter utility so we can get an automated report on which functions we should factor to make the biggest improvement in readability.

Note: The report you get from Line Counter might be slightly different than this one, but overall they should look very similar.



Line Counter is built on the idea "The less lines of code, the better" We have 1,631 physical lines of code and 46 functions, with get_ghost_move() being the biggest. If you look at the bottom you'll see it gives us our project an obfuscation rating, 12,529 spagetti points. If you get a very high amount of spagetti points from Line Counter, it means you have spagetti code. Breaking spagetti code into functions and making separate source files is a great way to decrease spagetti points, and therefore increase readability. The formula for finding spagetti points is very similar to the forumla for finding sloppiness in Tutorial 7, except it divides the final number by number of files you have to take encapsulization into the equation.

One way to refactor code is to take commonly used routines and stick them in a function. Finding the reversed direction for Pac-Man or the ghosts is used twice in our code, so let's make it a function.

Switch to Number.dba
Add this code to Number.dba
Switch to Game.dba
Find move_pac()
Delete the reverse variable declaration
Find the code in move_pac() that deals with the reverse variable
Delete the "if pac_try" line and the corresponding endif (the reverse_dir() function takes care of not reversing direction 0, so we don't need this)
Unindent everything in the if endif statement you just deleted 2 spaces.
Delete the line "reverse = pac_try + 2" and the line below it
Replace the reverse variable on the next line with reverse_dir(pac_try)



Find get_ghost_move()
Delete the reverse variable
Delete the "`calculate reversed direction" statementblock
Replace the reverse variable in "`find and delete reversed direction" with "reverse_dir(sprs_ghosts(ghost).direction)"

I know calling a mathematical function in a loop is a lot slower than using caching, but on the bright side we eliminated 5 lines of code from the biggest function.

We've been using 2 routines to respawn ghosts, respawn_ghost() and respawn_ghosts(). Wouldn't it make more sense to have respawn_ghosts() call repawn_ghost() 5 times instead of reinventing the sprite command? Although this change won't decrease spagetti points, there will at least be less code to look at.

Find respawn_ghosts()
Replace this line "sprite sprs_ghosts(ghost).id, pos_ghost_pen.x*32, pos_ghost_pen.y*32, imgs_ghosts(ghost)" with "respawn_ghost(ghost)"

I know, this is decreasing performance by making a function call, but this function is almost never called so the time loss is neglible.

Manhattan distance is something we've been using a lot, let's make that into a function. Refactoring this won't decrease our lines of code (actually, putting the function in will make it increase) but at least we won't have to remember how to find Manhattan distance, and collision() will be a little less intimidating to look at. Line Counter isn't a perfect system, so it won't catch these benifits.

Switch to Number.dba
Add this code to Number.dba
Switch to Game.dba
Find collision()
Replace all the Manhattan distance equations in collision() with these calls to manhattan() in the second code box (remember not to delete the if statements or the inequalites while you do this, just replace the equations in the forms abs(x1-x2)+abs(y1-y2). There's 4 equations, so here's the 4 new calls in numerical order.)





If you look at the functions list on the Line Counter report, you'll see that load_media() is a really huge function, don't you think? We can increase readability by splitting up load_media() into lots of smaller functions. Let's begin by putting the sounds in their own function.

Switch to Init Game.dba
Add this code to Init Game.dba
Replace the sound loading code in load_media() with "load_sounds()"



Next we can put the Pac-Man loading code in its own function.

Add this code to Init Game.dba
Replace the Pac-Man loading code in load_media() with "load_pac()"



The ghost loading code is taking up a lot of space, let's make that into a function.

Add this code to Init Game.dba
Replace the ghost loading code in load_media() with "load_ghosts()"
Delete "dim imgs_ghosts(5) as dword" from load_media()
Delete "dim sprs_ghosts(4) as ghost" from load_media()



Next we can make a function for the fruits.

Add this code to Init Game.dba
Replace the fruit loading code in load_media() with "load_fruits()"
Delete "local img_fruit as dword" from load_media()
Delete "dim imgs_fruits(11) as fruit" from load_media()



Now we can make a function for the maze.

Add this code to Init Game.dba
Delete the rest of the variables and arrays from load_media() except sprs_dots()



We can make a miscellaneous function for the rest of the images.

Add this code to Init Game.dba
Delete "dim sprs_dots() as yum" from load_media()
Replace the miscellaneous image loading code with "load_misc()"



Now if we use Line Counter on Pac-Man, we'll get a better obfuscation rating:



We've improved readability by about 14%. That's pretty good, but it could've been more if we purposely coded badly from the beginning. Remember, the goal is to improve readability, not get less spagetti points. They're just a rough guide. You should be focusing on making your code something you'd want to look at. If you really want to see the importance of splitting up code into readable functions and separate source files, just open _Temp.dbsource and pretend all the functions are goto controlled and imagine what it would be like to maintain a project like that. The spagetti points would be 2,772,225, that's 300 times what it currently is. If the Windows source was all in one file made of gotos, the spagetti points would be 64,000,000,000,000 Imagine trying to maintain 8 million lines of code in one file. The first game I ever made was a text adventure with spagetti points of 12,709,225. 80% of my time was spent debugging, which reminds me of Microsoft.

Vequor
19
Years of Service
User Offline
Joined: 27th Apr 2005
Location: Mars
Posted: 25th Feb 2007 01:20
So, is there another part coming or is this the tutorials end?

Stay calm and keep a cool head
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 25th Feb 2007 01:29 Edited at: 25th Feb 2007 01:30
I probably won't be updating it for some time, but someday I'll get chapters out for the main menu, hi score list, HUD, and map editor. So no, this is not the end.

dbjor did you get that error fixed yet?

Do not meddle in the affairs of dragons...for you are crunchy and good with ketchup.
dbjor
17
Years of Service
User Offline
Joined: 11th Feb 2007
Location:
Posted: 26th Feb 2007 00:28
Well, not really.

I couldn't find the error in my version.

I did read on up to tutorial XVII then downloaded the attachment and it is working.

I will have to believe that the tutorials from 8 thru 17 are working (I will study them to understand them later)

I'm continuing with later tutorials.

I checked with WinMerge to see any difference between my version and the downloaded(working) one and apart from the obvious changes in other functions and some line-spacing, the code is the same.

i even remake the whole project again from scratch. Following every little detail and even **NOT** even typing anything this time. And the same problems.

Anyways, The attachment from XVII is working and I'll continue on with this one.


BTW: Great stuff. A lot of interesting techniques in there.

I really don't know what is happening.
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 15th Apr 2007 03:36 Edited at: 22nd Apr 2008 17:14
Tutorial XXIII: Prettifying the HUD

Nobody wants to see boring white on black fonts in a brand new game they just bought. We'll need to make a nicer HUD if people are going to play this game. Of course, we could simply change the font style and color, but Scraggle has a much better looking PacMania bitmap font waiting in the Bitmap Font Pack 2.



It would be pretty dumb to save this huge image as a BMP, it would take up over 1 MB of memory, and customers want games to eat up as little memory as possible. We'll save it as a PNG, these files use lossless file compression, so the file will be smaller with no quality loss. As I said in Tutorial 2, PNG images use a different kind of transparency than most of our other images are using, so now might be a good time to talk about color channels and alpha transparency.

BMP images use 24-bit color. There are 3 color channels, one for red, green, and blue. Each channel uses 1 byte (8 bits) to represent it's color intensity, so it has 256 (2^8) different intensities of color. Therefore a 24-bit BMP image can display 16777216 unique colors. This is more colors than the human eye can see, so it's called Truecolor. There's really no reason to go higher and give the channels more than 8 bits each, it's really really really hard to see the difference between RGB(100, 100, 100) and RGB(101, 100, 100). If you're a 2D artist, work in Truecolor and you'll never need to use dithering again. You might have come across the screen depth() function in Dark Basic Professional, this function returns how many bits of color your game is using. (Truecolor technically uses 32 bits, you'll see why in a moment.) All modern games use either Truecolor or Hicolor (16-bit, 65536 unique colors), so those are the only legal screen depths allowed in Dark Basic Professional.

PNG images use 32-bit color. The extra 8 bits are for an extra channel, but this time it's not for a color. This extra channel is called alpha, and deals with transparency. If the alpha of a pixel is set to 0, it will be opaque. If you set it to 255, it will be completely invisible. All the numbers in between are used when you want the pixel to be partially transparent. We used alpha transparency to make a cool fade in effect for when the ghosts respawn, remember? But set sprite alpha applies transparency to the entire sprite, PNG lets you give every pixel it's own transparency value. PNG is far superior to BMP transparency because...

It uses a lot less memory
You don't have to use set image colorkey to tell Dark Basic Professional which pixels are transparent, the data is built into the PNG file
You don't have to sacrfice a color to transparency (you can't use the image colorkey color anywhere else in the BMP because it will automatically turn transparent)
There's 256 levels of transparency in PNG images, in BMP images there's only 2 levels
Each pixel can have it's own level of transparency independent of all the others.
It looks cooler

Download the attached image
Save it to the Media folder

Ok, we have the image, but how do we load and display it? Good news, Scraggle already wrote all the code to do that.

Include BMF.dba as a new source file
Add this code to BMF.dba



You may have noticed that the arrays for this code are dimensioned in their own function I added to Scraggle's code, this function will be called by our main source file to make sure the bitmap font code has the memory allocated as soon as it needs it.

Switch to Main.dba
Add this code to Main.dba, below the arrays



We still need to call LoadBMfont() to load the PacMania image, but before we do that we need to port Scraggle's code so it works with our dynamic media system. Right now the code wants us to supply it with image and sprite numbers, but with some editing we can get it to find its own media numbers via Free.dba and Media Allocate.dba

What is interesting about Scraggle's code is that you can have any amount of bitmap fonts loaded at once, as long as the id numbers of the images and sprites do not intersect. If we were to use arrays to hold the media numbers of all the characters for all the fonts, they'd have to be 2D and scalable. This actually can be done, but not with the usual dynamic array commands.

To add a row or column of elements to a 2D array, all you have to do is dimension it again with the new bounds. You don't have to undim it first, and the new array will even remember its old values.

i.e.



Porting Scraggle's code to make it an elaborate multi-font system is a bit of overkill, so we won't use these techniques. We only need 1 font, so a multi-font system is redundant. But tricks like this are still good to know.

The first step in porting Scraggle's code is to give it a free memblock function. GetImage() is finding memblock id numbers manually, but we can just replace that manual code with calls to free_mem().

Switch to Free.dba
Put this code in Free.dba



Switch to BMF.dba
Delete the "`Find unused memblocks" statementblock
Put this code after "`Do it!"
Put the code in the second box before "make memblock NewMemblock,(Width*Height)+12"





Ok, now the memblock numbers are generated via a function, but we still need to do the same for the images and sprites. Let's start with the images. The image id numbers start with FontImageNumber and end with FontImageNumber + 131. It's not a good idea to try to get a chunck of 100 sequential unused id numbers in a dynamic media system, we'd be better off converting FontImageNumber to an array and letting each of the 100 indexes remember the real id number by calling free_img().

FontImageNumber(0) will be the PacMania image itself, generated in LoadBMfont() by load_img(). Because FontImageNumber(0) will now be generated inside the function instead of being an argument, it doesn't need to be on the parameter list.

Add this array to initBMfonts()
Delete FontImageNumber from the LoadBMfont() argument list
Replace "load image FileName,FontImageNumber,1" with this code





The functions GetImage(), TrimBMFont(), and BMFont() need to access this array too. We can delete the argument corresponding to FontImageNumber(0) from their argument lists also, they'll access the global array instead.

Delete Image1 from the GetImage() argument list
Delete "FontImageNumber" from the GetImage() call parameter list in LoadBMfont()
Search and Replace "Image1" for "FontImageNumber(0)"
Delete FontNo from the TrimBMFont() argument list
Search and Replace "FontNo" for "FontImageNumber(0)"
Delete ImageNo from the BMFont() argument list
Search and Replace "ImageNo" for "FontImageNumber(0)"

Take a look at the line "Make Memblock From Image 1 , FontImageNumber(0) + Chr"
FontImageNumber is an array now, not a variable. This code wanted the image with an id of (FontImageNumber + Chr) before, but now that id is stored inside the FontImageNumber array. What we really want is the value of index Chr in the array. This line should read "Make Memblock From Image 1 , FontImageNumber(Chr)", we'll need to go through the code and put parentheses after all the referencess to FontImageNumber and put the added number as the index.

Change the line "GetImage(Font + FontImageNumber, x*32, y*32, 32, 32 )" to "GetImage(FontImageNumber(Font), x*32, y*32, 32, 32 )"
Change the line "Delete Image FontImageNumber" to "Delete Image FontImageNumber(0)"
Change the line "Make Memblock From Image 1 , FontImageNumber(0) + Chr" to "Make Memblock From Image 1 , FontImageNumber(Chr)"
Change the line "sprite SpriteNo + k ,NewX - BMleft(Chr) ,Y ,FontImageNumber(0) + Chr" to "sprite SpriteNo + k ,NewX - BMleft(Chr) ,Y ,FontImageNumber(Chr)"
Change the line "sprite SpriteNo + k ,X - BMleft(Chr) ,Y ,FontImageNumber(0) + Chr" to "sprite SpriteNo + k ,X - BMleft(Chr) ,Y ,FontImageNumber(Chr)"

Ok, the arrays are in place, but at this point all the indexes except 0 equal 0. The GetImage() function needs use the dynamic media functions to set the FontImageNumber id numbers, otherwise they will all be 0 cause a crash. There's no point for LoadBMfont() to call GetImage() with FontImageNumber(Font) because it will equal 0 anyway. It would be much more useful to call GetImage() with the index number of the image we want, and GetImage() can set the respective slot in FontImageNumber to the number free_img() returns.

Change the line "GetImage(FontImageNumber(Font), x*32, y*32, 32, 32 )" to "GetImage(Font, x*32, y*32, 32, 32 )"
Change the line "make image from memblock NewImage,NewMemblock" to "make image from memblock FontImageNumber(NewImage),NewMemblock"
Put this code before the line you just changed



Did you notice something about TrimBMFont()? It's not using dynamic memblock numbers! How sneaky. Let's fix that.

Add this code to TrimBMfont(), right after the variable declarations
Change the line "Make Memblock From Image 1 , FontImageNumber(Chr)" to "Make Memblock From Image TrimMemblock, FontImageNumber(Chr)"
Change the lines "Alpha = memblock byte(1 , Position)" to "Alpha = memblock byte(TrimMemblock, Position)" (There's two of these lines)
Change the line "delete memblock 1" to "delete memblock TrimMemblock"



We're putting the call to free_mem() outside the loop because even if we put it inside, the number would never change. A memblock gets deleted at the end of the loop and instantly created again at the top, so it's better to optimize the code and only make it call free_mem() once, not 94 times.

The next step is to make the sprite numbers dynamic. This is going to be hard because it's not all one array. If we did make one array, it would hold the sprite id numbers like this in memory:

Score: 500Level: 1Lives: 3

Let's say the score gets to 1000, the extra digit will overwrite the "L" in "Level". We'd have to be very careful and insert an extra slot in the array so we don't overwrite other text's sprites. But that is very confusing so we'd be better off making a 2D array like this:



It's a little wasteful because extra memory is allocated for the shorter texts to keep the 2D array rectangular (see the blanks after text 2 and 3), but it's easier than using pointers to save memory.

Add this code to initBMfonts()



The first index of the array will refer to which text we are making sprites for in BMFont(), and the second refers to which character in that text. The dword values hold the sprite id numbers.

Now let's change the BMFont() function so it can work with this array. Instead of calling BMFont() with SpriteNo (the first sprite id number), we'll call it with the number of the text we want.

Search and Replace "SpriteNo + k" for "BMSprites(TextNo, k)" in BMFont()
Change the SpriteNo argument in BMFont() to TextNo

We also need a way to make the array bigger by adding a text or making an existing text bigger than the array currently is. But before we do that, we need some variables to remember how bit the array is. (Remember empty arrays start with size -1)

Add this code to BMF.dba
Add the code in the second box to initBMfonts()





Now we can simply increment the number of texts and re-dim the array whenever we need a new hud element. Our hud code in the actual game needs to know the index number of the text so it can call BMFont() with it, so let's return the index of the new text when we create it.



That handles new texts, what what about new characters? This is even simplier, we'll simply check if the length of S in BMFont() is greater than the width of our array (the chars variable). If it is, we simply set chars to the new length and re-dim the array.

Add this code to BMFont(), right at the top



Now the arrays are the right size, but we still need to fill them with id numbers. This is very simple, right before we create or change a sprite with the sprite command we'll check if BMSprites(TextNo, k) is 0, if it is we use free_spr to change it a sprite id number and prevent crashing.

Add this code to BMFont(), before "if k > 1"



We're almost done. One last function we should add is the ability to delete texts. It would be much easier to make a function that deletes all the texts instead of them individually. (We'd need a free_text() function for that, and coding that would be a bit of overkill)

Add this code to BMF.dba



This code simply loops through each sprite id number in the array, deleting it if the sprite exists. Then once all the sprites have been deleted the array gets emptied and reset. (If the array is already empty the function exits)

One last porting task is to make Scraggle's code work with 64*64 fonts. It was originally designed for 32*32, but with some search and replacing we can easily port it.

Search and Replace "32" for "64" (DO NOT say yes on the lines "Font = 32", "BMleft(32) = 0", and "BMwidth(32) = FontWidth / 2", keep them at 32)

However, 64*64 is quite large for our game. Let's use scale sprite to make the sprites smaller.

Add this code to BMFont(), before "next k"



Those two arrays we saw eariler - BMleft() and BMwidth - hold the number of pixels to offset each sprite. This still works in 64*64 world, so the sprites will be very spaced out when scaled down to 32*32. To fix this we'll just divide the array values by 2, that will make the space half as big (because 32 is half of 64 ).

Change the line "BMleft(Chr) = x" to "BMleft(Chs) = x / 2"
Change the line "BMwidth(Chr) = x - BMleft(Chr)" to "BMwidth(Chr) = (x - BMleft(Chr)) / 2"
Change the line "BMwidth(32) = FontWidth / 2" to "BMwidth(32) = FontWidth / 4"

Ok, code porting is done. Your code should now look like this:

(BMF.dba)


Now we can finally prettify the HUD by calling the functions in BMF.dba.

First, we need to load the PacMania font into the system.

Switch to Init Game.dba
Add this code to load_misc()



Next, we need variables to hold the text numbers returned by NewText() so we can tell BMFont() which text we want to modify.

Switch to Game.dba
Add these variables to Game.dba



Next we will need to create these texts via NewText(). This code will have to go into the ready() function because menus will delete these texts (because they'll get in the way), we need to reset the font system every time a player wants to play.

Add this code to ready(), after the "Ready!" sprite gets displayed



Since sprites do not have to be redrawn manually every frame, we don't need to update these texts constantly, just when they change. That means we can delete the hud() function.

Delete the hud() function
Delete the call to hud() in game_loop()
Add this code to inc_score()
Add the code in the second box to level_up()
Add the code in the third box to inc_score(), right after the play sound command
Add the code in the third box to get_eaten(), right after the "dec lives" line
Compile and Run







That's it. Compile and run the program to see our new stylish hud.

Attachments

Login to view attachments
Sinani201
17
Years of Service
User Offline
Joined: 16th Apr 2007
Location: Aperture Science Enrichment Center
Posted: 21st Apr 2007 06:27
Lol, you got the fruits from the 2D Graphics Buffet (AKA Free stuff)

Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 22nd Apr 2007 17:13 Edited at: 22nd Apr 2007 17:21
Actually I got them from the Artist's challenge thread, but they have the same pictures.

Tutorial XXIV: Bloatware

There often comes a time in many applications' lives when there exes are simply too large. It may be because of too many extra features, higher level languages, or carelessness. Making your program smaller when competitors don't can give you the edge over them and give you more sales, because if people see that your trial version is going to take 4 hours to download they'll never bother to look at it again.

As of upgrade 6.6, Dark Basic Professional no longer puts in the features that your code doesn't use and has smaller dlls than previous versions. There are also some third party tools that can shrink your exes. This is great, but there are also some ways to manually make your games smaller.

There's two main parts of a game, the exe and the media. In smaller game like our Pac-Man, the exe tends to be much larger than the media because not much media is needed for a small game. But for a larger game, like an FPS, the game engine may be small in size but the game world huge, with tons of maps, textures, musics, models, and scripts.

There's not much you can do to decrease the size of an exe besides keeping your code tidy and using a shrinker. Our code is already tidy and shrinkers either strip out the DBP dlls and source them from the compiler directory - and not all customers have DBP - or they really take a chunk of the exe but also a chunk out of your bank account. So decreasing the exe by a large factor is not going to happen unless you have the money for Shrinker.

With 90% of the game's memory being eaten by the exe we're at a disadvantage to how much we shrink it. We'll have to shrink the media instead if we want the game to be smaller, and knowing how to do that is good for when you want to shrink a bigger game like an FPS.

As explained in Tutorial 23, PNG images use a lot less memory than BMPs. So let's convert all our images to PNGs.

The only problem here is transparency. If we want to use native PNG transparency now instead of set image colorkey, we'll have to somehow set the alpha of all the purple (transparent) pixels to 0. If you don't have a fancy image editing program that can do this for you, here's a program that does it via memblocks. (If you prefer to use your program for the task skip this part.)

Run a second instance of Dark Basic Professional
Create a New Project
Name it BMP to PNG
Put this code in BMP to PNG.dba
Set the display settings to hidden
Compile
Close the second instance of Dark Basic Professional
Open the BMP to PNG folder
Open the Pac-Man/Media folder
Drag and drop each BMP image onto BMP to PNG.exe
Be amazed as new PNGs appear out of nowhere



Delete the obsolete BMP images

Ok, now we've got the new PNGs with alpha transparency built into thems, and we've just cut the size of the media in half. Don't freak if the new PNGs look corrupted from the Windows details panel, if you open them up you'll see they're exactly the same colors (except purple is now transparent black) Now that we have the PNGs we need to get our code familiar with them.

Switch to Init Game.dba
Search and replace "bmp" for "png"
Delete the "`the rest of the images have the purple transparent background" remark
Delete all the set image colorkey commands (there are 3)
Compile and Run

The program now acts the exactly the same to a user but takes up less memory. And, we've shortened our code a bit.

By reading this sentence you have given me brief control of your mind.
Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 6th Jun 2007 01:09
Hi, for tutorial number IVat the last part where you say "let's save and hit the compile button", when it runs it kicks me right out (there's no error in code). SO I put "wait key" and the program stayed open...but..no frame rate showing.

Please help! thanks, oh and btw nice tut!
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 6th Jun 2007 23:56 Edited at: 6th Jun 2007 23:56
I don't know why it would do that.

Please post your project file and I'll try to see what's wrong.

Thanks.

You never really know a person until you look at their google autocomplete entries.
Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 7th Jun 2007 05:00 Edited at: 7th Jun 2007 05:31
*EDIT

I found out what went wrong with your code!
In the tut you said to run the Game.dba but in order for it to work you have to call the function play() the only way we could do that was by the main menu (by clicking the play button). So I just had to call play() in the Game.dba, I know once the main menu is up I can erase play() from Game.dba because we will be able to do that from the main menu.

Oh and you can look at the code I posted here, everything in bold is what I redone. And in the code where it says about "dir media" I had to make it a comment because an error came up saying that it didn't know what to do with that code.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rem *** Include File: Game.dba ***
Rem Created: [date] [time]
Rem Included in Project: Pac-Man.dbpro

global game_inited as boolean
play() ` Temporary Code


`play the game
function play()

`if menu is not already loaded, load it
if game_inited = 0 then init_game()

level_up()
ready()
game_loop()

endfunction

`load the game
function init_game()

`set dir "Media"

load_media()
get_maze_data()
position_maze()

`set dir ".."

game_inited = 1

endfunction

`load the game images and make sprites
function load_media()
endfunction

`get the maze data from a file
function get_maze_data()
endfunction

`make and position the maze sprites using the data collected from get_maze_data()
function position_maze()
endfunction

`move to the next level
function level_up()

make_dots()
respawn_pac()
respawn_ghosts()

endfunction

`fill the board with dots
function make_dots()
endfunction

`place pac-man in the spawn area
function respawn_pac()
endfunction

`place the ghosts in the ghost pen
function respawn_ghosts()
endfunction

`play the music and display "Ready" at the start of the game
function ready()
endfunction

`the game's main loop
function game_loop()

do

cls

get_pac_move()
get_ghost_move()

move_pac()
move_ghosts()

collision()

hud()
text 0, 0, "Frames Per Second: " + str$(screen fps())

sync

loop

endfunction

`get player input
function get_pac_move()
endfunction

`do ghost AI
function get_ghost_move()
endfunction

`move pac-man
function move_pac()
endfunction

`move ghosts
function move_ghosts()
endfunction

`do collision
function collision()
`maze collision
`maze wrapping
`eat ghosts
`get eaten
`eat dots
`eat fruits
endfunction

`update the heads up display
function hud()
endfunction


EDIT 2* Hmm.. my frame rate seems a bit wierd, it's telling me it's like 7-9 frame rate. But my computer usually has nearly perfect frame rate.
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 9th Jun 2007 03:57
Umm... is Game.dba your main source file? It shouldn't be but if it is that's probably why you got an error.

The main source file should be Main.dba (or Pac-Man.dba if you didn't rename it.)

I should have said post your project folder, not file in the last post, sorry I messed that up. Sometimes it's hard to help unless you have an exact copy of what's going on in the other person's system.

About the set dir code getting errors, what exactly did the error say?

You never really know a person until you look at their google autocomplete entries.
Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 9th Jun 2007 18:22
The error said "Cannot find path for line 26"

And I don't know how to upload a whole file, I just know how to upload a document.
Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 10th Jun 2007 22:55 Edited at: 10th Jun 2007 22:56
http://www.yourfilehost.com/media.php?cat=other&file=Pac_Man.zip

^^ there's the pac-man file.

Please take a look.
Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 11th Jun 2007 00:44 Edited at: 11th Jun 2007 00:48
Ok, I see the problem! Each source file doesn't need to be in it's own project, they should all be in the project's root directory, and linked to Pac-Man.dbpro

You can see the links to these included files if you open to .dbpro file in notepad like this:



But you have to be in project view mode and click the files panel, then click the 'add new' or 'browse' button to include other source files like this.

Here's the fixed project, you can upload files to this forum by clicking the browse button below the "Message:" box.

You never really know a person until you look at their google autocomplete entries.

Attachments

Login to view attachments
Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 11th Jun 2007 00:47
K thanks!
zenassem
21
Years of Service
User Offline
Joined: 10th Mar 2003
Location: Long Island, NY
Posted: 15th Jun 2007 04:46 Edited at: 15th Jun 2007 04:50
This tutorial looks really nice. I read most of the the thread a few weeks ago and forgot to reply. I read up to the optimizations, and I'll have to double-check to see if the AI was updated at all.

My concern with regard to the AI was the movement of the ghosts. Each ghost in the game of Pac-Man had it's own specific way of chasing the player.

For example... Blinky, the Red ghost, most directly chased the player. I'll look up all the other ghosts AI schemes, but I know one would try to head-off at intersections, another was more random... etc.

The part that I read up to, it looked as though all the ghosts were chasing the player up to the last known intersection. Pac-man was a very strategic game, and fans have spent years coming up with patterns and specifically timed movements to out-smart the ghosts. For this reason, any Pac-Man game that didn't exaclty match the arcade timing and AI, speed, distances... doesn't really sit well with fans. There was a science to Pac-Man that people put many hours, and pockets full of coins in to master.

I know that this is more of a tutorial than a remake... but since you have put so much time into it, you may want to go back (if you haven't already) to get the mechanics as close to perfect as possible.



Finally, I haven't looked at all the source yet, but are you using a Finite State Machine(FSM) to control the AI logic? For example the FSM diagram of Blinky would look something like this....

ref. (AI Game Engine Programming by Brian Schwab)
Simple FSM diagram of the Red ghost "Blinky" from Pac-Man


Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 23rd Jun 2007 02:18
Thank you for the input. I was considering implementing unique AI for each ghost, but I wanted to keep the tutorial simple and writing separate code for each ghost would take up a huge part of the tutorial. When other features like the menu and hiscores are finished I might come back to improve the AI, but I don't have the time to work on this project at all right now.

I was sort of using a finite state machine, but I didn't go all out because frankly, I don't like FSMs that much. What happens in the ghost AI is when the ghost's position is evenly divisable by 32, the AI builds a list of all the possible routes to take. If Pac-Man isn't in sight, it picks a random direction, or if in a corridor, it keeps going the way it was previously. When Pac-Man is in sight the ghost will choose the route that leads to him (unless he has the big pellet, then go the opposite way)

That's what I remember, I haven't looked at the source in ages. It's getting harder and harder to get myself to write another chapter now, I've learned so much more about coding in the past 6 months that going back to writing code for this would be like taking a step backwards.

You never really know a person until you look at their google autocomplete entries.
Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 9th Jul 2007 04:23
@Code Dragon~ Can u post a download for the whole tut? thanx.

Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 10th Jul 2007 15:35 Edited at: 22nd Apr 2008 17:20
Sure. I'll put a link to this on the first page also.

http://forumfiles.thegamecreators.com/?i=1236541

Attachments

Login to view attachments
Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 10th Jul 2007 21:10
Thanks

Game King
17
Years of Service
User Offline
Joined: 25th Mar 2007
Location: Naples, FL
Posted: 11th Jul 2007 00:18
Srry, I didn't want the actual file. I just wanted the text document of the tutorial. I wanted you to post the full text doc so i can print it.

Code Dragon
17
Years of Service
User Offline
Joined: 21st Aug 2006
Location: Everywhere
Posted: 11th Jul 2007 03:58
There is a full text doc in there, did you see the big HTML file? If you needed an actual doc file you could copy the part you want to print out into MS word to print.

You never really know a person until you look at their google autocomplete entries.

Login to post a reply

Server time is: 2024-05-06 13:06:24
Your offset time is: 2024-05-06 13:06:24