Game Time

Spring 2019

The questions below are due on Sunday March 10, 2019; 11:59:00 PM.
 
You are not logged in.

If you are a current student, please Log In for full access to the web site.
Note that this link will take you to an external site (https://oidc.mit.edu) to authenticate, and then you will be redirected back to this page.

Back to Exercise 05

Over the next two weeks, we're making this cool game shown below to tap into that sweet cashflow that Fortnite has been raking in recently. It will build off of what we've already built in previous weeks:

CODE FOR THIS EXERCISE IS HERE!

1) Modifications to Ball Class

1.1) Object References

When we implemented our Ball class last week, you may note that quite a few things are linked to global variables/constants. This provides a conundrum for our use of Ball objects. If you look at our original implementation of it in Exercise 04 we "hard-coded" the class to work with a particularly named TFT_eSPI object called tft which was defined as the following at a global scale. (TFT stands for Thin Film Transistor, in case you were wondering...the technology of our LCDs)

TFT_eSPI tft = TFT_eSPI();  // Invoke library, pins defined in User_Setup.h

But what if we did the following in our code instead?

TFT_eSPI awesome_tft = TFT_eSPI();  // Invoke library, pins defined in User_Setup.h

In this situation now the object which accesses the screen and displays all our pretty images and messages is no longer called tft, but rather awesome_tft. As a result, everything that assumed that object name will fail. We do this for a few other variables as well, and this is a problem, since it prevents our class from being modular, requiring interdependency with other code chunks. This can get very confusing and frustrating.

The solution is that we need to remove that name dependency from internal to our Ball class and we'll do this with an object pointer which we'll set up to point to the globally declared TFT object. The first step in doing this will require the addition of a TFT_eSPI pointer in our Ball class definition like so (one extra line):

class Ball{
  //....code from before
  //....code from before
  TFT_eSPI *local_tft;
  public:
    //and so on...
}

Then we need to modify our class constructor statement so that it has a pointer to a TFT_eSPI object like so:

Ball(TFT_eSPI *tft_to_use, int rad=4, float ms=1, bool clr=true){}

Then in order to use this properly, we need to provide that variable a reference to TFT_eSPI object which is handed into the Ball constructor.

Ball(TFT_eSPI* tft_to_use, int rad=4, float ms=1){//other arguments being left out for brevity
  //rest of constructor stuff from before
  local_tft = tft_to_use; //our TFT member variable!
}

And as a result, in the global scope when we declare our instance of Ball, we hand it the address of the instance of our TFT handler, which in this case is awesome_ftf.

TFT_eSPI awesome_tft = TFT_eSPI();  // Like we've been doing for the most part:
Ball myball(&awesome_tft, 5, 2);//hand the address of the TFT object! again some other arguments left out for brevity.

Inside of our class now, when we want to access or call something with our LCD, we will have to do so through the object pointer, local_tft

. A question arises, however: Do we just do something like local_tft.drawCircle(); or *local_tft.drawCircle(); when we want to draw a circle on the screen, for example? The actual syntax utilizes what's called the member access operator, which looks like a right-facing arrow, and you apply this to the object pointer itself (not its dereferenced self). As a result, within the class definition, if we wanted to draw a circle, like we need to do in the step member function in the Ball class, we carry this out in the following way:

  local_tft->drawCircle(x_pos, y_pos, RADIUS, BKGND_CLR);
  moveBall();
  local_tft->drawCircle(x_pos, y_pos, RADIUS, BALL_CLR); 

This allows our class definitions to be much more flexible and self-contained...this will be key as we start to package our code up into modular libraries, which we'll probably do starting next week.

1.2) Removing Other Outside Dependencies

While we're at it, let's remove a few other hard-coded values that are relying on global variables. Instead let's make them utilize values specified upon declaration of the object. We can do this by expanding out our Ball constructor member function like shown below. Note the loop_period (duration of each discrete time step) and the playing-field boundaries are now specified:

    Ball(TFT_eSPI* tft_to_use, int dt, int rad = 4, float ms = 1,
         int ball_color = TFT_WHITE, int background_color = BACKGROUND,
         int left_lim = 0, int right_lim = 127, int top_lim = 0, int bottom_lim = 159) {
      x_pos = 64; //x position
      y_pos = 80; //y position
      x_vel = 0; //x velocity
      y_vel = 0; //y velocity
      x_accel = 0; //x acceleration
      y_accel = 0; //y acceleration
      local_tft = tft_to_use; //our TFT
      BALL_CLR = ball_color; //ball color
      BKGND_CLR = background_color;
      MASS = ms; //for starters
      RADIUS = rad; //radius of ball
      K_FRICTION = 0.15;  //friction coefficient
      K_SPRING = 0.9;  //spring coefficient
      LEFT_LIMIT = left_lim + RADIUS; //left side of screen limit
      RIGHT_LIMIT = right_lim - RADIUS; //right side of screen limit
      TOP_LIMIT = top_lim + RADIUS; //top of screen limit
      BOTTOM_LIMIT = bottom_lim - RADIUS; //bottom of screen limit
      DT = dt; //timing for integration
    }

which means creation of an instance of the Ball class could be done like so (or many other ways, specifying non-default values):

Ball ball1(&tft, LOOP_PERIOD, 6, 6, TFT_RED, BACKGROUND);

or if we had a new, smaller screen:

Ball ball1(&tft, LOOP_PERIOD, 9, 6, TFT_BLUE, TFT_BLACK, 0,50,0,50);

1.3) Public Position Access Function

As our Ball class is currently written, it is impossible for outside objects and code to have access to some private variables, in particular the x_pos and y_pos member variables. This will be important as we implement a game (where is the ball? Is it in a bad spot?). Now the cheap way to do this would be to make those variables public, but that can sometimes be dangerous1 Instead we'll provide a "chaperoned" way of getting those through the addition of two public member functions:

int getX(){
  return x_pos;
}
int getY(){
  return y_pos;
}

It is important to realize that these two functions are public, but what they return is private information. In a sense, they are allowing outsides "read-only" privelege with respect to certain member variables in the Ball object.

The final result of our more robust Ball class is shown below.

class Ball {
    float x_pos = 64; //x position
    float y_pos = 80; //y position
    float x_vel; //x velocity
    float y_vel; //y velocity
    float x_accel; //x acceleration
    float y_accel; //y acceleration
    TFT_eSPI* local_tft; //tft
    int BALL_CLR;
    int BKGND_CLR;
    float MASS; //for starters
    int RADIUS; //radius of ball
    float K_FRICTION;  //friction coefficient
    float K_SPRING;  //spring coefficient
    int LEFT_LIMIT; //left side of screen limit
    int RIGHT_LIMIT; //right side of screen limit
    int TOP_LIMIT; //top of screen limit
    int BOTTOM_LIMIT; //bottom of screen limit
    int DT; //timing for integration
  public:
    Ball(TFT_eSPI* tft_to_use, int dt, int rad = 4, float ms = 1,
         int ball_color = TFT_WHITE, int background_color = BACKGROUND,
         int left_lim = 0, int right_lim = 127, int top_lim = 0, int bottom_lim = 159) {
      x_pos = 64; //x position
      y_pos = 80; //y position
      x_vel = 0; //x velocity
      y_vel = 0; //y velocity
      x_accel = 0; //x acceleration
      y_accel = 0; //y acceleration
      local_tft = tft_to_use; //our TFT
      BALL_CLR = ball_color; //ball color
      BKGND_CLR = background_color;
      MASS = ms; //for starters
      RADIUS = rad; //radius of ball
      K_FRICTION = 0.15;  //friction coefficient
      K_SPRING = 0.9;  //spring coefficient
      LEFT_LIMIT = left_lim + RADIUS; //left side of screen limit
      RIGHT_LIMIT = right_lim - RADIUS; //right side of screen limit
      TOP_LIMIT = top_lim + RADIUS; //top of screen limit
      BOTTOM_LIMIT = bottom_lim - RADIUS; //bottom of screen limit
      DT = dt; //timing for integration
    }
    void step(float x_force = 0, float y_force = 0 ) {
      //your final step code from Exercise 02!!!
      local_tft->drawCircle(x_pos, y_pos, RADIUS, BKGND_CLR);
      moveBall();
      local_tft->drawCircle(x_pos, y_pos, RADIUS, BALL_CLR);
    }
    void reset(int x = 64, int y = 32) {
      x_pos = x;
      y_pos = y;
      x_vel = 0;
      y_vel = 0;
      x_accel = 0;
      y_accel = 0;
    }
    int getX() {
      return x_pos;
    }
    int getY() {
      return y_pos;
    }
  private:
    void moveBall() {
      //your final moveBall code from the last few weeks!
    }
};

Is the member function moveBall public or private?
public
private

Is the member function getX public or private?
public
private

Is the member variable x_pos public or private?
public
private

2) The Game Class

We're now going to start building more complex data structures that will use our previously-created classes. In this exercise, we'll create a Game class that has a Ball object contained within it.

class Game {
    int score;
    int food_x_pos; // Refers to center of the square
    int food_y_pos; // Refers to center of the square
    int food_half_width; // Like "radius", except it's a square
    int left_limit=0; //left side of screen limit
    int right_limit=127; //right side of screen limit
    int top_limit=0; //top of screen limit
    int bottom_limit=159; //bottom of screen limit
    int player_radius = 4;
    TFT_eSPI* game_tft; //object point to TFT (screen)
  public:
    Ball player; //make instance of "you"
    Game(TFT_eSPI* tft_to_use, int loop_speed):
      player(tft_to_use, loop_speed, player_radius, 1, TFT_RED, BACKGROUND,
              left_limit, right_limit, top_limit, bottom_limit){
      game_tft = tft_to_use;
      score = 0;
      food_x_pos = 40; // Initial x pos
      food_y_pos = 40; // Initial y pos
      food_half_width = 1;
    }
    void step(float x, float y) {
      player.step(x, y);
      int new_score = collisionDetect(); //checks if collision occurred (food found/lost)
      if (new_score != score) {//got some nomnoms!  (score can only increase currently so change in score means food found)
        score = new_score;
        //erase old food position
        game_tft->fillRect(food_x_pos - food_half_width, food_y_pos - food_half_width, 2 * food_half_width, 2 * food_half_width,BACKGROUND);
        food_x_pos = random(right_limit - left_limit - 2 * food_half_width)
                     + left_limit + food_half_width;
        food_y_pos = random(bottom_limit - top_limit - 2 * food_half_width)
                     + top_limit + food_half_width;
      }
      int top_left_x = food_x_pos - food_half_width;
      int top_left_y = food_y_pos - food_half_width;
      int side = 2 * food_half_width;
      //draw new food position
      game_tft->fillRect(top_left_x, top_left_y, side, side,TFT_GREEN);
      game_tft->setCursor(0,0,1);
      game_tft->print("Score: ");
      game_tft->println(score);
    }
  private:
    int collisionDetect() {
      //your code here (see exercise below)
    }
};

You should study this code to figure out how it is working, but at a high level the code will:

  • Place a piece of "food" at a random spot in the screen
  • Take accelerometer inputs and use that to move a Ball object
  • Check to see if the food and Ball object overlap...if so the food has been eaten, score is incremented, and new food piece randomly dropped.
  • The process continues forever

THE CODE FOR THIS ASSIGNMENT, INCLUDING THE TWO CLASS PROTOTYPES DISCUSSED ABOVE ARE LOCATED HERE

Your job will be to write the collisionDetect member function for the Game class, but before we get to that let's look at how the different classes/objects interplay. In particular several key lines to note are the creation and declaration of the Ball object within the Game constructor.

    Ball player; //make instance of "you"
    Game(TFT_eSPI* tft_to_use, int loop_speed):
      player(tft_to_use, loop_speed, player_radius, 1, TFT_RED, BACKGROUND,
             left_limit, right_limit, top_limit, bottom_limit) {
      game_tft = tft_to_use;

In the first line we create a Ball object known as player. However we have not initialized it yet since that only happens in the when we call the constructor of the Game class.After starting the definition on the Game constructor in the second line above, we instead we use a : and then call the constructor for the Ball object that we've already defined in our class definition on the third line. When we do this, we are free to hand in any inputs to it that are themselves being handed to the Game constructor; you can see we're doing that with the loop_speed and some other variables that Ball would need to know about. After that, all is good.

OK now your job is to fill out this new Game class by coding up the collisionDetect member function. This function takes in no arguments, and determines if the food and player are currently overlapping. If so, it returns a score+1, else it returns score, but does not itself change the score member variable). Make use of the member variables player_radius, food_x_pos, food_y_pos and food_half_width. Do not hardcode the values for these variables! For an example of how this member function is used, study the rest of the code!

For the sake of simplicity, our algorithm for collisionDetect will pretend that the ball is actually a square, with the same center point and a "half width" equal to player_radius (i.e. we use the circumscribed square). In general "half width" in our lingo refers to half the side length of the square. Recall that food_x_pos and food_y_pos refer to the center of the food and the x and y positions in the Ball class also refer to the center. We define two squares to be "overlapping" if there are points that are strictly inside both squares. So two squares that are perfectly adjacent are not considered overlapping.

Before you write any code, think of a few ways to programmatically determine if two squares overlap. Try to keep it simple!

int collisionDetect() { //your code here }

Back to Exercise 05


 
Footnotes

1That's like fixing the fact that you keep losing your keys, by just decided to never lock your doors. (click to return to text)



This page was last updated on Tuesday March 05, 2019 at 07:32:11 AM (revision d03027a).
 
Course Site powered by CAT-SOOP 14.0.4.dev5.
CAT-SOOP is free/libre software, available under the terms
of the GNU Affero General Public License, version 3.
(Download Source Code)
CSS/stryling from the Outboxcraft library Beauter, licensed under MIT
Copyright 2017