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 (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
Add Private Data Members to the Sprite
Class
Our Sprite
class needs to store three things:
- A 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 the 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 one might be most expedient. In your own code, go ahead and use
std::vector
if you want, but we won't use it until we know how it works inside.
Add Public Member Functions to the Sprite
Class
Our sprite class already has two constructors:
- 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 second parameterized constructor that takes an input stream instead of a filename, to make testing easier, but otherwise works the same way.
We've provided stub implementations of both constructors in sprite.cpp
.
It also needs:
- A
getBoundingBox
function that returns the sprite's bounding box. - A
setVelocity
function that sets the sprite's velocity. - A
setPosition
function that moves the sprite to a new location. - A
getCharAt(x,y)
function that returns the character at the specified position in the sprite's character array, wherex
andy
areint
s representing the column and row within the sprite. - 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.
You will need to add the declarations of these functions to sprite.hpp
and provide implementations in sprite.cpp
.
Hay! What about the private helper function
readFromStream
?You don't need to change it, but you can use it in your constructors to read the sprite data from a file or stream. And you'll need to implement it if you want your constructors to work! Talking of implementing…
Disabling the Default Constructor
It won't make any sense to have default Sprite
s, so disable the default constructor in sprite.hpp
by adding the line Sprite() = delete;
to the public section of the class.
Implement the Member Functions
You will need to implement all of the member functions described above in sprite.cpp
.
Implement the Constructors and Sprite Loading
The file already contains stub 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, your constructors must properly use the member-initialization list**. We covered this topic in Week 4, Lesson 1, and we remind you about how we expect you to use this C++ feature in Section 4.1 of the CS 70 Idioms Page.
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 treats 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 tests to check your work.
If you Makefile
is working correctly, you should be able to compile and run the tests using the command:
cs70-make sprite-test
./sprite-test
(or 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 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.)