You may think that you have already been debugging and in a way you have, but you could have gotten through the previous two chapters without setting a single breakpoint because the programs are simple enough that it is unnecessarry. Games on the other hand consist of thousands of lines of code so it is easy to write a program that is complex enough that debugging without breakpoints becomes nearly impossible.
We are only one section away from learning how to create a computer game. At this point you have learned essentially everything you need to in order to create a game. You may be tempted to skip this section and start writing games right away. However, if you ever try to write your own game you will need some serious debugging skills. Complex programs can easily become nightmares to debug if you don't really know what you are doing. For this reason, each problem in this section contains a description of a single program that is riddled with errors. Some programs won't even compile, some fail during runtime, and some run fine but don't do what they are supposed to do. You can find the programs for each problem in the folder Unit1/Debugging.
Excercise 5-1. Open the project DebugProject1 in Unit1/Debugging. This program should print the key that is pressed and held to the screen for all A..Z. Compile this program. If it won't compile, then fix any errors.
There are two syntax errors in the code. The first is in the for loop in which a do is omitted from the end. The second is after Chr(K) where a + is omitted for string concatenation.
Now run this program and test it by pressing different letter keys (A, B, C, etc). The first step is to identify the bug before trying to fix it in code. What is the bug in this program?
The program prints a garbled mess to the screen and only prints for the A key.
Now find the bug in the code and fix it.
First, since it only prints anything for the A key and nothing else for any other keys, the first place to look would be the if statement:
if Keyboard[65].Down then
This should read Keyboard[K].Down. 65 is the ASCII character code for A which is why text is printed only when A is pressed. When we fix this the program runs correctly. The problem before was that when A was pressed, Keyboard[65].Down would have been true for every iteration of the loop which means that every letter from A to Z was printed to the screen at the same time.
Excercise 5-2. Open the project DebugProject2 in Unit1/Debugging. This program should be identical to the flower program we did in
Section 3.10. Try compiling it. If it won't compile, then fix any errors.
There is one syntax error. A semicolon is placed at the end of an if statement just before the else. Semicolons should never preceed else:
if K mod 2 = 0 then Points[K] := PolarToXY(
Flower.Position.X,Flower.Position.Y,
360/N*K,Radius); // <- here's the problem
else Points[K] := PolarToXY(0,0,
360/N*K,Radius/2);
When you run the program it clearly does not draw flowers. There are actually 2 bugs preventing this. Since you know the problem is in drawing. Start by looking for errors in the DrawFlower subroutine and any subroutines it calls. These 2 bugs will be difficult to find, but really examine the logic of the code before looking at the solution. Although breakpoints probably won't help at this point, it might be useful to comment out the code in OnEnterFrame that draws flowers and just call some of the subroutines like PolarToXY to verify whether or not they are working as they should (i.e., try drawing a circle with it). Remember that (* *) comments can be used to block comment out multiple lines of code.
The first bug is in PolarToXY. Theta needs to be multiplied by PI/180 because sin and cos in Object Pascal expect to get their arguments in units of radians and not degrees. The second bug is far more buried and more difficult to find:
if K mod 2 = 0 then Points[K] := PolarToXY(
Flower.Position.X,Flower.Position.Y,
360/N*K,Radius)
else Points[K] := PolarToXY(0,0, // <- here's the problem
360/N*K,Radius/2);
Flower.Position.X and Flower.Position.Y should have been passed to the second PolarToXY instead of 0,0. This problem illustrates a very important point in programming. You should first verify that all your subroutines work as they are supposed to independent of any other code. This illustrates the utility of encapsulation. You encapsulate the code for computing polar coordinates into the PolarToXY subroutine and verify that the code does what it is supposed to. This is not only a good programming technique, but it is also a good place to start with debugging since you can test separate parts of the code and then set aside the parts that definitely work so that you can search in the parts that might contain the bug.
Excercise 5-3. Open the project DebugProject3 in Unit1/Debugging. This program should be similar to the fireworks program we did in
Section 4.6. Try running it and identify what the error is.
The flare simply burns out rather than detonating. When fixing the bug, keep in mind what causes a flare to detonate in the code. Hint: You will find that setting a breakpoint in TFlare.StepAndDraw and then stepping through the code (F8) is far more useful than trying to examine the code while it is not running. When you set the breakpoint, do so intelligently. In other words, there is no reason to set a breakpoint in the loop because this part of the code does not control detonation. Try setting it on case FlareType of.
Fix the bug identified in the previous problem by setting a breakpoint and stepping through the code (F8).
FlareType determines whether or not a flare detonates. In StepAndDraw you can see that FlareType is set to ftBurnOut. Looking back to TFlare.Create, we can see that FlareType is set to ftBurnOut by default. Change this to ftDetonator.
Fixing the previous problem introduces another (rather cool) bug. What is it?
It appears that every flare that is generated tends to detonate.
Fix the new bug by considering the code near the creation of new flares after the detonation of a flare.
When any new flares are created they are now by default detonating flares. What we really want is new flares created by a detonation to be burn out flares. We have to set FlareType to ftBurnOut explicitly in the Detonate procedure.
Excercise 5-4. Open the project DebugProject4 in Unit1/Debugging. This program should be similar to the prime number exercise we did in
Section 3.6. Try running it and identify what the error is.
The most obvious problem is that it just lists the numbers 2 to 21 instead of the first 20 primes.
Set a breakpoint at the beginning of the main while statement. Step through the code and see what happens. Remember that F8 steps line by line and F7 steps into a subroutine. For example, try stepping until the following line is highlighted:
if IsPrime(CandidateInteger) then
Then step into the IsPrime subroutine using F7. Step through each line of the subroutine and observe the value in each variable. You can also use Run -> Evaluate/Modify from the main menu. This will allow you to evaluate expressions such as K mod X to see what the result is. Continue doing this until you identify the first problem.
The first problem is that K mod X should be X mod K. The reason is that we want to test for divisibility of the CandidateInteger by every number less than it, not the other way around.
Once the previous bug is fixed run the program and see if everything works (i.e. whether or not the first 20 primes are listed).
The program almost works. The problem is that the number 4 somehow made it into the list of primes.
Repeat the process of setting a breakpoint as done previously and step through the while loop until 4 comes up as a CandidateInteger. Now step into IsPrime and try to determine what the problem is.
This bug can be fixed by changing the start index of the for loop in IsPrime to 2. The issue is that 4 was not tested for divisibility by 2. The other option that will work equally well is to exclude all even numbers from testing by incrementing CandidateInteger by 2, but if you do this you will also need to add 2 to the list before the main while loop or it will be omitted. The final correct list should read as follows:
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
Excercise 5-5. Open the project DebugProject5 in Unit1/Debugging. This program is similar to the one that relates triangles, circles, and waves from
Section 3.1. Try running it and identify what the error is.
The sine wave seems to be fine but the circle and triangle are not drawn properly. This probably means we can safely disregaurd everything below the comment {Draw the sine wave} and focus on the code above it to find the bug.
Remember the first step in debugging. Test the subroutines to make sure they work. Try commenting everything out (using parenthesis asterisk comments (* *) and playing with PolarToXY with values you know should work. PolarToXY, as it is written, takes a Theta value and Radius and outputs X and Y coordinates to the last two arguments passed to it. Since you know that 0 degrees is the X axis, passing a radius of 5 and a theta of 0 should return the coordinates X = 5 and Y = 0. Try it and see what the result is (use breakpoints to evaluate the contents of the output variables after the subroutine has finished running).
If you try running something like PolarToXY(0,5,X,Y) you will notice that X and Y are definitely not being assigned the correct value. You should try to step into PolarToXY to see what is actually happening inside the procedure. For optimization reasons, you cannot examine the last line (in this case the OutY variable) after it has run because the variables are cleaned up before the subroutine exits. To prevent this, write some meaningless code below OutY like the following so you can set a breakpoint there:
procedure PolarToXY(Theta,Radius : Double;
OutX,OutY : Double);
begin
OutX := sin(Theta*PI/180)*Radius;
OutY := cos(Theta*PI/180)*Radius;
if OutY = 0 then // Set breakpoint here
begin
end;
end;
After setting a breakpoint and examining OutX and OutY as suggested in the previous problem, can you identify the bug?
sin and cos are backwards. Running PolarToXY(0,5,X,Y) should give OutX = 5 and OutY = 0 but instead gives OutX = 0 and OutY = 5. This is because sin gives the opposite leg (i.e., the y axis) and cos gives the adjacent leg (i.e., the x axis).
Now that the first bug is fixed, uncomment the original code and try to run the program. You will notice that it still doesn't work. Try examining the call to PolarToXY to see if there are any obvious problems.
The formal arguments have the order Theta and then Radius. The actual arguments passed on line 31 have it backwards as Radius and then Theta are switched. As the call is written, Radius is passed into the Theta variable and Theta is passed to the Radius.
After fixing the previous bug, you will notice that the program still does not work. Is there something else wrong with PolarToXY?
OutX and OutY are assigned properly. The problem is that they are discarded because as PolarToXY is defined right now they are arguments passed by value. They should be arguments passed by reference. You can fix it by adding the var directive before OutX and OutY. Now the program should run correctly.
Excercise 5-6. Open the project DebugProject6 in Unit1/Debugging. This program simply creates a list of shapes and then writes their areas to the screen. This program has a number of compiler and design errors. Try compiling it to find the compiler errors. Attempt to fix all the compiler errors before looking at the solution.
The first error the compiler locates is on line 12 of the Shape unit. Area is declared as an abstract method so it should not have any declaration. You can simply delete this function entirely (but do not delete the declaration).
Next on line 9 of the Square unit, a declaration has been made for Area but this method is not implemented. It appears to be implemented as the function Area, but without the TSquare prefix this function is not treated as part of the class TSquare. Correct this error by adding TSquare. to the function's name on line 14. You might also notice that the function Area does not return a value. Although this is not a compiler error (it is a design error) we might as well fix it now by adding Result := Side*Side.
The next error is detected on line 11 of the Circle unit but is actually caused by line 8. Area has been declared as an abstract method when it really should be an override of the abstract method Area from TShape. Change virtual; abstract; to override;. You might also notice that TCircle descends from TObject instead of TShape. This prevents the abstract method Area from being detected and results in the "no method in ancestor class" error. Correct this by adding Shape to the uses clause change TObject to TShape. Also, TCircle.Area is in the wrong place: it belongs after implementation.
Next, in the main project source file TList is an undeclared identifier because the Classes unit has not been used in the uses clause. Finally, on line 26, TShape is an undeclared identifier because the Shape unit has not been used in the uses clause. After this correction is made the program should successfully compile.
Although the program now compiles, it crashes with a runtime error (that in some cases even can crash the Lazarus IDE). Any ideas? Hint: Since the IDE does not help us out by locating the line on which the runtime error occurs, the best way to fix this is to comment out all the code between the begin-end block (using (* *) or { } comments) and then uncomment each line to see if it causes an error.
Commenting out the code reveals that the error occurs in the for loop. The only thing that could be logically causing the error in the for loop is the call to Area. Since Area could be called from either TSquare or TCircle we will have to look in both. One good idea to look for the bug would be to not add any circles to the list and see if it occurs. If it does cause an error, remove all the squares and add the circles back in and see if it occurs. Note this can easily be done by commenting out the Shapes.Add line for circles and then for squares. Do this in the next problem if you have not already found the error.
Assuming you are still looking for the error from the previous problem, do what the solution says to do before looking at the actual solution.
Commenting out the circles still allows the same error to occur but commenting out the squares causes a slightly different error. This is what makes runtime errors so much more difficult to fix than compiler errors. Not only do we not know where the error is occurring, but once we prevent that error another one pops up. We'll give you a hint. Look at the bounds of the for loop.
Fix one of the runtime errors by modifying the bounds of the for loop.
TList is a 0-indexed array so we should loop from 0 to Shapes.Count-1 and not Shapes.Count.
After fixing the last runtime error, the program will work if both Shapes.Add(S) lines are commented out. Thus, we should look in the Square unit for the source of the original error.
In the Square unit you will notice that Area is not declared as an override. This means that when TShape.Area is called, it does not bind to TSquare.Area and instead hits an invalid memory reference. Fix this error by placing the override directive after the declaration for the Area function in TSquare. Once you fix this bug and uncomment Shapes.Add(S) you should see the following in the terminal with no errors:
78.54
25
4