Homework 3: Define the Sprite
Class
The New Oxford American Dictionary defines a sprite as
- An elf or fairy.
- A computer graphic which may be moved on-screen and otherwise manipulated as a single entity.
As cool as it might be to summon supernatural beings from C++, we're going to focus on the second definition. A familiar example of a sprite (in the computer graphics sense) is your mouse cursor, which is free to move around on the screen on top of the other content, and can at any moment be seen as having not just a position but a velocity as well (i.e., how quickly it's moving and in what direction). In classic arcade games like Pac-Man, the player character is a sprite that moves around the screen, interacting with other sprites like the ghosts.
In this assignment, the terminal screen is our canvas, and our sprite is a collection of ASCII characters that form an image. But the concept is the same, they have a starting position, a size, and a velocity. We'll be implementing the Sprite
class to represent these ASCII art sprites in our asciimation program.
Your Task
Complete the sprite.hpp
Header File
Oooh… I noticed that there are
// TODO
comments in thesprite.hpp
file for everything that needs to be done.That's right, but you should still read over this section before diving in.
Sure. I'll totally do that. cough cough
Add Private Data Members to the Sprite
Class
Our Sprite
class needs to store three things:
- A one-dimensional fixed-size array of characters (of size
MAX_SPRITE_DATA
) that holds the characters that make up the sprite's image. - A bounding box that defines the position and size of the sprite on the screen.
- A velocity (in x and y directions) which defines how the sprite moves on the screen.
You will need to define private data members for these three things (the velocity will be two int
s, one for each direction). Follow the CS 70 naming conventions for good and consistent naming of data members.
Hay! Why is the array a fixed size? Can't the sprites vary in size?
At this point in the course, we haven't learned the techniques necessary to implement dynamically sized arrays, so we'll use a fixed-size array for simplicity. The maximum size is large enough to accommodate most sprites, and we can keep track of the actual size using the bounding box.
It's still a bit wasteful, isn't it? If I have a tiny sprite, I'm still using up space for the maximum size. I heard about
std::vector
, can't we use that?It's a bit like the difference between buying a cake and making one yourself. If you want to learn cookery, you need to learn all the basics of baking, but if you just want to celebrate someone's birthday, buying a cake from a shop might be the most expedient option. In other code you write, go ahead and use
std::vector
if you want, but don't usestd::vector
in code you write for CS 70 until after we've explored how it works inside.
Add Public Member Functions to the Sprite
Class in sprite.hpp
We've already provided declarations of the class's two constructors in sprite.hpp
:
- A parameterized constructor that takes three parameters:
- A reference to a string that represents the filename containing the characters we should read into the
Sprite
's contents, and - Two
int
s giving the initial location of theSprite
.
- A reference to a string that represents the filename containing the characters we should read into the
- A second parameterized constructor that takes an input stream instead of a filename, to make testing easier, but otherwise works the same way.
You need to add the following public member functions to the Sprite
class:
- A
getBoundingBox()
function that returns the sprite's bounding box indicating where it is on the screen and how big it is.- The function doesn't take any parameters.
- The return type should be
BoundingBox
.
- A
setVelocity(vx,vy)
function that sets the sprite's velocity.- The function should take two
int
parameters. - The function doesn't return anything.
- The function should take two
- A
setPosition(x,y)
function that moves the sprite to a new location.- The function should take two
int
parameters. - The function doesn't return anything.
- The function should take two
- A
getCharAt(x,y)
function that returns the character at the specified position in the sprite's character array.- The function should take two
int
parameters: the column and row within the sprite. These coordinates are relative to the top-left corner of the sprite, so(0,0)
is the top-left character,(1,0)
is the character to its right, and so on. They are not screen coordinates. - The function should return a
char
. - You can assume that the inputs will always be valid (i.e., within the bounds of the sprite's dimensions).
- The function should take two
- A
move(screenBounds)
function that updates the sprite's position based on its velocity, wrapping around the screen if necessary.screenBounds
is aBoundingBox
representing the size of the screen and should be passed by constant reference.- The function doesn't return anything.
Add the declarations of these functions to sprite.hpp
with appropriate interface comments (in the next section, you'll implement them in sprite.cpp
).
Disable the Default Constructor, Copy Constructor, and Assignment Operator
It won't make any sense to have default Sprite
s, and we also don't want anyone to accidentally work with a copy of a Sprite
rather than the original. So we want to disable the default constructor, copy constructor, and assignment operator. We've already shown you how to disable the copy constructor and assignment operator in class BoundingBox
, so you can follow that example.
We cover how to do this in the lesson pages on synthesized constructors and the assignment operator. If you've not got to that part of this week's lessons yet, you can skip this part and leave it as a
// TODO
comment for now, and come back to it later.
Implement the Member Functions in sprite.cpp
With the header file done, it's time to implement the member functions in
sprite.cpp
.
If you want to be able to test your code as you write it, you can implement stub functions for your unwritten functions that either return default values (or do nothing for
void
functions), or you could have them throw astd::logic_error
exception to indicate that they haven't been implemented yet. Then you can run the tests and see which ones fail, and as you implement each function, the corresponding tests will start to pass. See below for how to build and run the tests.
Finish the Constructors by Implementing Sprite Loading
sprite.cpp
already contains implementations of the two constructors and some of the code for readFromStream
. You will need to fill in the rest of their implementations.
Note: To receive full credit for writing idiomatic C++ code, all your constructors must properly use the member-initialization list. We recently covered this topic in lessons , and we also remind you about how we expect you to use this C++ feature in Section 4.1 of the CS 70 Idioms Page. These constructors are a good reminder of what member-initialization lists look like.
The main work left to do is to finish the readFromStream
function, which reads the sprite data from a stream. The sprite files have a specific format:
- Line 1: The width (must be positive)
- Line 2: The height (must be positive)
- Line 3: All width × height characters of the sprite in a single line.
The provided code in readFromStream
includes error checking for the dimensions. You'll need to read the characters into your contents_
array. Because the sprite format itself collapses the 2-D grid of characters into a single line and we also store the data in a 1-D array, you can just read the characters in order into the array. In fact, we require you do it that way rather than use a nested loop to read the characters row by row.
The readFromStream
function should throw a std::runtime_error
exception if the sprite file is malformed. There is already some checking code but your loop should also throw an error if it can't read enough characters.
Implement getCharAt
The getCharAt
function should return the character at the specified position in the sprite's character array. Remember that the sprite's character array is a 1-D array, so you'll need to do a bit of arithmetic to convert the 2-D coordinates into a 1-D index. You can assume that the inputs will always be valid (i.e., within the bounds of the sprite's dimensions), but see the hints below for some ideas about how you might handle invalid inputs if you wanted to.
The testing code in
sprite_test.cpp
includes a test forgetCharAt
that you can use to check your implementation. And check out the Helpful Hints below for some more tips on how to implement this function if you get stuck.
Implement setPosition
, setVelocity
, and getBoundingBox
These should be very straightforward.
Implement move
The move
function should update the sprite's position based on its velocity. If the sprite moves off the edge of the screen, it should wrap around to the other side. For example, if a sprite moves off the right edge of the screen, it should reappear on the left edge. The BoundingBox
class has a handy member function, wrapPoint(int& x, int& y)
, that you can use to help with this aspect of the implementation.
Testing
As you implement the member functions, you can test them using the provided sprite_test.cpp
file. This file contains a suite of tests that will check whether your Sprite
class behaves as expected. As provided, some of the tests are commented out as the functions they test hadn't been implemented yet. As you implement each function, you can uncomment the corresponding test(s) to check your work.
If your Makefile
is working correctly, you should be able to compile and run the tests using the commands:
cs70-make sprite-test
./sprite-test
(You can run regular make
if you prefer.)
Helpful Hints
These hints will help you write your
Sprite
class!
Static Constants
The Sprite
class has a public static constant:
static constexpr int MAX_SPRITE_DATA = 800;
This constant defines the fixed size for the character array in our program. You should use this constant in your code to avoid using “magic numbers”.
Why is it
static
?Because it's a property of the class itself, not of any particular instance of the class. All
Sprite
objects share the same value for this constant. We covered this in the mini-lesson on static class members.
Array Arithmetic
If it's not clear to you how to convert the 2-D coordinates into a 1-D index, consider drawing a picture of your sprite as a 2-D grid and numbering each item with its index in the array. What formula will turn the row and column into the index?
This is a general handy tip. Human beings aren't actually very efficient at thinking about things abstractly (it's too easy to get muddled), but we do well working with concrete examples. So if you're ever stuck on a problem, try working through a specific example to see if that helps you see the general case.
Out-of-Bounds Access
Think about what you want your getCharAt
function to do if it's given an invalid row or column. From the standpoint of the assignment specification, providing out-of-range coordinates is against the rules, and so the resulting behavior is undefined.
One option is to just assume that you'll always get good inputs—hey, it could happen!
But more robust choices can make debugging easier. For example, you might choose a deterministic character that you'll always return if you get bad inputs to the function. Or you might throw an exception or fail an assertion. Your choice here won't affect your grade, but might make your coding process smoother.
(When logged in, completion status appears here.)