We're going to keep everything in the terminal, since dealing with graphics libraries takes us a too far off-topic for one lab period. However, we will use a library called "curses" (actually "ncurses") that allows us to write to arbitrary positions in the terminal window and to read keystrokes in a "nonblocking" manner.
#include "easycurses.h"
in your
.cpp files, and you need to compile with the -l ncurses
compiler flag, like:
g++ -Wall p1.cpp easycurses.cpp -l ncurses -o p1
Download p1.cpp under your account. This source code demonstrates some simple aspects of ncurses. Here's a few comments:
|
Initializing Ncurses
This line of code should appear at the point in your main() when you're ready
to go into "ncurses mode". When this is executed, the screen goes blank and
you are ready to write to arbitrary locations in the terminal window.
Draw a character
This draws the char 'B' at position (20,30) in the terminal. How about a pause
after you draw? The following sleeps your program for 800000 microseconds
(i.e., 0.8 second):
Non-blocking I/O with inputChar()Ncurses (at least as we've initialized it) changes the model for how we read data. You fetch the next character with a call toinputChar() .
The funny thing is that this is "non-blocking I/O", which means that you don't
hang around and wait for input to come.
If you call
Contrast this with inputChar() and nobody presses a key,
inputChar simply returns immediately with
an error code.
cin >> c or cin.get() .
Thus, this loop just spins waiting for inputChar() to report that the user has
pressed q.
Exiting NcursesTheendCurses() function call is required in order for ncurses to
clean up after itself and restore the terminal window.
|
Compile p1.cpp:
g++ -Wall p1.cpp easycurses.cpp -l ncurses -o p1then run it. Use 'q' to quit.
What if we need to use cout
for debugging information? Of course we do!
Well this is slightly problematic because ncurses now controls the screen.
Suppose in the Part1 code we wanted to print out a debugging
message "Printed a B!"
just after writing the "B". We'd
just add the line:
cout << "Printed a B!" << endl;
... immediately after the drawCharAndRefresh('B',20,30)
.
Compile and run that.
What you will find is that this message pops up in the middle of our ncurses screen and messes things up. The "C" is no longer printed immediately to the right of the "B". It's a real problem when your debugging messages cause new bugs! So how can we write out debugging messages in an ncurses program?
cerr
) instead of standard out
(cout
) for the debugging message.
This doesn't solve the problem immediately, but it helps.
To see how ...
cerr << "Printed a B!" << endl;
... and run your program like this:
~/$ ./p1 2> err
What this does is run your program as usual except that any
output written to standard error (cerr) gets "redirected" to
the file err
, which is created or overwritten
each time this is run. Let your program run to completion,
then give the command cat err
to see what is written.
~/$ tail -f err
Go back to your original terminal and type something like
this:
~/$ echo "whoooooooo" > err
You should see the whoooooo pop up in the other terminal.
Spooky, eh?
~/$ ./p1 2> err
... and watch as your ncurses window is pure and unblighted
by debugging messages, but the message does pop up at the
appropriate time in the second window.
2> err
is telling the shell to
redirect standard error (cerr in our program) output to the
file err.
tail -f
is constantly
monitoring the file err, printing out any new lines that get
written to the file.
Download rundebug to the same directory as your other files.
This automates Part 1b above. The script will open a second debugging
terminal for you automatically and then run your p1
program. Just
download it, and turn its executable bit on:
chmod 755 rundebug
Now instead of running ./p1
you just:
./rundebug p1
After running, close the debug window that pops up.
You don't need to submit any file for part 1.
drawChar()
and
drawCharAndRefresh()
.
Q: What's the difference between drawCharAndRefresh() and drawChar()?
A: drawCharAndRefresh() is simply drawChar() + refreshWindow()The function
refreshWindow()
makes what you drew actually appear
in the screen window.
In other words:
Due to this difference, if you want to draw multiple objects at the same time,
you should call drawChar()
multiple times and then call
refreshWindow()
once. On the other hand, if you just call
drawCharAndRefresh()
multiple times, those objects won't appear at
the same time; rather, they will appear one by one.
Write a program from scratch that works as follows:
|
~/bin/submit -c=IC210 -p=lab10 p*.cpp
Copy p2.cpp into p3.cpp
Now we will animate things, i.e. each character will move on the screen.
// read your characters as in part 2
// draw them with drawChar() calls
// call refreshWindow() (only once) to show what you drew on the screen
// sleep for 80,000 micro-seconds (i.e., 0.08 sec).
for(int frame=0; frame < 20; frame++)
{
// 1. Move all characters one step:
// a. erase them in the old positions with drawChar() calls
// b. compute the new positions for characters (based on the direction of each character)
// c. draw them in the new positions with drawChar() calls
// 2. Call refreshWindow() (only once) to show what you drew on the screen
usleep(80000); // sleep for 80,000 micro-seconds (i.e., 0.08 sec).
// This corresponds to flipping to the next "frame" in an animation.
// The program is drawing about 12.5 (= 1/0.08) frames per second.
}
// exit the program right away; you don't need to take any user input
drawCharAndRefresh()
!!!
For part 3 and later parts:
drawCharAndRefresh()
at all.
drawChar()
and refreshWindow()
.
drawCharAndRefresh()
, the screen will blink a lot. This is
because the program would show intermediate steps on the screen. In particular,
drawCharAndRefresh()
calls refreshWindow()
(i.e., drawCharAndRefresh() = drawChar() + refreshWindoow()
).
drawCharAndRefresh()
will refresh the screen, showing no
character on the screen for a moment.
Note also that refreshWindow()
should be called exactly once every
0.08 second. This is because refreshWindow()
is a costly
function; physically refreshing the entire screen is not as cheap as changing
some variables. Too many calls to refreshWindow()
will slow down
your program and you will feel it.
Therefore, many calls to drawCharAndRefresh()
will make your program
blinking and slow! Don't use drawCharAndRefresh()
at all!
~/bin/submit -c=IC210 -p=lab10 p*.cpp
Copy p3.cpp into p4.cpp
Of course at this point your characters move only to the right, and things are not that interesting. So, to add a little drama, let's say that at each step, we make the character wander around a bit.
srand(time(0)); // need: #include <cstdlib>
// #include <ctime>
or else it will do the same thing every time — that would be boring!
Now in the main loop, immediately before actually moving, each character should first determine the new direction as follows:
Recall: The following code will print "yes" with probability 1/5:
if( rand() % 5 == 0 )
cout << "yes" << endl;
You have to call rand() for each character to consider the probability of turning individually.
drawChar()
.
At this moment, each character should remember its direction. Otherwise, you won't be able to say:
Note the direction could be E, W, S, or N.
Note: Character A
may have a different direction from Character
B
. Each character should remember its own direction.
int r = rand()%2;
cerr << endl << "frame = " << frame << "*****************************" << endl;
rand()
call. Since a second rand() call
outputs a different number, store the output of the rand() call and use it
for cerr and your if-conditions as follows:
int r = rand()%5;
cerr << "rand()%5: " << r << endl;
int r = rand()%2;
cerr << "rand()%2: " << r << endl;
drawChar()
call. Whenever you
call drawChar(c, row, col)
, do:
cerr << "drawChar(" << c << ", " << row << ", " << col << ")" << endl;
cerr << "character " << label << ": direction = " << direction << endl;
In the above snippet, label
and direction
indicate
some variable(s) in your code for the label of the character and its new
direction.
rundebug
script (read Part 1).
~/bin/submit -c=IC210 -p=lab10 p*.cpp
Copy p4.cpp into p5.cpp
Instead of rendering exactly 20 frames, your solution for this part should run forever until the user enters 'q' (as with part 2). If you do this, at some point your characters move off the screen, and ncurses goes crazy --- nothing it draws makes sense anymore. Just kill the program with ctrl-c, and don't worry about it.
Now, though your animated character makes random turns, eventually it's going to make its way off the screen and strange things will happen. It'd be much cooler if the screen acted like a walled-in space that the character cannot escape. So let's do that. The only real difficulty here is that you need to know the height and width of the terminal window (in characters). Our easycurses function for that:
void getWindowDimensions(int& row, int& col); // declared in easycurses.h
So the call getWindowDimensions(h,w)
sets h to
the number of rows on the screen, and w to the number of
columns. Note, then, that the valid positions you can
drawChar(ch,row,col)
to are when 0 ≤ row < h and 0
≤ col < w.
Now that you can get the height and width of the terminal window, when you come to a "move" step you must check to see whether the move in question would take you off the screen. If it does, then instead of moving just change direction and leave it at that. Make the character "bounce" by simply reversing direction (north goes to south, east to west, etc). If you do this, your character will be walled in and will never leave the terminal window.
~/bin/submit -c=IC210 -p=lab10 p*.cpp
~/bin/submit -c=IC210 -p=lab10 p*.cpp
char kb = inputChar();... and if kb is an 'a', change the one character's direction to west, a 'd', change it to east, an 's' change it to south, and a 'w', change it to north. Note that this one character won't be subject to the random direction changes, and we won't worry about the walls for him ... it'll be up to the user to keep the player on the board.
(Typing a 'q' should still be used to quit the game.)
~/bin/submit -c=IC210 -p=lab10 p*.cpp
~/bin/submit -c=IC210 -p=lab10 p*.cpp