CS 70

Homework 3: Define the Sprite Class

The New Oxford American Dictionary defines a sprite as

  1. An elf or fairy.
  2. 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:

  1. A fixed-size array of characters (of size MAX_SPRITE_DATA) that holds the characters that make up the sprite's image.
  2. A bounding box that defines the position and size of the sprite on the screen.
  3. 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 ints, one for each direction). Follow the CS 70 naming conventions for the good and consistent naming of data members.

  • Horse speaking

    Hay! Why is the array a fixed size? Can't the sprites vary in size?

  • LHS Cow speaking

    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.

  • Duck speaking

    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?

  • LHS Cow speaking

    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 ints giving the initial location of the Sprite.
  • 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, where x and y are ints 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 a BoundingBox 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.

  • Horse speaking

    Hay! What about the private helper function readFromStream?

  • LHS Cow speaking

    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 Sprites, 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.

  • RHS Cow speaking

    The testing code in sprite_test.cpp includes a test for getCharAt 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

  • LHS Cow speaking

    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".

  • Hedgehog speaking

    Why is it static?

  • LHS Cow speaking

    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?

  • RHS Cow speaking

    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.

To Complete This Part of the Assignment...

You'll know you're done with this part of the assignment when you've done all of the following:

(When logged in, completion status appears here.)